(翻译) 《C# to IL》第二章 IL基础
-2-
如果你真的想要理解C#代码,那么最好的方法就是通过理解由C#编译器生成的代码。本章和下面两章将关注于此。
我们将用一个短小的C#程序来揭开IL的神秘面纱,并解释由编译器生成的IL代码。这样,我们就可以“一箭双雕”:首先,我们将揭示IL的奥妙,其次,我们将会更加直观地理解C#编程语言。
我们将首先展示一个.cs文件,并随后通过C#编译器以IL编写一个程序。它的输出和cs文件是相同的。输出将会显示IL代码。这会增强我们对C#和IL的理解。好吧,不再啰嗦,这就开始我们的冒险之旅。
a.cs
class zzz
{
public static void Main()
{
System.Console.WriteLine("hi");
zzz.abc();
}
public static void abc()
{
System.Console.WriteLine("bye");
}
}
c:/il>csc a.cs
c:/il>ildasm /output=a.il a.exe
a.il
// 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反汇编器生成的。
在exe文件上执行ildasm后,我们观察一下该程序所生成的IL代码。先排除一部分代码——它们对我们理解IL是没有任何帮助的——包括一些注释、伪指令和函数。剩下的IL代码,则和原始的代码尽可能的保持一样。
Edited a.il
.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
{
public static void Main()
{
System.Console.WriteLine( " hi " );
}
static zzz()
{
System.Console.WriteLine( " bye " );
}
}
a.il
.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
{
public static void Main()
{
System.Console.WriteLine( " hi " );
new zzz();
}
zzz()
{
System.Console.WriteLine( " bye " );
}
}
a.il
.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。无论何时创建一个对象——一个类的实例,都会包括两个基本的实体:
- 函数
- 字段或变量,如data
当一个函数被调用时,它并不知道也不关心谁调用了它或它在哪里被调用。它从栈上检索它的所有参数。没有必要在内存中有一个函数的两份复制。这是因为,如果一个类包括了1兆的代码,那么每当我们对其进行new操作时,都会占据额外的1兆内存。
当new被首次调用时,会为代码和变量分配内存。但是之后,在new上的每一次调用,只会为变量分配新的内存。从而,如果我们有类的5个实例,那么就只有代码的一份复制,但是会有变量的5份独立的复制。
每个非静态的或实例函数都传递了一个句柄,它表示调用这个函数的对象的变量位置。这个句柄被称为this指针。this由ldarg.0表示。这个句柄总是被传递为每个实例函数的第1个参数。由于它总是被默认传递,所以在函数的参数列表中没有提及。
所有的操作都发生在栈上。pop指令移出栈顶的任何元素。在这个例子中,我们使用它来移除一个zzz的实例,它是通过newobj指令被放置在栈顶的。
a.cs
{
public static void Main()
{
System.Console.WriteLine( " hi " );
new zzz();
}
zzz()
{
System.Console.WriteLine( " bye " );
}
static zzz()
{
System.Console.WriteLine( " byes " );
}
}
a.il
.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
{
public static void Main()
{
int i = 6 ;
long j = 7 ;
}
}
a.il
.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
{
static int i = 6 ;
public long j = 7 ;
public static void Main()
{
}
}
a.il
.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
{
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
.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
{
public static void Main()
{
System.Console.WriteLine( 10 );
}
}
a.il
.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
{
public static void Main()
{
System.Console.WriteLine( " {0} " , 20 );
}
}
a.il
.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
{
static int i = 10 ;
public static void Main()
{
System.Console.WriteLine( " {0} " , i);
}
}
a.il
.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
.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
{
public static void Main()
{
int i = 10 ;
System.Console.WriteLine(i);
}
}
a.il
.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
.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
{
static int i;
public static void Main()
{
i = 10 ;
System.Console.WriteLine( " {0} " ,i);
}
}
a.il
.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
{
public static void Main()
{
zzz a = new zzz();
a.abc( 10 );
}
void abc( int i)
{
System.Console.WriteLine( " {0} " ,i);
}
}
a.il
.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
{
public static void Main()
{
zzz a = new zzz();
a.abc( 10 );
}
void abc( object i)
{
System.Console.WriteLine( " {0} " ,i);
}
}
a.il
.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
{
public static void Main()
{
int i;
zzz a = new zzz();
i = zzz.abc();
System.Console.WriteLine(i);
}
static int abc()
{
return 20 ;
}
}
a.il
.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
{
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
.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函数使用。