欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

一步步教你读懂NET中IL(图文详解)

程序员文章站 2024-02-29 13:41:04
接触net也有1年左右的时间了,net的内部实现对我产生了很大的吸引力。个人觉得:能对这些底部的实现进行了解和熟练的话,对以后自己写代码是有很大帮助的,好了,废话不多说,请...

接触net也有1年左右的时间了,net的内部实现对我产生了很大的吸引力。个人觉得:能对这些底部的实现进行了解和熟练的话,对以后自己写代码是有很大帮助的,好了,废话不多说,请看下边:

.net clr 和 java vm 都是堆叠式虚拟机器(stack-based vm),也就是說,它們的指令集(instruction set)都是採用堆叠运算的方式:执行时的资料都是先放在堆叠中,再进行运算。javavm 有約 200 個指令(instruction),每個指令都是 1 byte 的 opcode(操作码),后面接不等数目的参数;.net clr 有超過 220個指令,但是有些指令使用相同的 opcode,所以 opcode 的数目比指令数略少。特別注意,.net 的 opcode 長度並不固定,大部分的 opcode 長度是 1 byte,少部分是 2 byte。

下面是一個简单的 c# 原始码:                

复制代码 代码如下:

using system;
public class test {
    public static void main(string[] args) {
        int i=1;
        int j=2;
        int k=3;
        int answer = i+j+k;
        console.writeline("i+j+k="+answer);
    }
}

將此原始码编译之后,可以得到一個 exe的程序。我們可以通过 ildasm.exe(图-0) 來反编译 exe 以观察il。我將 main() 的 il 反编译条列如下,這裡共有十八道il 指令,有的指令(例如 ldstr 与 box)后面需要接参数,有的指令(例如 ldc.i4.1 與与add)后面不需要接参数。一步步教你读懂NET中IL(图文详解)
图-0
ldc.i4.1
stloc.0
ldc.i4.2
stloc.1
ldc.i4.3
stloc.2
ldloc.0
ldloc.1
add
ldloc.2
add
stloc.3
ldstr      "i+j+k="
ldloc.3
box        [mscorlib]system.int32
call       string [mscorlib]system.string::concat(object, object)
call       void [mscorlib]system.console::writeline(string)
ret

此程式执行時,关键的记忆体有三种,分別是:

1、managed heap:這是动态配置(dynamic allocation)的记忆体,由 garbage collector(gc)在执行時自動管理,整個process 共用一個 managed heap。

2、call stack:這是由 .net clr 在执行時自動管理的记忆体,每個 thread 都有自己专属的 call stack。每呼叫一次 method,就会使得call stack 上多了一個 record frame;呼叫完毕之后,此 record frame 会被丢弃。一般來說,record frame 內记录着 method 参数(parameter)、返回位址(return address)、以及区域变数(local variable)。java vm 和 .net clr 都是使用 0, 1, 2… 编号的方式來識別区别变数。

3、evaluation stack:這是由 .net clr 在执行時自動管理的记忆体,每個 thread 都有自己专属的 evaluation stack。前面所謂的堆叠式虚拟机器,指的就是這個堆叠。

后面有一連串的示意图,用來解說在执行時此三种记忆体的变化。首先,在進入 main() 之后,尚未执行任何指令之前,记忆体的狀況如图1 所示:

一步步教你读懂NET中IL(图文详解)

图1                

接着要执行第一道指令 ldc.i4.1。此指令的意思是:在 evaluation stack 置入一個 4 byte 的常数,其值為 1。执行完此道指令之后,记忆体的变化如图2 所示:

ldc.i4.1:表示加载一个值为1到堆栈中,该条指令的语法结构是:
ldc.typevalue:ldc指令加载一个指定类型的常量到stack.
ldc.i4.number:ldc指令更加有效.它传输一个整型值-1以及0到8之间的整数给计算堆栈

一步步教你读懂NET中IL(图文详解)

图2       

接着要执行第二道指令 stloc.0。此指令的意思是:从 evaluation stack 取出一個值,放到第 0 号变数(v0)中。這裡的第 0 号变数其实就是原始码中的i。执行完此道指令之后,记忆体的变化如图3 所示:

一步步教你读懂NET中IL(图文详解)

图3                

后面的第三道指令和第五道指令雷同於第一道指令,且第四道指令和第六道指令雷同於第二道指令。為了节省篇幅,我不在此一一贅述。提醒大家第 1 号变数(v1)其实就是原始码中的 j,且第 2 号变数(v2)其实就是源码中的 k。图4~7 分別是执行完第三~六道指令之后,记忆体的变化图:

一步步教你读懂NET中IL(图文详解)

图4                

一步步教你读懂NET中IL(图文详解)

图5

一步步教你读懂NET中IL(图文详解)
图6

一步步教你读懂NET中IL(图文详解)
图7

接着要执行第七道指令 ldloc.0 以及第八道指令 ldloc.1:分別將 v0(也就是 i)和 v1(也就是 j)的值放到 evaluation stack,這是相加前的准备動作。图8 與图9 分別是执行完第七、第八道指令之后,记忆体的变化图:

一步步教你读懂NET中IL(图文详解)

图8

一步步教你读懂NET中IL(图文详解)
图9

接着要执行第九道指令 add。此指令的意思是:从 evaluation stack 取出兩個值(也就是 i 和 j),相加之后將結果放回 evaluation stack 中。执行完此道指令之后,记忆体的变化如图10 所示:

一步步教你读懂NET中IL(图文详解)
图10

接着要执行第十道指令 ldloc.2。此指令的意思是:分別將 v2(也就是 k)的值放到 evaluation stack,這是相加前的准备動作。执行完此道指令之后,记忆体的变化如图11 所示:

一步步教你读懂NET中IL(图文详解)
图11

接着要执行第十一道指令 add。从 evaluation stack 取出兩個值,相加之后將結果放回 evaluation stack 中,此為 i+j+k 的值。执行完此道指令之后,记忆体的变化如图12 所示:

一步步教你读懂NET中IL(图文详解)
图12

接着要执行第十二道指令 stloc.3。从 evaluation stack 取出一個值,放到第 3 号变数(v3)中。這裡的第3号变数其实就是原始码中的 answer。执行完此道指令之后,记忆体的变化如图13 所示:

一步步教你读懂NET中IL(图文详解)
图13

接着要执行第十三道指令 ldstr "i+j+k="。此指令的意思是:將 "i+j+k=" 的 reference 放進 evaluation stack。执行完此道指令之后,记忆体的变化如图14 所示:

一步步教你读懂NET中IL(图文详解)
图14

接着要执行第十四道指令 ldloc.3。將 v3 的值放進 evaluation stack。执行完此道指令之后,记忆体的变化如图15 所示:

一步步教你读懂NET中IL(图文详解)
图15

接着要执行第十五道指令 box [mscorlib]system.int32,从此处可以看出,int到string实际是进行了装箱操作的,所以会有性能损失,可以在以后的编码中减少装箱操作来提高性能。此指令的意思是:从 evaluation stack 中取出一個值,將此 value type 包裝(box)成為 reference type。执行完此道指令之后,记忆体的变化如图16 所示:

一步步教你读懂NET中IL(图文详解)
图16

接着要执行第十六道指令 call string [mscorlib] system.string::concat(object, object)。此指令的意思是:从 evaluation stack 中取出兩個值,此二值皆為 reference type,下面的值当作第一個参数,上面的值当作第二個参数,呼叫 mscorlib.dll 所提供的 system.string.concat() method 來將此二参数進行字串接合(string concatenation),將接合出來的新字串放在 managed heap,將其 reference 放進 evaluation stack。值得注意的是:由於 system.string.concat() 是 static method,所以此處使用的指令是 call,而非 callvirt(呼叫虚拟)。执行完此道指令之后,记忆体的变化如图17 所示:

一步步教你读懂NET中IL(图文详解)
图17

請注意:此時 managed heap 中的 int32(6) 以及 string("i+j+k=") 已經不再被參考到,所以变成垃圾,等待 gc 的回收。

接着要执行第十七道指令 call void [mscorlib] system.console::writeline(string)。此指令的意思是:从 evaluation stack 中取出一個值,此值為 reference type,將此值当作参数,呼叫 mscorlib.dll 所提供的 system.console.writeline() method 來將此字串显示在 console 視窗上。system.console.writeline() 也是 static method。执行完此道指令之后,记忆体的变化如图18 所示:

一步步教你读懂NET中IL(图文详解)图18

接着要执行第十八道指令 ret。此指令的意思是:結束此次呼叫(也就是 main 的呼叫)。此時会檢查 evaluation stack 內剩下的資料,由於 main() 宣告不需要传出值(void),所以 evaluation stack 內必須是空的,本范例符合這樣的情況,所以此時可以順利結束此次呼叫。而 main 的呼叫一結束,程式也随之結束。执行完此道指令之后(且在程式結束前),记忆体的变化如图19 所示:

一步步教你读懂NET中IL(图文详解)图19

通过此范例,读者应该可以对于 il 有最基本的认识。对 il 感兴趣的读者应该自行阅读 serge lidin 所著的《inside microsoft .net il assembler》(microsoft press 出版)。我认为:熟知 il 每道指令的作用,是 .net 程式員必备的知识。.net 程式員可以不会用 il assembly 写程式,但是至少要看得懂 ildasm 反编译出來的 il 組合码。