IL快速入门教程

前言

先说说学IL代码有什么用

很多人觉得这玩意平常写代码也用不上,学了也没什么卵用。

这里暂且不说什么学了会对Net理解更深一些什么乱七八糟的东西

最重要的一点就是当你的面试官问你

懂不懂IL?

这个时候怎么回答?

1、我觉得那东西没什么卵用,所以没学

2、略懂,可以探讨一下

聪明如你肯定知道哪个回答更靠谱点

事实上学会IL并不会花费我们特别多的时间

而这点东西却可以跟面试官吹上半天,是不是很实用、、、

小小的对比

同样是生涩的代码

正则表达式,初看时会觉得很难

然后学习后发现只需要记住一些正则表达式符号就基本可以处理一般的问题了

同样的IL代码相比较于正则表达式还要简单一些

大多数人开始看到IL代码的指令时会觉得它跟汇编似得

光是看一眼就头晕目眩,不想学了

而实际上,只需要记住几个规律就可以掌握他们

偷偷告诉你

我们在对net下的软件进行反编译的时候经常会见到IL代码

把它们掌握了,去改一些东西也会更加得心用手

PS:我就经常这么干

正文

这里我们把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跟Callvirt的区别

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:

源代码:

IL代码

Developer的构造函数:

Developer的GetVocation:

People的IsHealthyWeight:

主函数Main:

小结

IL其实不难,有没有用则仁者见仁,智者见智

有兴趣就学一下,也花不了多少时间,确实也没必要学多深,是吧。

当然,也是要有耐心的,复杂的IL看起来还真是挺头痛

参与评论

游客评论不支持回复他人评论内容,如需回复他人评论内容请