正则表达式:
第一类 :直观型
这一类的特点是一看名字就知道是干嘛的,不需要多讲,如下:
说明 |
|
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 |
将提供的值类型的大小(以字节为单位)推送到计算堆栈上。 |
将值类转换为对象引用。 |
|
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:equivalentwith,==
ge:greaterthanorequivalentwith,>=
gt:greaterthan,>
le:lessthanorequivalentwith,<=
lt:lessthan,<
ne: notequivalentwith,!=
这样是不是很好理解了,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 string GetVocation(); 30 31 bool IsHealthyWeight(int height,int weight) 32 { 33 var healthyWeight = (height - 80) * 0.7; 34 return weight <= healthyWeight * 1.1 && weight >= healthyWeight * 0.9; @H_301_872@//@H_301_872@标准体重是 (身高-80) * 0.7,区间在10%内都是正常范围 35 } 36 } 37 38 class Developer : People 39 { 40 public Developer(string name) 41 { 42 Name = name; 43 } 44 45 override string GetVocation() 46 { 47 return Developer48 } 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的构造函数:
Developer的GetVocation:
People的IsHealthyWeight:
主函数Main:
很简单吧,当然,这个例子也很简单,没有事件,没有委托,也没有async/await之类,这些有兴趣的可以写代码跟一下,这几种都会在编译时插入也许你不知道的代码。
就这么简单学一下,应该差不多有底气和面试官吹吹牛逼了。
结束
IL其实不难,有没有用则仁者见仁,智者见智,有兴趣就学一下,也花不了多少时间,确实也没必要学多深,是吧。
当然,也是要有耐心的,复杂的IL看起来还真是挺头痛。好在有工具ILSpy,可以在option里选择部分不反编译来看会比较简单些。
参考: