这一章弄几个小例子,给个IL程序的大概影响,学习IL最好的方式就是先熟悉下IL中的指令,再看C#编译器给你产生的IL代码,记得咋看不:用C#代码,CSC编译,再用ildasm打开EXE文件,就看到了给你生成好的IL代码。
下面给几个例子:
class zzz
{
public static void Main()
{
System.Console.WriteLine("hi");
zzz.abc();
}
public static void abc()
{
System.Console.WriteLine("bye");
}
}
你可以把上面程序编译,在用ildasm看生成的IL,也可以自己动手写:
IL代码都是俺动手敲的,虽然和编译器生成的不一样,比如类名,方法名,啥的。。不过不影响学习IL:
.assembly extern mscorlib{}
.assembly ak{}
.class private auto ansi Hello extends System.Object{
.method public hidebysig static void Main() il managed
{
.entrypoint
ldstr "hi"
call void [mscorlib]System.Console::WriteLine(string)
call void Hello::Say()
ret
}
.method public hidebysig static void Say() il managed{
ldstr "bye"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
说下IL中的方法调用:call void Hello::Say()
调用方法时:一次提供如下信息:
1、返回类型
2、类名
3、调用方法名
4、参数类型
上述规则同样适用于调用基类的构造函数。
IL中,方法名前面的类名是强制性的,也就是说不会对调用方法所在的类做任何的假设(比如说在C#中可以直接写Say()就行,因为假设Say()在本类中,IL中不做假设)因此类名必须写上。
C#:
class zzz
{
public static void Main()
{
System.Console.WriteLine("hi");
}
static zzz()
{
System.Console.WriteLine("bye");
}
}
提供了个静态构造函数,静态构造函数是给CLR调用的,CLR保证静态构造函数在访问该类的任何代码以前调用,而且只会调用一次。因此上面代码构造函数先执行,Main再执行
写成IL:
.assembly extern mscorlib{}
.assembly ak{}
.class private auto ansi Hello extends System.Object{
.method public hidebysig static void Main() il managed
{
.entrypoint
ldstr "hi"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
.method private hidebysig specialname rtspecialname static void .cctor() il managed{
ldstr "bye"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
可以看出:C#中,静态构造函数是和类名相同的,IL中静态构造函数有个特殊的名字.cctor
C#:
class zzz
{
public static void Main()
{
System.Console.WriteLine("hi");
new zzz();
}
zzz()
{
System.Console.WriteLine("bye");
}
}
私有构造函数(没写修饰符(public private),生成IL的时候就悄悄给你加上privatescope了)。这个和C#中不一样,C#中默认是private。IL:
.assembly extern mscorlib{}
.assembly ak{}
.class private auto ansi Hello extends System.Object{
.method public hidebysig static void Main() il managed
{
.entrypoint
ldstr "hi"
call void [mscorlib]System.Console::WriteLine(string)
newobj instance void Hello::.ctor()
pop
ret
}
.method private hidebysig specialname rtspecialname instance void .ctor() il managed{
ldstr "bye"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
C#中的new关键字对应IL中的newobj指令,虽然IL是基于栈的语言,但也有高级的一面,比如newobj指令,但这些‘高级’指令的提供是为了对应高级语言(C#)中的相应命令(new----newobj)。
使用newobj跟调用其他方法的规则一样,方法的所有标识必须全部指定:instance void Hello::.ctor()。构造函数也是方法嘛。只是名字特殊了点。
说下newobj instance void Hello::.ctor()的效果:在堆中指定一定空间存储对象的实例,并将对象实例的地址放栈上,所以这条指令执行完,栈顶上会有指向堆中对象的地址的值。
这里说下ldarg.0指令,意思是:将第一个参数放栈上,在实例方法中,第一个参数是默认传递的this,静态方法第一个参数就是实际显示传过来的值。为啥实例方法调用时要默认的传递一个this呢:为了指明方法要操作的实例对象。一个类可能存在N多对象,但方法只有一个,所以实例方法调用时,你得告诉方法要操作的是哪一个对象,因此就默认传递了this参数,举个例子:
Person leeteng = new Peron("leeteng",25);
Person wangteng = new Peron("wangteng",21);
第一句创建个25岁叫leeteng的对象,第二句类似。调用leeteng.ShowAge(),实际上相当于leeteng.ShowAge(this),这个this指向了堆中的leeteng对象,wangteng.ShowAge(this),这个this指向了堆中的wangteng对象,因为不同对象调用方法时默认尝试this指向了各自的实例,所以方法就在相应对象上操作,上面的代码写成IL是啥样子呢:
Person leeteng = new Peron("leeteng",25);
leeteng.ShowAge();
Person wangteng = new Peron("wangteng",21);
wangteng.ShowAge();
改IL:
ldstr "leeteng"
ldc.i4 25
newobj instance void Person::.ctor(string,int32)
call instance void Person::ShowAge()
ldstr "wangteng"
ldc.i4 21
newobj instance void Person::.ctor(string,int32)
call instance voi Person::ShowAge()
记住一句话:实例方法调用,默认传递this,this指向要方法要操作对象的地址(对象在堆上,地址在栈上)
局部变量:C#:
class zzz
{
public static void Main()
{
int i = 7;
long j = 8;
}
}
IL:
.assembly extern mscorlib{}
.assembly ak{}
.class private auto ansi Hello extends System.Object{
.method public hidebysig static void Main() il managed
{
.entrypoint
.locals(int32 v_0, int64 v_1)
ldc.i4 7
stloc.0
ldc.i4 8
conv.i8
stloc.1
ret
}
}
在上面C#程序中。Main函数中创建了两个变量i j ,它们是局部变量,所以在栈上创建,在IL中局部变量的名字是没啥意义的,因为都给他们编号了:0 1 2 。。
IL中,局部变量的创建由.locals(int32 v_0, int64 v_1)指令实现,并且给他们起了名字v_0 v_1等等,数据类型也从C#中的int long 变成了IL中的int32 int64,因为C#中的关键字int只是个别名,到了IL这就得改成真名了int32 int64.。。(因为IL只认真名)
ldc.i4 7 将7入栈(4字节) int32
stloc.0 从栈顶取个数(7),放到第一个局部变量里
ldc.i4 8 将8入栈(4字节)int32
conv.i8 将栈顶的数扩展到8字节(占8字节的数8) int64
stloc.1 将栈顶的数放到(8字节的8)放到第二个局部变量里
如此变完成了变量的初始化。
这个:
Int i = 8;
Int j = 7;
Int k;
K = i + j;
写成IL:
.locals(int32 v_0, int32 v_1, int32 v_2)
ldc.i4 8 数8入栈
stloc.0 把栈顶数放到第一个局部变量里
ldc.i4 7 数7入栈
stloc.1 把栈顶数放到第二个局部变量里
ldloc.0 第一个局部变量入栈
ldloc.1 第二个局部变量入栈
Add 从栈顶取俩数 相加 把结果压栈
stloc.2 把栈顶数放第三个局部变量里
如果想打印下结果:
ldloc.2
call void [mscorlib]System.Console::Write(int32)
15就显示出来了
字段:
class zzz
{
static int i= 6 ;
public long j = 7;
public static void Main()
{
}
}
写成IL:
.class private auto ansi Hello extends System.Object{
.field private static int32 i
.field public int64 j
.method public hidebysig static void Main() il managed
{
.entrypoint
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor(){
ldc.i4 7
conv.i8
stfld int64 Hello::j
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
.method public hidebysig specialname rtspecialname static void .cctor(){
ldc.i4 6
stsfld int32 Hello::i
ret
}
}
.field private static int32 i
.field public int64 j
定义两个字段,一个私有静态字段,一个公有实例字段
.method public hidebysig specialname rtspecialname instance void .ctor(){
ldc.i4 7 7压栈
conv.i8 4字节转8字节
stfld int64 Hello::j 取栈顶数存道Hello::j字段里
ldarg.0 this入栈
call instance void [mscorlib]System.Object::.ctor()调用父类构造函数
Ret 返回
}
.method public hidebysig specialname rtspecialname static void .cctor(){
ldc.i4 6
stsfld int32 Hello::i 去栈顶数存到静态字段Hello::i里
ret
}
注意:stsfld是存储静态字段的指令 stfld是存储实例字段的指令
看见静态构造函数的public修饰符了么,编译器生成的也是public,不过我感觉这个修饰符没用,因为静态构造函数是CLR调用的,修饰符无所谓的。
但实例构造函数就不同了,你得指定它的修饰符(如果不指定任何修饰符,编译器生成的修饰符是privatescope,具体啥意思还未清楚)
上面一点点C#就得这么堆IL,嘎嘎。
注意:int i = 4; 类似这样的C#声明并初始化的字段,都要在构造函数里进行初始化,而且初始化行为发生在调用父类构造函数以前。
Static int j = 9;这样的要在静态构造函数里进行初始化
字段赋值IL指令是:stfld(实例字段)stsfld(静态字段),而且字段名前面必须加上所属类型的名字Hello::i
局部变量赋值IL指令是:stloc
有了上面的例子,下面直接贴个例子(不是我写的):
a.cs
class zzz{
static int i= 6 ;
public long j = 7;
public static void Main()
{
new zzz();
}
static zzz()
{
System.Console.WriteLine("zzzs");
}
zzz()
{
System.Console.WriteLine("zzzi");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends System.Object
{
.field private static int32 i
.field public int64 j
.method public hidebysig static void vijay() il managed
{
.entrypoint
newobj instance void zzz::.ctor()
pop
ret
}
.method public hidebysig specialname rtspecialname static void .cctor() il managed
{
ldc.i4.6
stsfld int32 zzz::i
ldstr "zzzs"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
ldc.i4.7
conv.i8
stfld int64 zzz::j
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ldstr "zzzi"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
Output
zzzs
zzzi
上面代码主要演示了:字段的初始化先发生,还是构造函数里面的代码先执行。通过上例可以清楚了。
仔细看看IL实例构造函数里的代码:先进行字段初始化(必须是声明时就赋值了int i = 7这样的),再调用父类构造函数,最后执行构造函数里的代码
到现在,你可以通过看一些编译器生成的IL来了解C#代码的实质,例如:
a.cs
class zzz
{
public static void Main()
{
System.Console.WriteLine(10);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldc.i4.s 10
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
}
Output
10
a.cs
class zzz
{
public static void Main()
{
System.Console.WriteLine("{0}",20);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (int32 V_0)
ldstr "{0}"
ldc.i4.s 20
stloc.0
ldloca.s V_0
box [mscorlib]System.Int32
call void [mscorlib]System.Console::WriteLine(class System.String,class System.Object)
ret
}
}
Output
20
System.Console.WriteLine("{0}",20);
对应的真正方法调用:
call void [mscorlib]System.Console::WriteLine(class System.String,class System.Object)
第二个参数是个object类型的,而20是值类型,怎么对应呢:答案是装箱;
通过装箱,将值类型转化成引用类型。
说下:这里的ldloca.s V_0
在.net2.0以后应该就不对了,不是载入地址,而是直接载入值,应该写成:ldloc.s V_0。
通过装箱,解决了值类型向应用类型的转换,给方法调用提供了便利,但是:装箱,意味着要在堆上分配空间,会给GC造成负担,频繁装箱 拆箱,影响性能,还在堆上生成了大量的小对象,造成内存碎片和GC压力。而且丧失了变异时的类型检查(NET框架程序设计里有详细的介绍,看那吧),所以,理解了IL,我们就知道哪里会发生装箱操作,就能够尽量避免发生装箱,当然,在NET2.0里引用了泛型的概念,装箱的副作用就显得没那么突出了,不过理解它的机制仍是很必要的(NET框架设计有详解)
上面的是将局部变量装箱,下面是将字段装箱:
a.cs
class zzz {
static int i = 10;
public static void Main() {
System.Console.WriteLine("{0}",i);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends System.Object
{
.field private static int32 i
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldstr "{0}"
ldsfld int32 zzz::i
box [mscorlib]System.Int32
call void [mscorlib]System.Console::WriteLine(class System.String, class System.Object)
ret
}
.method public hidebysig specialname rtspecialname static void .cctor() il managed
{
ldc.i4.s 10
stsfld int32 zzz::i
ret
}
}
Output
10
如果没有静态构造函数呢,看下:
.class private auto ansi zzz extends System.Object
{
.field private static int32 i
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldstr "{0}"
ldsfld int32 zzz::i
box [mscorlib]System.Int32
call void [mscorlib]System.Console::WriteLine(class System.String, class System.Object)
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor() il managed {
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
}
虽然声明了静态字段,但是没有初始化,如果没有在构造函数中初始化,那么字段会保持默认值0,所以上面的结果是0.
下个例子:
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (int32 V_0)
ldloc.0
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
声明了局部变量,但没有初始化,所以输出结果是不确定的,你可以试试输出是什么。
上面的代码用C#是写不出来的,因为你使用了没有初始化的局部变量,编译不能通过,IL里没事。
下面例子演示调用有参数的方法:
a.cs
class zzz
{
public static void Main()
{
zzz a = new zzz();
a.abc(10);
}
void abc(int i) {
System.Console.WriteLine("{0}",i);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class zzz V_0)
newobj instance void zzz::.ctor()
stloc.0
ldloc.0
ldc.i4.s 10
call instance void zzz::abc(int32)
ret
}
.method private hidebysig instance void abc(int32 i) il managed
{
ldstr "{0}"
ldarg.s i
box [mscorlib]System.Int32
call void [mscorlib]System.Console::WriteLine(class System.String,class System.Object)
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor(){
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
}}
新IL指令ldarg.0 表示:将第一个参数加载到栈上(好像不是新的了)
接着:
class zzz
{
public static void Main()
{
zzz a = new zzz();
a.abc(10);
}
void abc(object i)
{
System.Console.WriteLine("{0}",i);
}
}
这次abc接受object参数,要想调用,怎么办,以前不用考虑,因为编译器背着你替你完成了(装箱),在IL中,你自己做主,所以在调用abc方法以前,必须完成装箱操作:
ldloc.s V_1
box [mscorlib]System.Int32
call instance void zzz::abc(class System.Object)
调用有返回值的方法:
class zzz
{
public static void Main()
{
int i;
zzz a = new zzz();
i = zzz.abc();
System.Console.WriteLine(i);
}
static int abc()
{
return 20;
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (int32 V_0,class zzz V_1)
newobj instance void zzz::.ctor()
stloc.1
call int32 zzz::abc()
stloc.0
ldloc.0
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
.method private hidebysig static int32 abc() il managed
{
.locals (int32 V_0)
ldc.i4.s 20
ret
}
}
上面有些代码是我直接抄的,可能会报错,大部分是没写构造函数,添上就行了。
如果返回值,那么在方法退出以前(ret指令以前),你必须把要返回的值放在栈上,类型必须和方法声明的类型一样。但是如果没有返回值,那么ret以前,栈必须是空的。
call int32 zzz::abc()
静态方法调用,不用this,如果调用有返回值的方法,方法返回时,返回值会压倒栈上,所以call int32 zzz::abc()完成后 ,方法返回值就会压到栈顶。
总结一下这贴的IL指令:
加载 存储局部变量:ldloc stloc
加载 存储字段 ldfld stfld(实例字段) ldsfld stsfld(静态字段)
加载参数 ldarg
上面所有的指令都有对应地址操作的指令,比如加载局部变量的地址:ldloca
装箱box
实例化一个对象 newobj