.NET值类型变量“活”在哪?

news/2024/6/18 19:05:59
 
.NET值类型变量“活”在哪个堆栈中?
——MSIL学习笔记(一)
 
  金旭亮
       不管是什么语言编的.NET程序,最后都会被各自的编译器编译成MSIL。当程序运行时,.NET JIT编译器从程序集中读入IL指令并将其动态编译为可被本地CPU执行的机器指令再执行。
       程序集中的IL代码以二进制方式存在,人阅读起来相当不便,正如传统的Win32程序可以被反汇编成汇编程序,.NET程序集中的IL代码也可以被反汇编成易于阅读的IL汇编程序。如果您愿意的话,可以用任意一个文本编辑器直接撰写IL汇编源代码,然后使用ilasm.exe程序将其编译为包含二进制形式的IL指令。CLR只能执行二进制的IL指令。
       .NET SDK的另一个工具ildasm.exe可以用于将一个程序集反汇编为IL程序,在学习.NET时,这个工具非常有用,可以展示出高级语言(如C#和VB.NET)编写的程序是如何被CLR执行的。
       然而,相比C#和VB.NET的资料满天飞,MSIL的技术资料少得可怜。我能够查阅的只有MSDN中有关IL指令的文档(还只是针对Reflection.Emit名字空间中的类的),以及一本由Serge Lidin著的《inside Microsoft .NET IL assembler》, Serge Lidin是汇编器ilasm.exe工具的主要开发者,因此,他的书应具有相当的权威性,然而,这位技术牛人的写作水平实在不敢恭维,整本书象是一本参考手册。此书国内引进了中文版,然而翻译得很不好。幸运的是其光盘中附上了英文原版,实乃国人之大幸。
       IL可以看成是一个“面向对象的汇编语言”,它提供了许多指令直接对对象进行操作,比如newobj指令创建对象,box指令进行装箱等。
       IL指令的一个最重要特性是它是基于堆栈的。几乎每一条指令都要与堆栈打交道:或者向堆栈中Push一些数据,或者从中Pop一些数据。
       请看以下C#代码段:
    class Program
    {
        static void Main(string[] args)
        {
            int i = 100;
            int j = 200;
            int reslut = i + j;
        }
    }
C#编译器将生成以下IL指令,其功能我在注释中有详细说明:
.method private hidebysig static void Main(string[] args) cil managed
{
 .entrypoint
 // 代码大小        15 (0xf)
 .maxstack 2
 .locals init ([0] int32 i,
           [1] int32 j,
           [2] int32 reslut)
 IL_0000: nop
 IL_0001: ldc.i4.s    100     //将100压入堆栈
 IL_0003: stloc.0      //从堆栈中弹出先前压入的100,传给局部变量i
 IL_0004: ldc.i4      0xc8     //将200压入堆栈
 IL_0009: stloc.1      //从堆栈中弹出先前压入的200,传给局部变量j
 IL_000a: ldloc.0      //将局部变量i的值压入堆栈
 IL_000b: ldloc.1      //将局部变量j的值压入堆栈
 IL_000c: add          //连继弹出两个整数,相加得300,又压入堆栈
 IL_000d: stloc.2      //从堆栈中弹出结果,保存到局部变量reslut中
 IL_000e: ret          //返回指令
} // end of method Program::Main
 
       可以看到,所有的指令都涉及到堆栈。
       然而,我在研究IL汇编程序的时候,却被“堆栈”两个字弄糊涂了。
      几乎所有的C#书,都说值类型变量是生存在堆栈中,当函数结束时会自动销毁。那么,这里的堆栈与上述IL代码中的堆栈是不是一回事?
      请看上述IL程序中有一个MaxStack指令,查看资料,得知其含义是为evaluation stack保留两个槽(slot),注意,这里的堆栈英文原文是evaluation stack,MSDN中文版译为“计算堆栈”,slot可用于存放值对象,大小是可变的。换句话说,evaluation stack中的每一个slot可以存放一个值对象(对象引用也可看成是一种“特殊”的值变量,其值代表内存地址)或各种CLR直接支持的基本类型数据。
       从上述IL程序中可以很明显地看到,局部变量i,j和result绝不会生存于evaluation stack,因为它只有2个slot,而我们有3个变量。那它们“活在”在哪儿?
        IL程序中引人注目的一句是locals init指令,这提醒我们函数拥有另一块内存区域专用于存放局部变量,所以,声明为局部变量的值类型并不“活”在evaluation stack中。那么,为何所有的 C#书(包括大名鼎鼎的Jeffrey Richter所著之《.NET框架程序设计》)都说值类型变量“活”在堆栈中?此堆栈在哪?至少有一点可以肯定,这个堆栈不会指的是evaluation stack。
        用ildasm.exe查看程序集清单(manifest),发现其中有一句:
       .stackreserve 0x00100000
      上述语句让CLR在装入程序集时保存1M的堆栈空间,这个空间供托管进程的托管线程使用,称为线程堆栈(Thread Stack)。既是线程堆栈,自然与线程相关,由于.NET托管进程可以创建多个托管线程,因此,每个线程也应该有自己的堆栈(Jeffrey Richter说也是1M,查看也是这位老先生写的《Windows核心编程》,说在Win2000在创建线程时其堆栈大小是可调整的)。
       .NET下每个托管线程都对应着一个线程函数,因此函数中定义的局部变量是在它拥有的线程堆栈中分配,而IL程序中的maxstack指令则从这一个1M的线程堆栈中再划出一块空间来作为evaluation stack。
      考虑一下函数调用的问题。
      IL使用call和callvirt两条指令调用特定类型所提供的方法。这就有一个函数参数传送的问题。以call指令为例,MSDN说在调用call指令之前,要将所有的实参压入evaluation stack,然后call指令再将其弹出,之后控制才会转到被调用的函数,而当被调用的函数执行完毕时,ret指令负责“将函数的返回值”从“被调用者的堆栈”(callee’s evaluation stack)复制到“调用者堆栈”(caller evaluation stack)中。您看MSDN文档中居然又出现了两个堆栈,是否有点晕了吗?
        查看Serge Lidin的书,他给出了这样一个图:
 
   
 
        如上图所示:CLR会给每一个被调用的方法分配三块内存,除了上面讲到的两块(Evaluation stack和局部变量表Local Variable table),还有一块是参数表(Argument table)。
      问题终于明晰了,call指令完成的工作应该是这样的:
 
     调用者按要调用函数的参数准备好实参,将它们压入“自己的”evaluation stack中,然后,call指令执行,它从调用者的evaluation stack弹出这些参数,放入被调用函数的Argument Table中。一切准备工作就绪,这时才开始执行被调用函数的第一条IL指令。
     当被调用函数执行完毕,如果有返回值,这个值应该被放在被调用函数自己的evaluation stack中(因为IL指令总是与堆栈打交道),然后,ret指令(每个函数最后一定是这条指令)将其弹出,再压入调用者的evaluation stack中,完成这一工作之后,执行流程转回到调用者。
      因此,线程每调用一个函数,将导致图中所示的三块区域在1M的线程堆栈中分配给调用函数,对于递归调用的情况,后调用的函数占用的内存区域将“压”在其调用者内存区域之上,每执行完一个函数,对应的栈顶指针移动一个位移(大小刚好等于此函数先前所占用的内存),从而导致这些内存被释放,其中的局部变量不再有效。
 
    分析.NET程序的IL指令还会得到一些有趣的结果,后面我会有更多的文章与网友们进行技术交流。
 
注:由于手头的资料不足, 此文所述内容仅是本人对CLR内部运行机理的一个推测,如有错误,敬请指正。by the way,望有网友能提供更多的MSIL技术资料信息,在此谢谢了。:-)
转载请注明作者及出处。
 
 

Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1451065



http://www.niftyadmin.cn/n/3659482.html

相关文章

kaldi提取mfcc特征出错Waveform and config sample Frequency mismatch: 16000 .vs 8000

kaldi提取mfcc特征出错,查看log日志文件发现是wav音频频率不匹配: Waveform and config sample Frequency mismatch: 16000 .vs 8000 (use --allow-downsampletrue to allow downsampling the waveform).这是因为输入的wav的采样频率是16000&#xff0…

高挂CSDN论战免战牌

自从我在CSDN的BLOG发表《我被中国计算机教育的现实打败了》系列文章之后,引发激烈争论,前期主战场主要在我的BLOG上。后期,CSDN上的名人袁峰先生先后在CSDN社区“程序人生”论坛发表《我想对金旭亮说》,《解读“一个普通IT人的十…

我对袁峰先生及众网友观点的回应

**********说明:过了两天来看BLOG,又是一堆的贴子。真没辄。免战牌挂得再高也没用,许多人还就喜欢上论战了。我的BLOG成了战场,许多人在此进行拉锯战。我想还是花点时间把问题说得再清楚些罢。也许说清楚了也就无话了。也希望网友…

python imp.load_source转importlib

import importlib, importlib.machinery importlib.machinery.SourceFileLoader(dml, steps/data/data_dir_manipulation_lib.py)

回应我的指责:学生来邮件把我痛骂了一通

公告:本来不打算再多说话,但看了一些人的观点,还是忍不住再说点什么。个人修养不到家,明知直言犯忌,还是直言,请看:《教师判分如何“高抬贵手”?》********************************…

我对《我被中国计算机教育的现实打败了》整个事件的反思

本来不打算再多说话,但看了一些人的观点,还是忍不住再说点什么。个人修养不到家,明知直言犯忌,还是直言,请看:《教师判分如何“高抬贵手”?》我对《我被中国计算机教育的现实打败了》整个事件的…

教师判分如何“高抬贵手”?

教师判分如何“高抬贵手”?——许多一线教师心里的话,我代他们说!金旭亮看到这么多人都说我应该放手让那些对计算机不感兴趣的人过关。我不禁苦笑,他们不太了解情况,还是把这些少数几个不及格学生的实情想得太好了。OO…

致《编程的奥秘》新老读者

致《编程的奥秘》新老读者:电子工业出版社博文视点公司打算举办一个“《编程的奥秘》2006金秋读书季”活动,将活动文告发给了我。作为作者,非常感谢博文视点公司的各位员工对本书所做的工作。《编程的奥秘》出版以来,我收到了数百…