我们用C#写的源码最终是要转换成IL语言的,(注:C#源码有C#编译器生成元数据和IL,程序真正运行时,按需要由JIT将IL及时编译成机器码),理解NET技术做好的方式就是理解IL。
一旦你掌握了IL,理解NET技术就不成问题了,因为所有针对NET平台的语言最终都要编译成IL,找了张图:
下面通过一个例子来展开IL的学习,打开记事本,输入:
.method void vijay()
{
}
这样就写了一个IL程序(但是不能运行),我们将它命名为a.il,用IL编译器试着编译一下,找到VS自带的命令行窗口(就不用设置环境变量了)
定位到a.il所在的文件夹,输入编译命令ilsam a.il
E:"il>ilasm a.il
Error: No entry point declared for executable
Could not create output file, error code=0x80004005
提示错误:可执行文件没有入口点(记得Main方法么,一样的道理)
IL中,一行代码的开始要么有个点 例如:.method void a()。任何以点开头的语句称为'指令directive'(针对编译器而言),效果是创建一个方法或者类(例如可以这样写: .class 意思是我要定义一个类,以后看多了就明白了)。不用点开头的语句称为'instruction',自己理解这两个文字的区别吧(其实点开头语句是编译器用来生成元数据的,不是点开头的语句是用来生成IL代码的,以后会具体说下,很好理解)。
语句:.method void a() 意思是定义一个叫a(名字随便起)的方法,没有参数,返回值为空。
为啥会报错,就像你得为C#程序指定一个入口函数Main一样,在IL中 你也得指定一个入口函数,告诉程序从哪里开始执行,
那就指定一个入口点,如下:
.method void a()
{
.entrypoint
}
用同样的命令再编,这样不报错了,而且生成了一个叫a.exe的文件(同一文件夹下)。 .entrypoint这句告诉编译器a是程序的入口函数。
运行一下程序,输入:a 报错:未处理的异常: System.BadImageFormatException: 未能加载文件或程序集“a.exe”或它的某一个依赖项。该模块应包含一个程序集清
单。
文件名:“a.exe”。
意思是a.exe不是一个有效的'程序集','程序集'是NET中最小的可发布,版本控制的单元(NET框架设计书讲的很明白),将程序改成:
.assembly ak{}
.method void a()
{
.entrypoint
}
编译通过,运行通过,虽然啥也没输出。(这里说下,上面的代码应该报错的,因为没有给a方法指定返回指令,但它不报错,我也没辙),完整的程序应该是:
.assembly ak{}
.method void a()
{
.entrypoint
ret
}
.assembly ak{}
定义一个程序集,这样才能被CLR执行
.method void a()
定义了入口方法,程序将从这里开始执行
急着想看输出么,来个经典的HELLO,WORLD吧,如下:
.assembly extern mscorlib{}
.assembly ak{}
.method static void a()
{
.entrypoint
ldstr "HELLO WORLD"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
编译运行,哈罗我的就出来了。
稍微解释一下这段程序:
.assembly extern mscorlib{}
表示:引用外部的程序集mscorlib(跟using System;完全不是一回事啊,以后说),为啥要引用外部程序集,很明显,你的程序里并没有定义叫WriteLine方法,为了使用这个方法,只有引用微软给你提供的程序集mscorlib。
[mscorlib]System.Console::WriteLine(string)
这个[mscorlib]表示你调用的方法在哪个程序集里,System.Console(这个System是using System;产生的)表示在mscorlib程序集里的一个类,WriteLine表示类中的方法,这句话的意思是:去一个叫mscorlib的程序集里找一个叫System.Console的类中的一个叫Console的方法,方法接受一个string类型参数,不返回值。(至于是怎么找到mscorlib的,还有一大段话要说,先不说),跑题了,越撤越多,继续。
在上面的例子中没看到Main的影子吧,Main是C#的词,对IL不好使,IL只认.entrypoint
,意思是入口函数的名字可以随便起,只要用.entrypoint指明,当然只能由一个这样的方法。
ldstr "HELLO WORLD"
IL是基于栈操作的,意味着:你得把要操作的对象放在栈上,相应操作从栈上取走它需要的操作数,并把操作的结果放在栈上。例如1 + 2,写成IL是:
ldc.i4.1 把1放在栈上
ldc.i4.2 把2放在栈上
Add 把 1 2 从栈上取走,相加,把结果3放在栈上
因此:ldstr "HELLO WORLD"是把字符串HELLO WORLD放在栈上(实际上是放的字符串的引用,以后说)
call void [mscorlib]System.Console::WriteLine(string)
在IL中,方法调用时不仅要指明返回类型,方法的参数类型也必须指定,一句话:调用方法时,要指明:方法返回类型、方法所在的程序集、方法所在类型的全名、方法的名字、方法所有的参数类型,这样写确实比较繁琐,但对编译器和运行时的代码验证提供了很大的方便。
下面为方法a加一些属性:
.assembly extern mscorlib{}
.assembly ak{}
.method public hidebysig static void a() il managed
{
.entrypoint
ldstr "HELLO WORLD"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
注意:目前为止还没出现'类'的概念。
Public:标识a方法的可见性,public表示改程序的任何代码都是访问到;
Hidebysig:将父类中的同名同签名的方法隐藏,如果有的话
Static:静态方法,不用多解释吧,入口方法必须为静态的
Il managed:方法为托管的 以后详细说
将上面的程序编译运行,结果还是一样。
类:
C#中如此定义类:class a{}
下面用IL定义个类,C#版HELLO WORLD:
.assembly extern mscorlib{}
.assembly ak{}
.class Hello{
.method public hidebysig static void Main() il managed
{
.entrypoint
ldstr "HELLO WORLD"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
跟以前的区别就是.class Hello 表示要定义一个类,挺简单吧 呵呵
再给类加些属性.class private auto ansi Hello
Private:该类为本程序集可见,public:外部程序集可访问
Auto:类型实例在内存中的布局方式由CLR决定
ansi:指定了和非托管代码交互时的字符串转换规则,托管代码中,一个字符占2个字节,称为:Unicode characters,而在非托管代码中,一个字符占1个字节(C语言中),称:ANSI characters。
再改代码:
.assembly extern mscorlib{}
.assembly ak{}
.class private auto ansi Hello extends System.Object{
.method public hidebysig static void Main() il managed
{
.entrypoint
ldstr "HELLO WORLD"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
这不用解释吧,所有类型都集成至Object类,以前不写为啥不报错,因为是默认的。
记得这句话不:如果C#一个类没有定义构造函数,编译器会自动生成一个默认构造函数,上面代码没看到啊,因为你没写,IL中是要自力更生的,添上:
.assembly extern mscorlib{}
.assembly ak{}
.class private auto ansi Hello extends System.Object{
.method public hidebysig static void Main() il managed
{
.entrypoint
ldstr "HELLO WORLD"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor() il managed{
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
}
哇,好长,不过它就是一个方法,只不过待遇和别的方法不一样(体现在specialname rtspecialname ),构造函数的名字必须为.ctor() ,instance表示这是一个实例方法
看下方法都做了些什么:
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
把第一个参数放栈上(ldarg.0 参数从0开始计算,取第二个参数就是ldarg.1),然后调用父类的构造函数call instance void [mscorlib]System.Object::.ctor()
上一帖说到:默认构造函数都要调用其父类的构造函数 一直到Object,构造函数的作用就是初始化实例的初始状态,子类会继承父类的实例字段,所以要调用其父类的构造函数让父类完成初始化操作:
说下:实例方法调用时,默认的传递一个this参数,this指向你要操作的实例,所以ldarg.0就是把this放栈上了,然后调用示例方法call instance void [mscorlib]System.Object::.ctor()
学习IL最好的方式是看编译器生成的现成IL,如下:
using System;
class tes{
static void Main(){
Console.WriteLine("HEllo,World");
}
} C#版的
将它编译:csc a.cs 便生成一个可执行文件a.exe,查看编译器生成的IL(所有NET语言源码都要被相应的编译器编译成IL)输入:ildasm a.exe 打开了窗口:
点击tes打开,看到两个方法:Main方法和编译器默认生成的构造函数.ctor
构造函数的IL:
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// 代码大小 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method tes::.ctor
哇,和咱们写的一样吧,它就多个 .maxstack 8,啥意思呢,表示:方法执行期间的最大栈帧数,这个一般是编译器按需生成的,你也可以自己指定,如果你指定3,那么栈上同时不能存在3个以上的操作数。
看下Main:
.method private hidebysig static void Main() cil managed
{
.entrypoint
// 代码大小 13 (0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "HEllo,World"
IL_0006: call void [mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ret
} // end of method tes::Main
哇,也几乎一模一样,就少了个nop,这个指令是用来将栈顶操作数弹出的,上面中的两个nop其实没啥用,因为nop时栈上没东西,还有就是一些: IL_000c:每个IL语句前面都有,干嘛的呢,用来标识IL语句在代码流中的位置,记得C#中的goto语句么,就是标识了类似goto语句的目的地。写个例子:
.assembly extern mscorlib{}
.assembly ak{}
.class private auto ansi Hello extends System.Object{
.method public hidebysig static void Main() il managed
{
.entrypoint
ldc.i4.1 1上栈
ldc.i4.2 2上栈
bgt dayu 如果1>2 就跳到一个叫dayu的地方 否则 继续执行
ldstr "1 < 2"
call void [mscorlib]System.Console::Write(string) 打印
br exit 无条件跳到一个叫 exit的地方
dayu:ldstr "1 > 2" 这个叫dayu的地方
call void [mscorlib]System.Console::Write(string) 打印
br exit 无条件跳到一个叫 exit的地方
exit: ret 这是个叫exit的地方
}}
一开始不懂得先记者,慢慢的以前不太明白的就理解了。
下一贴:IL基础
另附IL指令集:
Instruction | Description | Stack Transition | |
1 | add | add two values, returning a new value | …, value1, value2à…, result |
2 | add.ovf.<signed> | add integer value with overflow check | …, value1, value2à…, result |
3 | and | bitwise AND | …, value1, value2 à…, result |
4 | arglist | get argument list | … à …, argListHandle |
5 | beq.<length> | branch on equal | …, value1, value2 à … |
6 | bge.<length> | branch on greater than or equal to | …, value1, value2 à … |
7 | bge.un.<length> | branch on greater/equal, unsigned or unordered | …, value1, value2 à … |
8 | bgt.<length> | branch on greater than | …, value1, value2 à … |
9 | bgt.un<length> | branch on greater than, unsigned or unordered | …, value1, value2 à … |
10 | ble.<length> | branch on less than or equal to | …, value1, value2 à … |
11 | ble..un<length> | branch on less/equal, unsigned or unordered | …, value1, value2 à … |
12 | blt.<length> | branch on less than | …, value1, value2 à … |
13 | blt.un.<length> | branch on less than, unsigned or unordered | …, value1, value2 à … |
14 | bne.un<length> | branch on not equal or unorded | …, value1, value2 à … |
15 | br.<length> | unconditional branch | …, à … |
16 | break | breakpoint instruction | …, à … |
17 | brfalse.<length> | branch on false, null, or zero | …, value à … |
18 | brtrue.<length> | branch on non-false or non-null | …, value à … |
19 | call | call a method | …, arg1, arg2 … argn à …, retVal (not always returned) |
20 | calli | indirect method call | …, arg1, arg2 … argn, ftn à …, retVal (not always returned) |
21 | ceq | compare equal | …, value1, value2à…, result |
22 | cgt | compare greater than | …, value1, value2à…, result |
23 | cgt.un | compare greater than, unsigned or unordered | …, value1, value2à…, result |
24 | ckfinite | check for a finite real number | …, value à …, value |
25 | clt | compare less than | …, value1, value2à…, result |
26 | clt.un | compare less than, unsigned or unordered | …, value1, value2à…, result |
27 | conv.<to type> | data conversion | …, value à …, result |
28 | conv.ovf<to type> | data conversion with overflow detection | …, value à …, result |
29 | conv.ovf.<to type>.un | unsigned data conversion with overflow detection | …, value à …, result |
30 | cpblk | copy data from memory to memory | …, destaddr, srcaddr, size à … |
31 | div | divide values | …, value1, value2à…, result |
32 | div.un | divide integer values, unsigned | …, value1, value2à…, result |
33 | dup | duplicate the top value of the stack | …, value à …, value, value |
34 | endfilter | end filter clause of SEH | …, value à … |
35 | endfinally | end the finally or fault clause of exception block | … à … |
36 | initblk | initialize a block of memory to a value | …, addr, value, size à … |
37 | jmp | jump to method | … à … |
38 | ldarg.<length> | load argument onto the stack | … à …, value |
39 | ldarga.<length> | load an argument address | …, à …, address of argument number argNum |
40 | ldc.<type> | load numeric constant | … à …, num |
41 | ldftn | load method pointer | … à …, ftn |
42 | ldind.<type> | load value indirect onto the stack | …, addr à …, value |
43 | ldloc | load local variable onto the stack | … à …, value |
44 | ldloca.<length> | load local variable address | … à …, address |
45 | ldnull | load a null pointer | … à …, null value |
46 | leave.<length> | exit a protected region of code | …, à |
47 | localloc | allocate space in the local dynamic memory pool | size à address |
48 | mul | multiply values | …, value1, value2 à …, result |
49 | mul.ovf<type> | multiply integer values with overflow check | …, value1, value2 à …, result |
50 | neg | negate | …, value à …, result |
51 | nop | no operation | …, à …, |
52 | not | bitwise complement | …, value à …, result |
53 | or | bitwise OR | …, value1, value2 à …, result |
54 | pop | remove the top element of the stack | …, value à … |
55 | rem | compute the remainder | …, value1, value2 à …, result |
56 | rem.un | compute integer remainder, unsigned | …, value1, value2 à …, result |
57 | ret | return from method | retVal on callee evaluation stack (not always present) à …, retVal on caller evaluation stack (not always present) |
58 | shl | shift integer left | …, value, shiftAmount à …, result |
59 | shr | shift integer right | …, value, shiftAmount à …, result |
60 | shr.un | shift integer right, unsigned | …, value, shiftAmount à …, result |
61 | starg.<length> | store a value in an argument slot | …, value à …, |
62 | stind.<type> | store value indirect from stack | …, addr, val à … |
63 | stloc | pop value from stack to local variable | …, value à … |
64 | sub | substract numeric values | …, value1, value2 à …, result |
65 | sub.ovf.<type> | substract integer values, checking for overflow | …, value1, value2 à …, result |
66 | switch | table switch on value | …, value à …, |
67 | xor | bitwise XOR | ..., value1, value2 à ..., result |