(翻译)Introduction to Microsoft's IL
我们用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 |
转载于:https://www.cnblogs.com/25233/archive/2008/09/18/1293308.html