随笔- 7  文章- 0  评论- 16 


这一章弄几个小例子,给个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


 posted on 2008-09-19 14:12 红泥 阅读(...) 评论(...) 编辑 收藏