一步一步教你读懂NET中IL

.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。

本文章以一个实际的例子,让你了解堆叠式 VM 的运作原理,并对 .NET IL(Intermediate Language)有最基本的领略。

下面是一个简单的 C# 原始码:

    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

.method public hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       36 (0x24)
  .maxstack  2
  .locals init ([0] int32 i,
           [1] int32 j,
           [2] int32 k,
           [3] int32 answer)
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  stloc.0
  IL_0003:  ldc.i4.2
  IL_0004:  stloc.1
  IL_0005:  ldc.i4.3
  IL_0006:  stloc.2
  IL_0007:  ldloc.0
  IL_0008:  ldloc.1
  IL_0009:  add
  IL_000a:  ldloc.2
  IL_000b:  add
  IL_000c:  stloc.3
  IL_000d:  ldstr      "i+j+k="
  IL_0012:  ldloc.3
  IL_0013:  box        [mscorlib]System.Int32
  IL_0018:  call       string [mscorlib]System.String::Concat(object, object)
  IL_001d:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0022:  nop
  IL_0023:  ret
} // end of method Test::Main

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

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 組合码。

原文转自:http://www.cnblogs.com/Leo_wl/p/3181108.html

谢谢原文博主分享精彩文章

时间: 2024-11-05 00:12:35

一步一步教你读懂NET中IL的相关文章

教你读懂redmine中的甘特图

Redmine是用Ruby开发的基于web的项目管理软件,他可以自动绘制甘特图,对于我们了解项目进度有很重要的帮助.但很多新人小白在使用redmine时,就是当成一个简单的备忘录来使用,对于甘特图神马的根本就不care,那么如何正确的使用甘特图呢?如何读懂redmine中的甘特图呢? Redmine是可以根据建立的问题的开始时间.结束时间和完成百分比自动绘制甘特图的.所以我们要使用redmine的甘特图,必须在新建问题时根据项目计划设置好开始时间,预估结束时间,每天在结束一天的工作后,注意及时更

一篇文章教你读懂Makefile

makefile很重要      什么是makefile?或许很多Winodws的程序员都不知道这个东西,因为那些Windows的IDE都为你做了这个工作,但我觉得要作一个好的和professional的程序员,makefile还是要懂.这就好像现在有这么多的HTML的编辑器,但如果你想成为一个专业人士,你还是要了解HTML的标识的含义.特别在Unix下的软件编译,你就不能不自己写makefile了,会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力.因为,makefil

教你读懂网络请求的瀑布图

本文题目来源于 阮一峰大神的微博 这是原文 我们目前都知道, 一个网页的访问速度对于SEO和用户体验来说非常的重要. 速度快的网站会获得更高的搜索引擎排名, 用户也能浏览其更多的网页;简单说来,聪明的SEO不仅仅优化其网站内容,同时也要考虑如何提升网站的性能. 正如我们在之前这篇文章所讨论的, 你可以使用WebPageTest 这个免费的工具来对你的网站性能进行优化. WebPageTest输出给你的最有用的内容之一, 是一个叫做瀑布图的东西. 瀑布图展现了浏览器为渲染网页而加载的所有的资源,

读懂Java中的Socket编程(转)

Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一.如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实现的.本文会介绍一下基于TCP/IP的Socket编程,并且如何写一个客户端/服务器程序. 餐前甜点 Unix的输入输出(IO)系统遵循Open-Read-Write-Close这样的操作范本.当一个用户进程进行IO操作之前,它需要调用Open来指定并获取待操作文件或设备读取或写入的权限.一旦IO操作对象被打开,那么这个用户进程可以对这个

读懂Java中的Socket编程

Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一.如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实现的.本文会介绍一下基于TCP/IP的Socket编程,并且如何写一个客户端/服务器程序. 餐前甜点 Unix的输入输出(IO)系统遵循Open-Read-Write-Close这样的操作范本.当一个用户进程进行IO操作之前,它需要调用Open来指定并获取待操作文件或设备读取或写入的权限.一旦IO操作对象被打开,那么这个用户进程可以对这个

教你看懂GERBER中的钻孔(.txt)文件

PCB在制作的时候也会导出相应钻孔的坐标位置,但是发现网上很少有关于这方面的资料,而一些项目中,可能就会用到钻孔的坐标信息,今天就抛下砖. M48 ;Layer_Color=9474304  % 图层颜色  数值转换成十六进制就是我们熟悉的color值 ;FILE_FORMAT=2:5     %数据格式,5位整数,2位小数,整数部分可能会出现2位,3位,4位,5位,小数部分只能是2位. INCH,TZ                         %单位inch ;TYPE=PLATED  

一文读懂BERT中的WordPiece

1. 前言 2018年最火的论文要属google的BERT,不过今天我们不介绍BERT的模型,而是要介绍BERT中的一个小模块WordPiece. 2. WordPiece原理 现在基本性能好一些的NLP模型,例如OpenAI GPT,google的BERT,在数据预处理的时候都会有WordPiece的过程.WordPiece字面理解是把word拆成piece一片一片,其实就是这个意思. WordPiece的一种主要的实现方式叫做BPE(Byte-Pair Encoding)双字节编码. BPE

一次读懂mybatis中的缓存机制

缓存功能针对于查询(没听说果UPDATE,INSERT语句要缓存什么,都是直接执行的) 默认情况下,mybatis会启用一级缓存. 如果使用同一个session对象调用了相同的SELECT语句,则直接会从缓存中返回结果,而不是再查询一次数据库. 注意:session调用commit或close方法后,这个session中的一级缓存就会被清空 例如: 根据日志输出可以看出,下面代码只会发出一条sql查询语句 SqlSession sqlSession = MyBatisSqlSessionFact

[转] 一文读懂BERT中的WordPiece

From: https://www.cnblogs.com/huangyc/p/10223075.html 1. 前言 2018年最火的论文要属google的BERT,不过今天我们不介绍BERT的模型,而是要介绍BERT中的一个小模块WordPiece. 回到顶部 2. WordPiece原理 现在基本性能好一些的NLP模型,例如OpenAI GPT,google的BERT,在数据预处理的时候都会有WordPiece的过程.WordPiece字面理解是把word拆成piece一片一片,其实就是这