IL代码底层运行机制

** IL 代码底层运行机制 **

刘强

[email protected]

2003 年 5 月 8 日

大家都知道,和 Java 一样, C# 也是基于堆栈的语言。也许对一般人来说,底层的运行细节并不是很重要;但了解这些,对我们理解、运用 C# 是很有帮助的。下面,我就通过一个很简单的例子来说明 IL 代码的底层运行机制,也许对你会有一些帮助。

我给出的例子表面上看是一个实现整数相减功能的函数;实际上,我也不知道究竟能够干什么。在实际当中,我们的程序当中会有很多种数据类型、引用类型,为了简便起见,我所给出的示例代码只用了一种数据类型,如下所示:

** public int Sub(int i,int j) **

** { **

** int s; **

** int t = 0; **

** int r = 4; **

** s=i; **

** r =i – j; **

** r + =s + t; **

** return r; **

** } **

这段代码很简单,任何学过 C# 的人都能看懂。首先,传入两个整型变量 i 和 j ,然后经过内部运算,返回一个整型值。函数体内定义了三个局部变量 s , t , r ,分别用于保存自定义值以及结果。我们可以将它包装进一个类中,然后将它编译成 .dll 装配件。运用 VS.NET 自带的 ildasm 反汇编工具进行反汇编,我们得到如下 IL 代码:

** .method public hidebysig instance int32 Sub(int32 i, **

** int32 j) cil managed **

** { **

** // Code size 22 (0x16) **

** .maxstack 3 **

** .locals init (int32 V_0, int32 V_1, int32 V_2, int32 V_3) **

** ldc.i4.0 **

** stloc.1 **

** ldc.i4.4 **

** stloc.2 **

** ldarg.1 **

** stloc.0 **

** ldarg.1 **

** ldarg.2 **

** sub **

** stloc.2 **

** ldloc.2 **

** ldloc.0 **

** ldloc.1 **

** add **

** add **

** stloc.2 **

** ldloc.2 **

** stloc.3 **

** br.s IL_0014 **

** ldloc.3 **

** ret **

** } **

IL 代码也可以由 VS.NET 自带的 IL 编译工具 ilasm 编译为 .dll 装配件或 .exe 可执行文件。

这里,我要对 IL 中出现的符号作一下简单解释。以点号 ’.’ 开头的标号为伪指示代码,只起指示作用,最终不会被 JIT 编译为本地可执行代码,如“ .method ”,“ .locals ”等。而不带点号 ’.’ 的标号为 IL 汇编代码,它们在运行时将会被 JIT 编译为本地可执行代码,如“ ldarg.1 ”等。

每条语句究竟代表了什么样的操作,我们下面在详细讲解。注意:局部变量的下标从 0 开始,因此要注意我下面所说的“第零个局部变量”等的含义。

首先,让我们看一看函数体内的第一条语句: ** .maxstack 3 ** 。从其本身我们也可以猜出该语句说明堆栈的大小。暂且不表,且看下文。

第二句: ** .locals init (int32 V_0, int32 V_1, int32 V_2, int32 V_3) ** 。 V_0 、 V_1 、 V_2 和我们在 CS 源程序中定义的局部变量 s , t , r 一一对应,我们大概也能猜到这一句是完成局部变量初始化工作的,但为什么在这里是四个呢?我们明明只定义了三个变量的。那么这由 C# 编译器自动维护的第四个变量有何作用?也暂且不表,先看下文。

** ldc.i4.0 **

这条语句作用是在堆栈中载入常数, i4 表示该常数为双字长的 32 位整型数,初始值为 0 。“ ldc ”可以理解为“ load constant ”,加载常数。如图 a ,它完成的操作如同 (top)<=0 , top=top+1 。

stloc.1

这条语句作用是将当前栈顶元素存入第一个局部变量。 ’1’ 表示操作对象为第一个局部变量。“ stloc ”可以理解为“ store to local ”,保存局部变量。如图 b ,它完成的操作如同 top=top-1 , s<=(top) 。

ldc.i4.4

这条语句完成的操作如同 (top)<=4 , top=top+1 ,如图 c 。

stloc.2

这条语句完成的操作如同 top=top-1 , t<=(top) ,如图 d 。

ldarg.1

** ldarg.2 **

这两条语句作用是在堆栈中载入第一个参数( i )、第二个参数( j )(和局部变量不同,参数的指示下标从 1 开始)。它完成的操作如同 (top)<=i , top=top+1 ,( top ) <=j , top=top+1 ,如图 e 。其中,“ ldarg ”可以理解为“ load argument ”,加载参数。

sub

这条语句作用是将当前栈顶元素求反,再下加到第二个栈单元中,如图 f 。它完成的操作如同 top=top-1 , temp= - ( top ), top=top-1 ,( top ) = ( top ) + temp , top=top+1 。


top

0


top


top

4


top


top

j

i


top

i-j

(a) (b) (c) (d) ( e) (f)

stloc.2

这条语句作用是将当前栈顶元素存入第二个局部变量 (r) 。它完成的操作如同 top=top-1 , r<=(top) ,即 r=i-j ,如图

ldloc.2

** ldloc.0 **

** ldloc.1 **

这三条语句作用是分别将第二、第零个、第一个局部变量加载到堆栈上,如图 h 。“ ldloc ”可以理解为“ load local variable ”,加载局部变量。

add

** add **

add 的用发和 sub 一样,只不过不将当前栈顶元素求反,再下加到第二个栈单元中,连续操作两次。如图 i 、 j 。

stloc.2

将当前栈顶元素存入第二个局部变量。如图 k 。

ldloc.2

在堆栈中载入第二个局部变量( r ),如图 l 。


top


top

r


top


top

r+s+t


top

s+t

r


top

t

s

r

(g) (h) (i) (j) (k) (l)

stloc.3

将当前栈顶元素存入第三个局部变量,亦即保存返回值,如图 m 所示。

br.s IL_0014

跳转到下一句( ldloc.3 )。如图 n 所示。

** ldloc.3 **

** ret **

将第三个局部变量(也即由编译器自动维护的变量)加载到堆栈上,然后返回,如图 o 。从这里我们也可以看出, 1 、第三个变量和返回值类型相同; 2 、扫尾后、返回前将第三个局部变量加载至堆栈。这可以让我们确定:第三个变量用于存储返回值。我们还要弄清楚为什么要专门分配一个局部变量来存储返回值,这一点在后面会有说明。


top


top


top

V_3

(m) (n) (o)

综观图 a~o ,我们回发现,整个函数过程所用到的最大栈数目就是 3 ,这也就不难理解第一条语句 ** .maxstack 3 ** 了。

现在,还有一点让人迷惑的是为什么要引入变量 V_3 ?如上例中,倒数第二条指令 ** ldloc.3 ** 也可以由 ** ldloc.2 ** 代替,因为我们所要的结果就是存储在第二个变量 r 中的,这不是浪费空间嘛。要注意了,并不所有的返回值都保存在局部变量中的。

有可能我们将参数直接返回,或者将类成员变量返回,如:

public int Laxi(int x)

{

return x; // 直接将参数返回

}

或者是 int age;...

public int GetAge()

{

return age; // 返回在类中定义的字段

}

或者是直接返回一个表达式:

public int GetInteger()

{

return age+4*6/2;

}

则必须进行这一步转换,以 return r 为例: ldloc.x -> stloc.y ->ldloc.y ->ret . 因为在以上三种情况当中,返回值都不存储在局部变量当中。

Published At
Categories with Web编程
Tagged with
comments powered by Disqus