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

接触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)后面不需要接参数。


图-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 所示:

图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之间的整数给计算堆栈

图2

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

图3

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

图4

图5


图6


图7

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

图8


图9

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


图10

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


图11

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


图12

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


图13

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


图14

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


图15

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


图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 所示:


图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 所示:

图18

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

图19

通过此范例,读者应该可以对于 IL 有最基本的认识。对 IL 感兴趣的读者应该自行阅读 Serge Lidin 所著的《Inside Microsoft .NET IL Assembler》(Microsoft Press 出版)。我认为:熟知 IL 每道指令的作用,是 .NET 程式員必备的知识。.NET 程式員可以不会用 IL Assembly 写程式,但是至少要看得懂 ILDASM 反编译出來的 IL 組合码。

(0)

相关推荐

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

    接触NET也有1年左右的时间了,NET的内部实现对我产生了很大的吸引力.个人觉得:能对这些底部的实现进行了解和熟练的话,对以后自己写代码是有很大帮助的,好了,废话不多说,请看下边: .NET CLR 和 Java VM 都是堆叠式虚拟机器(Stack-Based VM),也就是說,它們的指令集(Instruction Set)都是採用堆叠运算的方式:执行时的资料都是先放在堆叠中,再进行运算.JavaVM 有約 200 個指令(Instruction),每個指令都是 1 byte 的 opcode

  • Eclipse中导入Maven Web项目并配置其在Tomcat中运行图文详解

    今天因为实习的关系需要讲公司已经开发的项目导入进Eclipse,而公司的项目是用Maven来构建的所以,需要将Maven项目导入进Eclipse下. 自己因为没有什么经验所以搞了得两个多小时,在这里和大家分享一下自己的经验已经在这之中遇到的一些问题. 首先我通过svn将公司的项目checkout到了本地. 因为Maven遵循的是规约比配置重要的原则,所以Maven项目的结构一般是进入目录后是一个pom.xml文件和一个src文件夹,当然可能还存在一些README之类的这些都不重要,最关键的就是p

  • 一文读懂ava中的Volatile关键字使用

    在本文中,我们会介绍java中的一个关键字volatile. volatile的中文意思是易挥发的,不稳定的.那么在java中使用是什么意思呢? 我们知道,在java中,每个线程都会有个自己的内存空间,我们称之为working memory.这个空间会缓存一些变量的信息,从而提升程序的性能.当执行完某个操作之后,thread会将更新后的变量更新到主缓存中,以供其他线程读写. 因为变量存在working memory和main memory两个地方,那么就有可能出现不一致的情况. 那么我们就可以使

  • 一文读懂JAVA中HttpURLConnection的用法

    针对JDK中的URLConnection连接Servlet的问题,网上有虽然有所涉及,但是只是说明了某一个或几个问题,是以FAQ的方式来解决的,而且比较零散,现在对这个类的使用就本人在项目中的使用经验做如下总结: 1:> URL请求的类别: 分为二类,GET与POST请求.二者的区别在于: a:) get请求可以获取静态页面,也可以把参数放在URL字串后面,传递给servlet, b:) post与get的不同之处在于post的参数不是放在URL字串里面,而是放在http请求的正文内. 2:>

  • 5 分钟读懂Python 中的 Hook 钩子函数

    1. 什么是Hook 经常会听到钩子函数(hook function)这个概念,最近在看目标检测开源框架mmdetection,里面也出现大量Hook的编程方式,那到底什么是hook?hook的作用是什么? what is hook ?钩子hook,顾名思义,可以理解是一个挂钩,作用是有需要的时候挂一个东西上去.具体的解释是:钩子函数是把我们自己实现的hook函数在某一时刻挂接到目标挂载点上. hook函数的作用 举个例子,hook的概念在windows桌面软件开发很常见,特别是各种事件触发的机

  • IDEA教程之Activiti插件图文详解

    本文作者:Spring_ZYL 文章来源:https://blog.csdn.net/gozhuyinglong 版权声明:本文版权归作者所有,转载请注明出处 一.安装Activiti插件 1.搜索插件 点击菜单[File]-->[Settings...]打开[Settings]窗口. 点击左侧[Plugins]按钮,在右侧输出"actiBPM",点击下面的[Search in repositories]链接会打开[Browse Repositories]窗口. 2.开始安装 进入[Browse

  • linux 中ls命令详解

    ls命令用来显示目标列表,在Linux中是使用率较高的命令.ls命令的输出信息可以进行彩色加亮显示,以分区不同类型的文件. 语法 ls(选项)(参数) 选项 -a:显示所有档案及目录(ls内定将档案名或目录名称为“.”的视为影藏,不会列出): -A:显示除影藏文件“.”和“..”以外的所有文件列表: -C:多列显示输出结果.这是默认选项: -l:与“-C”选项功能相反,所有输出信息用单列格式输出,不输出为多列: -F:在每个输出项后追加文件的类型标识符,具体含义:“*”表示具有可执行权限的普通文

  • Java中的HashSet详解和使用示例_动力节点Java学院整理

    第1部分 HashSet介绍 HashSet 简介 HashSet 是一个没有重复元素的集合. 它是由HashMap实现的,不保证元素的顺序,而且HashSet允许使用 null 元素. HashSet是非同步的.如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步.这通常是通过对自然封装该 set 的对象执行同步操作来完成的.如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来"包装" set.

  • Servlet中操作文件详解及实例

    Servlet中操作文件详解及实例 因为Servlet本来就是一个.Java文件,因此servlet中操作文件和普通java文件操作文件是一样的. 读取文件主要代码: FileReader f=new FileReader("f:\\lissdy.txt"); BufferedReader bw=new BufferedReader(f); 读出一行数据 String num=bw.readLine(); 注意一定要关闭文件流 bw.close(); 写文件的方法与之类似,具体代码为:

  • java集合中的list详解

    1.List接口 该接口定义的元素是有序的且可重复的.相当于数学里面的数列,有序可重复 booleanaddAll(intindex,Collection<?extendsE>c);将指定集合中所有元素,插入至本集合第index个元素之后defaultvoidreplaceAll(UnaryOperatoroperator);替换集合中每一个元素值defaultvoidsort(Comparator<?superE>c);给集合中的元素进行排序Eget(intindex);获取集合

随机推荐