(转) 读懂IL
引言
转自园子里的一片关于IL的好文,分享的同时,方便自己今后查阅。
原文链接:http://www.cnblogs.com/brookshi/p/5225801.html
------
略过作者调侃内容,直接进入干活部分!
将IL用法分为三类,如下。
第一类 :直观型
这一类的特点是一看名字就知道是干嘛的,不需要多讲,如下:
名称 |
说明 |
Add |
将两个值相加并将结果推送到计算堆栈上。 |
Sub |
从其他值中减去一个值并将结果推送到计算堆栈上。 |
Div |
将两个值相除并将结果作为浮点(F 类型)或商(int32 类型)推送到计算堆栈上。 |
Mul |
将两个值相乘并将结果推送到计算堆栈上。 |
Rem |
将两个值相除并将余数推送到计算堆栈上。 |
Xor |
计算位于计算堆栈顶部的两个值的按位异或,并且将结果推送到计算堆栈上。 |
And |
计算两个值的按位"与"并将结果推送到计算堆栈上。 |
Or |
计算位于堆栈顶部的两个整数值的按位求补并将结果推送到计算堆栈上。 |
Not |
计算堆栈顶部整数值的按位求补并将结果作为相同的类型推送到计算堆栈上。 |
Dup |
复制计算堆栈上当前最顶端的值,然后将副本推送到计算堆栈上。 |
Neg |
对一个值执行求反并将结果推送到计算堆栈上。 |
Ret |
从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。 |
Jmp |
退出当前方法并跳至指定方法。 |
Newobj |
New Object创建一个值类型的新对象或新实例,并将对象引用推送到计算堆栈上。 |
Newarr |
New Array将对新的从零开始的一维数组(其元素属于特定类型)的对象引用推送到计算堆栈上。 |
Nop |
如果修补操作码,则填充空间。尽管可能消耗处理周期,但未执行任何有意义的操作。Debug下的 |
Pop |
移除当前位于计算堆栈顶部的值。 |
Initobj |
Init Object将位于指定地址的值类型的每个字段初始化为空引用或适当的基元类型的 0。 |
Isinst |
Is Instance测试对象引用是否为特定类的实例。 |
Sizeof |
将提供的值类型的大小(以字节为单位)推送到计算堆栈上。 |
Box |
将值类转换为对象引用。 |
Unbox |
将值类型的已装箱的表示形式转换为其未装箱的形式。 |
Castclass |
尝试将引用传递的对象转换为指定的类。 |
Switch |
实现跳转表。 |
Throw |
引发当前位于计算堆栈上的异常对象。 |
Call |
调用由传递的方法说明符指示的方法。 |
Calli |
通过调用约定描述的参数调用在计算堆栈上指示的方法(作为指向入口点的指针)。 |
Callvirt |
对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。 |
强调一下,有三种call,用的场景不太一样:
Call:常用于调用编译时就确定的方法,可以直接去元数据里找方法,如静态函数,实例方法,也可以call虚方法,不过只是call这个类型本身的虚方法,和实例的方法性质一样。另外,call不做null检测。
Calli: MSDN上讲是间接调用指针指向的函数,具体场景没见过,有知道的朋友望不吝赐教。
Callvirt: 可以调用实例方法和虚方法,调用虚方法时以多态方式调用,不能调用静态方法。Callvirt调用时会做null检测,如果实例是null,会抛出NullReferenceException,所以速度上比call慢点。
第二类:加载(ld)和存储(st)
我们知道,C#程序运行时会有线程栈把参数,局部变量放上来,另外还有个计算栈用来做函数里的计算。所以把值加载到计算栈上,算完后再把计算栈上的值存到线程栈上去,这类指令专门干这些活。
比方说 ldloc.0:
这个可以拆开来看,Ld打头可以理解为Load,也就是加载;loc可以理解为local variable,也就是局部变量,后面的 .0表示索引。连起来的意思就是把索引为0的局部变量加载到计算栈上。对应的 ldloc.1就是把索引为1的局部变量加载到计算栈上,以此类推。
知道了Ld的意思,下面这些指令 也就很容易理解了。
ldstr = load string,
ldnull = load null,
ldobj = load object,
ldfld = load field,
ldflda = load field address,
ldsfld = load static field,
ldsflda = load static field address,
ldelem = load element in array,
ldarg = load argument,
ldc 则表示加载数值,如ldc.i4.0,
关于后缀
.i[n]:[n]表示字节数,1个字节是8位,所以是8*n的int,比如i1, i2, i4, i8,i1就是int8(byte), i2是int16(short),i4是int32(int),i8是int64(long)。
相似的还有.u1 .u2 .u4 .u8 分别表示unsigned int8(byte), unsigned int16(short), unsigned int32(int), unsigned int64(long);
.R4,.R8 表示的是float和double。
.ovf (overflow)则表示会进行溢出检查,溢出时会抛出异常;
.un (unsigned)表示无符号数;
.ref (reference)表示引用;
.s (short)表示短格式,比如说正常的是用int32,加了.s的话就是用int8;
.[n] 比如 .1,.2 等,如果跟在i[n]后面则表示数值,其他都表示索引。如 ldc.i4.1就是加载数值1到计算栈上,再如ldarg.0就是加载第一个参数到计算栈上。
ldarg要特别注意一个问题:如果是实例方法的话ldarg.0加载的是本身,也就是this,ldarg.1加载的才是函数的第一个参数;如果是静态函数,ldarg.0就是第一个参数。
与ld对应的就是st,可以理解为store,意思是把值从计算栈上存到变量中去,ld相关的指令很多都有st对应的,比如stloc, starg, stelem等,就不多说了。
第三类:比较指令,比较大小或判断bool值
有一部分是比较之后跳转的,代码里的 if 就会产生这些指令,符合条件则跳转执行另一些代码:
以b开头:beq, bge, bgt, ble, blt, bne
先把b去掉看看:
eq: equivalent with, ==
ge: greater than or equivalent with , >=
gt: greater than , >
le: less than or equivalent with, <=
lt: less than, <
ne: not equivalent with, !=
这样是不是很好理解了,beq IL_0005就是计算栈上两个值相等的话就跳转到IL_0005, ble IL_0023是第一个值小于或等于第二个值就跳转到IL_0023。
以br(break)开头:br, brfalse, brtrue,
br是无条件跳转;
brfalse表示计算栈上的值为 false/null/0 时发生跳转;
brtrue表示计算栈上的值为 true/非空/非0 时发生跳转
还有一部分是c开头,算bool值的,和前面b开头的有点像:
ceq 比较两个值,相等则将 1 (true) 推到栈上,否则就把 0 (false)推到栈上
cgt 比较两个值,第一个大于第二个则将 1 (true) 推到栈上,否则就把 0 (false)推到栈上
clt 比较两个值,第一个小于第二个则将 1 (true) 推到栈上,否则就把 0 (false)推到栈上
以上就是三类常用的,把这些搞明白了,IL指令也就理解得七七八八了。就像看文章一样,认识大部分字后基本就不影响阅读了,不认识的猜下再查下,下次再看到也就认得了。
例子
下面看个例子,随手写段简单的代码,是否合乎逻辑暂不考虑,主要是看IL:
源代码:

- 1 using System;
- 2
- 3 namespace ILLearn
- 4 {
- 5 class Program
- 6 {
- 7 const int WEIGHT = 60;
- 8
- 9 static void Main(string[] args)
- 10 {
- 11 var height = 170;
- 12
- 13 People people = new Developer("brook");
- 14
- 15 var vocation = people.GetVocation();
- 16
- 17 var healthStatus = People.IsHealthyWeight(height, WEIGHT) ? "healthy" : "not healthy";
- 18
- 19 Console.WriteLine($"{vocation} is {healthStatus}");
- 20
- 21 Console.ReadLine();
- 22 }
- 23 }
- 24
- 25 abstract class People
- 26 {
- 27 public string Name { get; set; }
- 28
- 29 public abstract string GetVocation();
- 30
- 31 public static bool IsHealthyWeight(int height, int weight)
- 32 {
- 33 var healthyWeight = (height - 80) * 0.7;
- 34 return weight <= healthyWeight * 1.1 && weight >= healthyWeight * 0.9; //标准体重是 (身高-80) * 0.7,区间在10%内都是正常范围
- 35 }
- 36 }
- 37
- 38 class Developer : People
- 39 {
- 40 public Developer(string name)
- 41 {
- 42 Name = name;
- 43 }
- 44
- 45 public override string GetVocation()
- 46 {
- 47 return "Developer";
- 48 }
- 49 }
- 50 }

在命令行里输入:csc /debug- /optimize+ /out:program.exe Program.cs
打开IL查看工具:C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools\ildasm.exe,不同版本可能目录不太一样。打开刚编译的program.exe文件,如下:
双击节点就可以查看IL,如:
Developer的构造函数:

- 1 .method public hidebysig specialname rtspecialname
- 2 instance void .ctor(string name) cil managed
- 3 {
- 4 // 代码大小 14 (0xe)
- 5 .maxstack 8
- 6 IL_0000: ldarg.0 //加载第1个参数,因为是实例,而实例的第1个参数始终是this
- 7 IL_0001: call instance void ILLearn.People::.ctor() //调用基类People的构造函数,而People也会调用Object的构造函数
- 8 IL_0006: ldarg.0 //加载this
- 9 IL_0007: ldarg.1 //加载第二个参数也就是name
- 10 IL_0008: call instance void ILLearn.People::set_Name(string) //调用this的 set_Name, set_Name这个函数是编译时为属性生成的
- 11 IL_000d: ret //return
- 12 } // end of method Developer::.ctor

Developer的GetVocation:

- 1 .method public hidebysig virtual instance string //虚函数
- 2 GetVocation() cil managed
- 3 {
- 4 // 代码大小 6 (0x6)
- 5 .maxstack 8 //最大计算栈,默认是8
- 6 IL_0000: ldstr "Developer" //加载string "Developer"
- 7 IL_0005: ret //return
- 8 } // end of method Developer::GetVocation

People的IsHealthyWeight:

- 1 .method public hidebysig static bool IsHealthyWeight(int32 height, //静态函数
- 2 int32 weight) cil managed
- 3 {
- 4 // 代码大小 52 (0x34)
- 5 .maxstack 3 //最大计算栈大小
- 6 .locals init ([0] float64 healthyWeight) //局部变量
- 7 IL_0000: ldarg.0 //加载第1个参数,因为是静态函数,所以第1个参数就是height
- 8 IL_0001: ldc.i4.s 80 //ldc 加载数值, 加载80
- 9 IL_0003: sub //做减法,也就是 height-80,把结果放到计算栈上,前面两个已经移除了
- 10 IL_0004: conv.r8 //转换成double,因为下面计算用到了double,所以要先转换
- 11 IL_0005: ldc.r8 0.69999999999999996 //加载double数值 0.7, 为什么是0.69999999999999996呢, 二进制存不了0.7,只能找个最相近的数
- 12 IL_000e: mul //计算栈上的两个相乘,也就是(height - 80) * 0.7
- 13 IL_000f: stloc.0 //存到索引为0的局部变量(healthyWeight)
- 14 IL_0010: ldarg.1 //加载第1个参数 weight
- 15 IL_0011: conv.r8 //转换成double
- 16 IL_0012: ldloc.0 //加载索引为0的局部变量(healthyWeight)
- 17 IL_0013: ldc.r8 1.1000000000000001 //加载double数值 1.1, 看IL_0010到IL_0013,加载了3次,这个函数最多也是加载3次,所以maxstack为3
- 18 IL_001c: mul //计算栈上的两个相乘,也就是 healthyWeight * 1.1, 这时计算栈上还有两个,第一个是weight,第二个就是这个计算结果
- 19 IL_001d: bgt.un.s IL_0032 //比较这两个值,第一个大于第二个就跳转到 IL_0032,因为第一个大于第二个表示第一个条件weight <= healthyWeight * 1.1就是false,也操作符是&&,后面没必要再算,直接return 0
- 20 IL_001f: ldarg.1 //加载第1个参数 weight
- 21 IL_0020: conv.r8 //转换成double
- 22 IL_0021: ldloc.0 //加载索引为0的局部变量(healthyWeight)
- 23 IL_0022: ldc.r8 0.90000000000000002 //加载double数值 0.9
- 24 IL_002b: mul //计算栈上的两个相乘,也就是 healthyWeight * 0.9, 这时计算栈上还有两个,第一个是weight,第二个就是这个计算结果
- 25 IL_002c: clt.un //比较大小,第一个小于第二个则把1放上去,否则放0上去
- 26 IL_002e: ldc.i4.0 //加载数值0
- 27 IL_002f: ceq //比较大小,相等则把1放上去,否则放0上去
- 28 IL_0031: ret //return 栈顶的数,为什么没用blt.un.s,因为IL_0033返回的是false
- 29 IL_0032: ldc.i4.0 //加载数值0
- 30 IL_0033: ret //return 栈顶的数
- 31 } // end of method People::IsHealthyWeight

主函数Main:

- 1 .method private hidebysig static void Main(string[] args) cil managed
- 2 {
- 3 .entrypoint //这是入口
- 4 // 代码大小 67 (0x43)
- 5 .maxstack 3 //大小为3的计算栈
- 6 .locals init (string V_0,
- 7 string V_1) //两个string类型的局部变量,本来还有个people的局部变量,被release方式优化掉了,因为只是调用了people的GetVocation,后面没用,所以可以不存
- 8 IL_0000: ldc.i4 0xaa //加载int型170
- 9 IL_0005: ldstr "brook" //加载string "brook"
- 10 IL_000a: newobj instance void ILLearn.Developer::.ctor(string) //new一个Developer并把栈上的brook给构造函数
- 11 IL_000f: callvirt instance string ILLearn.People::GetVocation() //调用GetVocation
- 12 IL_0014: stloc.0 //把上面计算的结果存到第1个局部变量中,也就是V_0
- 13 IL_0015: ldc.i4.s 60 //加载int型60
- 14 IL_0017: call bool ILLearn.People::IsHealthyWeight(int32, //调用IsHealthyWeight,因为是静态函数,所以用call
- 15 int32)
- 16 IL_001c: brtrue.s IL_0025 //如果上面返回true的话就跳转到IL_0025
- 17 IL_001e: ldstr "not healthy" //加载string "not healthy"
- 18 IL_0023: br.s IL_002a //跳转到IL_002a
- 19 IL_0025: ldstr "healthy" //加载string "healthy"
- 20 IL_002a: stloc.1 //把结果存到第2个局部变量中,也就是V_1, IL_0017到IL_002a这几个指令加在一起用来计算三元表达式
- 21 IL_002b: ldstr "{0} is {1}" //加载string "{0} is {1}"
- 22 IL_0030: ldloc.0 //加载第1个局部变量
- 23 IL_0031: ldloc.1 //加载第2个局部变量
- 24 IL_0032: call string [mscorlib]System.String::Format(string, //调用string.Format,这里也可以看到C# 6.0的语法糖 $"{vocation} is {healthStatus}",编译后的结果和以前的用法一样
- 25 object,
- 26 object)
- 27 IL_0037: call void [mscorlib]System.Console::WriteLine(string) //调用WriteLine
- 28 IL_003c: call string [mscorlib]System.Console::ReadLine() //调用ReadLine
- 29 IL_0041: pop
- 30 IL_0042: ret
- 31 } // end of method Program::Main

很简单吧,当然,这个例子也很简单,没有事件,没有委托,也没有async/await之类,这些有兴趣的可以写代码跟一下,这几种都会在编译时插入也许你不知道的代码。
就这么简单学一下,应该差不多有底气和面试官吹吹牛逼了。
结束
IL其实不难,有没有用则仁者见仁,智者见智,有兴趣就学一下,也花不了多少时间,确实也没必要学多深,是吧。
当然,也是要有耐心的,复杂的IL看起来还真是挺头痛。好在有工具ILSpy,可以在option里选择部分不反编译来看会比较简单些。
(转) 读懂IL的更多相关文章
- 读懂IL代码就这么简单(三)完结篇
一 前言 写了两篇关于IL指令相关的文章,分别把值类型与引用类型在 堆与栈上的操作区别详细的写了一遍 这第三篇也是最后一篇,之所以到第三篇就结束了,是因为以我现在的层次,能理解到的都写完了,而且个人认 ...
- 读懂IL代码就这么简单(二)
一 前言 IL系列 第一篇写完后 得到高人指点,及时更正了文章中的错误,也使得我写这篇文章时更加谨慎,自己在了解相关知识点时,也更为细致.个人觉得既然做为文章写出来,就一定要保证比较高的质量,和正确率 ...
- 读懂IL代码就这么简单 (一)
一前言 感谢 @冰麟轻武 指出文章的错误之处,现已更正 对于IL代码没了解之前总感觉很神奇,初一看完全不知所云,只听高手们说,了解IL代码你能更加清楚的知道你的代码是如何运行相互调用的,此言一出不明觉 ...
- 读懂IL代码就这么简单
原文地址:http://www.cnblogs.com/zery/p/3366175.html 一前言 感谢 @冰麟轻武 指出文章的错误之处,现已更正 对于IL代码没了解之前总感觉很神奇,初一看完全不 ...
- 读懂IL
读懂IL 先说说学IL有什么用,有人可能觉得这玩意平常写代码又用不上,学了有个卵用.到底有没有卵用呢,暂且也不说什么学了可以看看一些语法糖的实现,或对.net理解更深一点这些虚头巴脑的东西.最重要的理 ...
- 轻松读懂IL
轻松读懂IL先说说学IL有什么用,有人可能觉得这玩意平常写代码又用不上,学了有个卵用.到底有没有卵用呢,暂且也不说什么学了可以看看一些语法糖的实现,或对.net理解更深一点这些虚头巴脑的东西.最重要的 ...
- 读懂IL代码就这么简单 ---- IL系列文章
读懂IL代码就这么简单 (一) 读懂IL代码就这么简单(二) 读懂IL代码就这么简单(三)完结篇 出处:http://www.cnblogs.com/zery/tag/IL%20%E7%B3%BB%E ...
- 【转载】读懂IL代码就这么简单(三)完结篇
一 前言 写了两篇关于IL指令相关的文章,分别把值类型与引用类型在 堆与栈上的操作区别详细的写了一遍这第三篇也是最后一篇,之所以到第三篇就结束了,是因为以我现在的层次,能理解到的都写完了,而且个人认为 ...
- 【转载】读懂IL代码就这么简单(二)
一 前言 IL系列 第一篇写完后 得到高人指点,及时更正了文章中的错误,也使得我写这篇文章时更加谨慎,自己在了解相关知识点时,也更为细致.个人觉得既然做为文章写出来,就一定要保证比较高的质量,和正确率 ...
- 【转载】读懂IL代码就这么简单 (一)
一前言 感谢 @冰麟轻武 指出文章的错误之处,现已更正 对于IL代码没了解之前总感觉很神奇,初一看完全不知所云,只听高手们说,了解IL代码你能更加清楚的知道你的代码是如何运行相互调用的,此言一出不明觉 ...
随机推荐
- 远程调用之RMI、Hessian、Burlap、Httpinvoker、WebService的比较
一.综述 本文比较了RMI.Hessian.Burlap.Httpinvoker.WebService5这种通讯协议的在不同的数据结构和不同数据量时的传输性能. RMI是java语言本身提供的远程通讯 ...
- Spring3: 在Bean定义中使用EL-表达式语言
5.4.1 xml风格的配置 SpEL支持在Bean定义时注入,默认使用“#{SpEL表达式}”表示,其中“#root”根对象默认可以认为是ApplicationContext,只有Applicat ...
- ffmpeg的centos、msys2、msvc编译
msys2 和 centos https://ffmpeg.org/download.html https://ffmpeg.zeranoe.com/builds/ Windows MSYS2准备 1 ...
- 转:A/B测试:实现方法
概念:http://www.aliued.cn/2010/09/13/ab-testing-basic-concept.html 我们先来看一个图: (注:感谢Algo提供本图.) 上图展示了 A/B ...
- Nginx 常用配置模板
user root root; worker_processes auto; worker_rlimit_nofile 51200; events { use epoll; worker_connec ...
- docker 私有registry 配置
备注:此处使用linux镜像配置仓库 registry 启动步骤 https://hub.docker.com/_/registry/ docker login docker pull registr ...
- LeetCode OJ:Valid Parentheses(有效括号)
Given a string containing just the characters '(', ')', '{', '}', '[' and ']', determine if the inpu ...
- LeetCode OJ:Add Binary(二进制相加)
Given two binary strings, return their sum (also a binary string). For example,a = "11"b = ...
- PostgreSQL full_page_write记录
PostgreSQL 在 checkpoint 之后在对数据页面的第一次写的时候会将整个数据页面写到 xlog 里面. 当出现主机断电或者OS崩溃时,redo操作时通过checksum发现“部分写”的 ...
- debounce与throttle区别
在2011年,Twitter网站曾爆出一个问题:在主页往下滚动时,页面会变得缓慢以致没有响应.John Resig发表了一篇文章< a blog post about the problem&g ...