若是你真的想要理解C#代码,那么最好的方法就是经过理解由C#编译器生成的代码。本章和下面两章将关注于此。程序员
咱们将用一个短小的C#程序来揭开IL的神秘面纱,并解释由编译器生成的IL代码。这样,咱们就能够“一举两得”:首先,咱们将揭示IL的奥妙,其次,咱们将会更加直观地理解C#编程语言。编程
咱们将首先展现一个.cs文件,并随后经过C#编译器以IL编写一个程序。它的输出和cs文件是相同的。输出将会显示IL代码。这会加强咱们对C#和IL的理解。好吧,再也不啰嗦,这就开始咱们的冒险之旅。sass
a.cs编程语言
class zzz
{
public static void Main()
{
System.Console.WriteLine("hi");
zzz.abc();
}
public static void abc()
{
System.Console.WriteLine("bye");
}
}ide
c:\il>csc a.cs函数
c:\il>ildasm /output=a.il a.exe学习
a.ilthis
//
Microsoft (R) .NET Framework IL Disassembler. Version 1.0.2204.21
//
Copyright (C) Microsoft Corp. 1998-2000
//
VTableFixup Directory:
//
No data.
.subsystem
0x00000003
.corflags
0x00000001
.assembly
extern
mscorlib
{
.originator = (
03
68
91
16
D3 A4 AE
33
)
//
.h
..3
.hash
= (
52
44
F8 C9
55
1F
54
3F
97
D7 AB AD E2 DF 1D E0
F2 9D 4F BC )
//
RD..U.T?

.O.
.ver
1
:
0
:
2204
:
21
}
.assembly
a as
"
a
"
{
//
--- The following custom attribute is added automatically, do not uncomment -------
//
.custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::
//
.ctor(bool, bool) = ( 01 00 00 01 00 00 )
.hash
algorithm
0x00008004
.ver
0
:
0
:
0
:
0
}
.module
a.exe
//
MVID: {3C938660-2A02-11D5-9089-9752D1D64E03}
.class
private
auto
ansi
zzz
extends
[mscorlib]System.Object
{
.method
public
hidebysig
static
void
Main() il
managed
{
.entrypoint
//
Code size 16 (0x10)
.maxstack
8
IL_0000:
ldstr
"
hi
"
IL_0005:
call
void
[mscorlib]System.Console::WriteLine(
class
System.String)
IL_000a:
call
void
zzz::abc()
IL_000f:
ret
}
//
end of method zzz::Main
.method
public
hidebysig
static
void
abc() il
managed
{
//
Code size 11 (0xb)
.maxstack
8
IL_0000:
ldstr
"
bye
"
IL_0005:
call
void
[mscorlib]System.Console::WriteLine(
class
System.String)
IL_000a:
ret
}
//
end of method zzz::abc
.method
public
hidebysig
specialname
rtspecialname
instance
void
.ctor() il
managed
{
//
Code size 7 (0x7)
.maxstack
8
IL_0000:
ldarg.0
IL_0001:
call
instance
void
[mscorlib]System.Object::.ctor()
IL_0006:
ret
}
//
end of method zzz::.ctor
}
//
end of class zzz
//
*********** DISASSEMBLY COMPLETE ***********************
上面的代码是由IL反汇编器生成的。spa
在exe文件上执行ildasm后,咱们观察一下该程序所生成的IL代码。先排除一部分代码——它们对咱们理解IL是没有任何帮助的——包括一些注释、伪指令和函数。剩下的IL代码,则和原始的代码尽量的保持同样。设计
Edited a.il
.assembly
mukhi {}
.class
private
auto
ansi
zzz
extends
System.Object
{
.method
public
hidebysig
static
void
vijay() il
managed
{
.entrypoint
ldstr
"
hi
"
call
void
System.Console::WriteLine(
class
System.String)
call
void
zzz::abc()
ret
}
.method
public
hidebysig
static
void
abc() il
managed
{
ldstr
"
bye
"
call
void
System.Console::WriteLine(
class
System.String)
ret
}
}
c:\il>ilasm a.il
Output
hi
bye
经过研究IL代码自己来掌握IL这门技术的好处是,咱们从C#编译器那里学习到如何编写至关好的IL代码。找不到比C#编译器更权威的“大师”来教导咱们关于IL的知识。
建立静态函数abc的规则,与建立其它函数是相同的,诸如Main或vijay。由于abc是一个静态函数,因此咱们必须在.method伪指令中使用修饰符static。
当咱们想调用一个函数时,必须依次提供如下信息:
- 返回的数据类型
- 类的名称
- 被调用的函数名称
- 参数的数据类型
一样的规则还适用于当咱们调用基类的.ctor函数的时候。在函数名称的前面写出类的名称是必须的。在IL中,不能作出类的名称事先已经创建的假设。类的默认名称是咱们在调用函数时所在的类。
所以,上面的程序首先使用WriteLine函数来显示hi,并随后调用静态函数abc。这个函数还使用了WriteLine函数来显示bye。
a.cs
class
zzz
{
public
static
void
Main()
{
System.Console.WriteLine(
"
hi
"
);
}
static
zzz()
{
System.Console.WriteLine(
"
bye
"
);
}
}
a.il
.assembly
mukhi {}
.class
private
auto
ansi
zzz
extends
System.Object
{
.method
public
hidebysig
static
void
vijay() il
managed
{
.entrypoint
ldstr
"
hi
"
call
void
System.Console::WriteLine(
class
System.String)
ret
}
.method
private
hidebysig
specialname
rtspecialname
static
void
.cctor() il
managed
{
ldstr
"
bye
"
call
void
[mscorlib]System.Console::WriteLine(
class
System.String)
ret
}
}
Output
bye
hi
静态构造函数老是在任何其它代码执行以前被调用。在C#中,静态函数只是一个和类具备相同名称的函数。在IL中,函数名称改变为.cctor。所以,你可能注意到在先前的例子中,咱们使用了一个名为ctor的函数(而不须要事先定义)。
不管咱们什么时候调用一个无构造函数的类时,都会自动建立一个没有参数的构造函数。这个自动生成的构造函数具备给定的名称.ctor。这一点,应该加强咱们做为C#程序员的能力,由于咱们如今正处在一个较好的位置上来理解那些深刻实质的东西。
静态函数会被首先调用,以后,带有entrypoint伪指令的函数会被调用。
a.cs
class
zzz
{
public
static
void
Main()
{
System.Console.WriteLine(
"
hi
"
);
new
zzz();
}
zzz()
{
System.Console.WriteLine(
"
bye
"
);
}
}
a.il
.assembly
mukhi {}
.class
private
auto
ansi
zzz
extends
System.Object
{
.method
public
hidebysig
static
void
vijay() il
managed
{
.entrypoint
ldstr
"
hi
"
call
void
System.Console::WriteLine(
class
System.String)
newobj
instance
void
zzz::.ctor()
pop
ret
}
.method
private
hidebysig
specialname
rtspecialname
instance
void
.ctor() il
managed
{
ldarg.0
call
instance
void
[mscorlib]System.Object::.ctor()
ldstr
"
bye
"
call
void
[mscorlib]System.Console::WriteLine(
class
System.String)
ret
}
}
Output
hi
bye
在C#中的关键字new,被转换为汇编器指令newobj。这就为IL不是一门低级汇编语言而且还能够在内存中建立对象提供了证据。指令newobj在内 存中建立了一个新的对象。即便在IL中,咱们也不会知道new或newobj真正作了些什么。这就证明了IL并非另外一门高级语言,而是被设计为其它现代 语言都可以编译为IL这样一种方式。
使用newobj的规则和调用一个函数的规则是相同的。函数名称的完整原型是必需的。在这个例子中,咱们调用了无参数的构造函数,从而函数.ctor会被调用。在构造函数中,WriteLine函数会被调用。
正如咱们先前承诺的,这里,咱们将要解释指令ldarg.0。不管什么时候建立一个对象——一个类的实例,都会包括两个基本的实体:
当一个函数被调用时,它并不知道也不关心谁调用了它或它在哪里被调用。它从栈上检索它的全部参数。没有必要在内存中有一个函数的两份复制。这是由于,若是一个类包括了1兆的代码,那么每当咱们对其进行new操做时,都会占据额外的1兆内存。
当new被首次调用时,会为代码和变量分配内存。可是以后,在new上的每一次调用,只会为变量分配新的内存。从而,若是咱们有类的5个实例,那么就只有代码的一份复制,可是会有变量的5份独立的复制。
每一个非静态的或实例函数都传递了一个句柄,它表示调用这个函数的对象的变量位置。这个句柄被称为this指针。this由ldarg.0表示。这个句柄老是被传递为每一个实例函数的第1个参数。因为它老是被默认传递,因此在函数的参数列表中没有说起。
全部的操做都发生在栈上。pop指令移出栈顶的任何元素。在这个例子中,咱们使用它来移除一个zzz的实例,它是经过newobj指令被放置在栈顶的。
a.cs
class
zzz
{
public
static
void
Main()
{
System.Console.WriteLine(
"
hi
"
);
new
zzz();
}
zzz()
{
System.Console.WriteLine(
"
bye
"
);
}
static
zzz()
{
System.Console.WriteLine(
"
byes
"
);
}
}
a.il
.assembly
mukhi {}
.class
private
auto
ansi
zzz
extends
System.Object
{
.method
public
hidebysig
static
void
vijay() il
managed
{
.entrypoint
ldstr
"
hi
"
call
void
System.Console::WriteLine(
class
System.String)
newobj
instance
void
zzz::.ctor()
pop
ret
}
.method
private
hidebysig
specialname
rtspecialname
instance
void
.ctor() il
managed
{
ldarg.0
call
instance
void
[mscorlib]System.Object::.ctor()
ldstr
"
bye
"
call
void
[mscorlib]System.Console::WriteLine(
class
System.String)
ret
}
.method
private
hidebysig
specialname
rtspecialname
static
void
.cctor() il
managed
{
ldstr
"
byes
"
call
void
[mscorlib]System.Console::WriteLine(
class
System.String)
ret
}
}
Output
byes
hi
bye
尽管实例构造函数只在new以后被调用,但静态构造函数老是会首先被调用。IL会强制这个执行的顺序。对基类构造函数的调用不是必须的。所以,为了节省本书的篇幅,咱们不会展现程序的全部代码。
在某些状况中,若是咱们不包括构造函数的代码,那么程序就不会工做。只有在这些状况中,构造函数的代码才会被包括进来。静态构造函数不会调用基类的构造函数,this也不会被传递到静态函数中。
a.cs
class
zzz
{
public
static
void
Main()
{
int
i
=
6
;
long
j
=
7
;
}
}
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,
int64
V_1)
ldc.i4.6
stloc.0
ldc.i4.7
conv.i8
stloc.1
ret
}
}
在C#程序中,咱们在Main函数中建立了2个变量i和j。它们是局部变量,是在栈上建立的。请注意,在转换到IL的过程当中,变量的名称会被丢弃。
在IL中,变量经过locals伪指令来建立,它会把自身的名称分配给变量,以V_0和V_1等等做为开始。数据类型也会被修改——从int修改成 int32以及从long修改成int64。C#中的基本类型都是别名。它们都会被转换为IL所能理解的数据类型。
当前的任务是将变量i初始化为值6。这个值必须位于磁盘上或计算栈上。作这个事情的指令是ldc.i4.value。i4就是从内存中获取4个字节。
在上面语法中提到的value,是必需要放置到栈上的常量。在值6被放置到栈上以后,咱们如今须要将变量i初始化为这个值。变量i会被重命名为V_0,它是locals指令中的第一个变量。
指令stloc.0获取位于栈顶的值,也就是6,并将变量V_0初始化为这个值。初始化一个变量的过程是至关复杂的。
第2个ldc指令将7这个值复制到栈上。在32位的机器上,内存只能以32字节的块(Chunk)来分配。一样,在64位的机器上,内存是以64字节的块来分配的。
数值7被存储为一个常量并只须要4个字节,可是long须要8个字节。所以,咱们须要把4字节转换为8字节。指令conv.i8就是用于这个意图的。它把 一个8字节数字放在栈上。只有在这么作以后,咱们才能使用stloc.1来初始化第2个变量V_1为值7。从而会有stloc.1指令。
所以,ldc系列用于放置一个常量数字到栈上,而stloc用于从栈上获取一个值,并将一个变量初始化为这个值。
a.cs
class
zzz
{
static
int
i
=
6
;
public
long
j
=
7
;
public
static
void
Main()
{
}
}
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
ret
}
.method
public
hidebysig
specialname
rtspecialname
static
void
.cctor() il
managed
{
ldc.i4.6
stsfld
int32
zzz::i
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()
ret
}
}
历经艰难以后,如今,你终于看到了成功,并明白咱们为何想要你首先阅读本书了。
让咱们理解上面的代码,每次一个字段。咱们建立了一个静态变量i,并将其初始化为值6。因为没有为变量i指定一个访问修饰符,默认值就是private。C#中的修饰符static也适用于IL中的变量。
实际的操做如今才开始。变量须要被分配一个初始值。这个值必须只能在静态改造函数中分配,由于变量是静态的。咱们使用ldc来把值6放到栈上。注意到这里并无使用到locals指令。
为了初始化i,咱们使用了stsfld指令,用于在栈顶寻找值。stsfld指令的下一个参数是字节数量,它必须从栈上取得,用来初始化静态变量。在这个例子中,指定的字节数量是4。
变量名称位于类的名称以前。这与局部变量的语法正好相反。
对于实例变量j,因为它的访问修饰符是C#中的public,转换到IL,它的访问修饰符保留为public。因为它是一个实例变量,因此它的值会在实例变量中初始化。这里使用到的指令是stfld而不是stsfld。这里咱们须要栈上的8个字节。
剩下的代码和从前保持一致。所以,咱们能够看到stloc指令被用于初始化局部变量,而stfld指令则用于初始化字段。
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输出很是清晰地证明了——首先初始化全部的变量,而后再调用构造函数中的代码。
你可能还会注意到,基类的构造函数会被首先执行,随后,也只能是随后,在构造函数中编写的代码才会被调用。
这种收获确定会加强你对C#和IL的理解。
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
经过重载WriteLine函数,咱们可以打印出一个数字而不是字符串。
首先,咱们使用ldc语句把值10放到栈上。仔细观察,如今这个指令是ldc.i4.s,那么值就是10。任何指令都在内存中获取4个字节,可是当以.s结尾时则只获取1个字节。
随后,C#编译器调用正确的WriteLine函数的重载版本,它从栈上接受一个int32值。
这相似于打印出来的字符串:
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
如今咱们将研究如何在屏幕上打印一个数字。
WriteLine函数接受一个字符串,以后是可变数量的对象。{0}打印逗号后面的第1个对象。即便在C#代码中没有任何变量,在转换为IL代码时,就会建立一个int32类型的变量。
使用ldstr指令把字符串{0}加载到栈上。而后,咱们把做为参数传递到WriteLine函数的数字放到栈上。为了作到这样,咱们使用 ldc.i4.s来加载常量值到栈上。在这以后,咱们使用stloc.0指令将V_0初始化为20,而后使用ldloca.s加载局部变量的地址到栈上。
这里咱们面临的主要难题是,WriteLine函数接受一个字符串(做为一个参数),以后是一个对象,做为下一个参数。在这个例子中,变量是值类型而不是引用类型。
int32是一个值类型变量,可是WriteLine函数想要一个“合格的”引用类型的对象。
咱们如何解决把一个值类型转换为一个引用类型所面临的困难选择呢?正如前面提到的那样,咱们使用指令ldloca.s来加载局部变量V_0的地址到栈上,咱们的栈包括一个字符串,位于值类型变量V_0的前面。
接下来,咱们调用box指令。引用类型和值类型是.NET中仅有的两种变量类型。装箱是.NET用来将一个值类型变量转换为引用类型变量的方法。box指 令获取一个未装箱的或值类型的变量,并将它转换为一个装箱的或引用类型的变量。box指令须要栈上的值类型的地址,并在堆上为其相匹配的引用类型分配空 间。
堆是一块内存区域,用来存储引用类型。栈上的值会随着函数的结束而消失,可是堆会在至关长的一段时间是有效的。
一旦这个空间被分配了,box指令就会初始化引用对象的实例字段。而后,在堆中分配这个新建立的对象的内存位置到栈上。box指令须要栈上的局部变量的一块内存位置。
存储在栈上的常量是没有物理地址的。所以,变量V_0会被建立,以提供内存位置。
堆上的这个装箱版本相似于咱们所熟悉的引用类型变量。它实际上不具备任何类型,从而看起来像System.Object。为了访问它的特定值,咱们须要首先对它进行拆箱。WriteLine会在内部作这件事情。
被装箱的参数的数据类型,必须和那些地址位于栈上的变量的数据类型相同。咱们随后将解释这些细节。
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}
"
ldsflda
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
上面的代码用来显示静态变量的值。.cctor函数将静态变量初始化为值10。而后,字符串{0}会被存储到栈上。
ldsldfa函数加载栈上某个数据类型的静态变量的地址。而后,和往常同样,进行装箱。上面给出的关于box功能的解释,在这里也是相关的。
IL中的静态变量的工做方式和实例变量相同。惟一的区别是它们有本身的一套指令。像box这样的指令须要栈上的一块内存位置,这在静态变量和实例变量之间是没有区别的。
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}
"
ldsflda
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
}
}
Output
0
在前面的程序中,惟一的变化是咱们移除了静态构造函数。全部的静态变量和实例变量都会被初始化为ZERO。所以。IL不会生成任何错误。在内部,甚至在静态函数被调用以前,字段i就会被分配一个初始值ZERO。
a.cs
class
zzz
{
public
static
void
Main()
{
int
i
=
10
;
System.Console.WriteLine(i);
}
}
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)
ldc.i4.s
10
stloc.0
ldloc.0
call
void
[mscorlib]System.Console::WriteLine(
int32
)
ret
}
}
Output
10
咱们将局部变量i初始化为值0。这是不能在构造函数中完成的,由于变量i尚未在栈上被建立。而后,使用stloc.0来分配值10到V_0。以后,使用ldloc.0来把变量V_0放到栈上,从而它对于WriteLine函数是可用的。
以后,Writeline函数在屏幕上显示这个值。字段和本地变量具备相似的行为,只有一点不一样——它们使用不一样的指令。
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)
ldloc.0
call
void
[mscorlib]System.Console::WriteLine(
int32
)
ret
}
}
Output
51380288
全部的局部变量都必须被初始化,不然,编译器就会生成一个莫名其妙的错误信息。这里,即便咱们注释了ldc和stloc指令,也不会有错误在运行时生成。然而,会显示一个很是巨大的数字。
变量V_0没有被初始化为任何值,它是在栈上建立的,并包括在内存位置上分配给它的任何可用的值。在你我机器上的输出会有很大不一样。
在相似的状况中,C#编译器将丢给你一个错误,而且不容许你进一步继续下去,由于变量尚未被初始化。另外一方面,IL是一个“怪胎”。它的要求是很宽松 的。它生成很是少的错误或在源代码上进行很是少的健康检查。但也存在缺点,就是说,程序员在使用IL时不得不更加当心和尽职尽责。
a.cs
class
zzz
{
static
int
i;
public
static
void
Main()
{
i
=
10
;
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
ldc.i4.s
10
stsfld
int32
zzz::i
ldstr
"
{0}
"
ldsflda
int32
zzz::i
box
[mscorlib]System.Int32
call
void
[mscorlib]System.Console::WriteLine(
class
System.String,
class
System.Object)
ret
}
}
Output
10
在上面的例子中,一个静态变量会在函数中被初始化,而不是在它建立的时候,就像前面看到的那样。函数vijay会调用存在于静态函数中的代码。
上面给出的处理是初始化静态变量或实例变量的惟一方式:
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}
"
ldarga.s
i
box
[mscorlib]System.Int32
call
void
[mscorlib]System.Console::WriteLine(
class
System.String,
class
System.Object)
ret
}
}
Output
10
上面的程序示范了关于咱们能如何调用具备一个参数的函数。把参数放在栈上的规则相似于WriteLine函数的规则。
如今让咱们理解关于一个函数是如何从栈上接受参数的。咱们经过在函数声明中声明数据类型和参数名称来做为开始。这就像在C#中工做同样。
接下来,咱们使用指令ldarga.s加载参数i的地址到栈上。随后box将把这个对象的值类型转换为对象类型,最后WriteLine函数使用这些值在屏幕上显示输出。
a.cs
class
zzz
{
public
static
void
Main()
{
zzz a
=
new
zzz();
a.abc(
10
);
}
void
abc(
object
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,
int32
V_1)
newobj
instance
void
zzz::.ctor()
stloc.0
ldloc.0
ldc.i4.s
10
stloc.1
ldloca.s
V_1
box
[mscorlib]System.Int32
call
instance
void
zzz::abc(
class
System.Object)
ret
}
.method
private
hidebysig
instance
void
abc(
class
System.Object i) il
managed
{
ldstr
"
{0}
"
ldarg.1
call
void
[mscorlib]System.Console::WriteLine(
class
System.String,
class
System.Object)
ret
}
}
Output
10
在上面的例子中,咱们将一个整数转换为一个对象,由于WriteLine函数须要这个数据类型的参数。
接受这种转换的惟一方法是使用box指令。box指令将一个整数转换为一个对象。
在函数abc中,咱们接受一个System.Object,并使用ldarg指令而不是ldarga。这样作的缘由是,咱们须要该参数的值和它的地址。为了把参数的值放到栈上,须要一个新的指令。
所以,IL使用它们本身的一套指令来处理局部变量、字段和参数。
a.cs
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
}
}
Output
20
函数返回值。这里,静态函数abc被调用。咱们从函数的签名中了解到它返回一个整数。返回值会被存储到栈上。
所以,stloc.1指令从栈上获取值并把它放在局部变量V_1中,在这个特定的例子中,它是函数的返回值。
newobj也像一个函数。它返回一个对象——在咱们的例子中,它是类zzz的一个实例——并把它放到栈上。
stloc指令被屡次重复使用来初始化咱们的局部变量。只是想再次提醒你一下,ldloc是这个过程的反转。
函数使用ldc指令把一个值放到栈上,并随后使用ret指令终止执行。
所以,栈扮演着双重角色。
a.cs
class
zzz
{
int
i;
public
static
void
Main()
{
zzz a
=
new
zzz();
a.i
=
zzz.abc();
System.Console.WriteLine(a.i);
}
static
int
abc()
{
return
20
;
}
}
a.il
.assembly
mukhi {}
.class
private
auto
ansi
zzz
extends
System.Object
{
.field
private
int32
i
.method
public
hidebysig
static
void
vijay() il
managed
{
.entrypoint
.locals
(
class
zzz V_0)
newobj
instance
void
zzz::.ctor()
stloc.0
ldloc.0
call
int32
zzz::abc()
stfld
int32
zzz::i
ldloc.0
ldfld
int32
zzz::i
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
}
}
Output
20
在上面的例子中,惟一的改变是函数abc的返回值被存储在一个实例变量中。
- stloc把栈上的值分配到一个局部变量中。
- 另外一方面,ldloc把局部变量的值放到栈上。
不理解的是——为何这个看上去像zzz的对象必须被再次放在栈上,尤为abc既然是一个静态函数而不是实例函数。提示你一下,栈上的this指针是不会被传递到静态函数的。
此后,函数abc会被调用,它把值20放在了栈上。指令stfld接受栈上的值20,并用这个值初始化实例变量。
IL汇编器会以相似的方式来处理局部变量和实例变量,惟一的区别是,它们的初始化指令是不一样的。
指令ldfld不是指令stfld的反转操做。它把一个实例变量的值放在栈上,使之能够被WriteLine函数使用。