30分钟?不需要,轻松读懂IL

先说说学IL有什么用,有人可能觉得这玩意平常写代码又用不上,学了有个卵用。到底有没有卵用呢,暂且也不说什么学了可以看看一些语法糖的实现,或对.net理解更深一点这些虚头巴脑的东西。最重要的理由就是一个:当面试官看你简历上写着精通C#时,问你一句:

"懂不懂IL?"

怎么回答?

"不好意思,那东西没什么卵用,所以我没学。"

还是

"还行,可以探讨一下。"

你觉得哪个回答好呢,答得好才更有底气要到更多的薪资,多个几千块也说不定,而这只不过花上不到半小时学习就可以跟面试官吹上一阵了,很实用,有没有。

为什么取这个标题呢,记得很久之前看过一篇文章,叫"正则表达式30分钟入门教程",学正则最重要的就是记住各个符号的含义。个人觉得相比难以直接看出实际意义的正则符号如"\w","\d","*","?","{}[]"等,IL的指令要容易得多。很多人见到IL一大堆的指令,和汇编一样,就感觉头大不想学了。其实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指令表: http://www.cnblogs.com/zery/p/3368460.html

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏python3

python 递归与高阶函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。

874
来自专栏JMCui

MongoDB系列六(聚合).

 一、概念     使用聚合框架可以对集合中的文档进行变换和组合。基本上,可以用多个构件创建一个管道(pipeline),用于对一连串的文档进行处理。这些构件包...

4235
来自专栏向治洪

Swift 4.0 新特性

WWDC 2017 带来了很多惊喜,在这次大会上,Swift 4 也伴随着 Xcode 9 测试版来到了我们的面前,虽然正式版要8月底9月初才会公布,但很多强大...

2009
来自专栏Golang语言社区

GO语言标准库概览

在Go语言五周系列教程的最后一部分中,我们将带领大家一起来浏览一下Go语言丰富的标准库。 Go标准库包含了大量包,提供了丰富广泛的功能特性。这里提供了概览仅仅是...

2494
来自专栏HTML5学堂

操作符与数据类型转换

上一期堡堡给大家讲解了关于JS的基础语法,虽然是一些非常基础的知识,但是它对大家的后期学习奠定了一定的基础。知识像一张网,基础越扎实,网住的鱼就越多,要告诉大家...

2788
来自专栏大内老A

从数据到代码——通过代码生成机制实现强类型编程[上篇]

我不知道大家对CodeDOM的代码生成机制是否熟悉,但是有一点可以确定:如果你使用过Visual Studio,你就应该体验过它带给我们在编程上的便利。随便列举...

1559
来自专栏Python

python2/3中 将base64数据写成图片,并将图片数据转为16进制数据的方法、bytes/string的区别

python 3中最重要的新特性可能就是将文本(text)和二进制数据做了更清晰的区分。文本总是用unicode进行编码,以str类型表示;而二进制数据以byt...

982
来自专栏每日一篇技术文章

AudioToolbox_如何录制PCM格式的数据

先来认识一下头文件 AudioConverter.h: 音频转换接口。定义用于创建和使用音频转换器的接口 AudioFile.h: 定义一个用于读取和写入...

651
来自专栏柠檬先生

你不知道的javaScript笔记(6)

语法   语句表达式       句子是完整表达某个意思的一组词,由一个或多个短语组成,他们之间由标点符号或者连接词连接起来。       语句相当于句子,表达...

1867
来自专栏用户画像

RAM刷新周期问题

RAM刷新有三种: 1、集中式刷新在一个刷新周期内(2ms),先让存储器读写,然后集中刷新,这样就存在死区问题,如果是存取周期为2us的话,这样对于64*64...

732

扫码关注云+社区