首页 > 编程开发 > Objective-C编程 >
-
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
// 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反汇编器生成的。
在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。无论何时创建一个对象——一个类的实例,都会包括两个基本的实体:
函数
字段或变量,如data
当一个函数被调用时,它并不知道也不关心谁调用了它或它在哪里被调用。它从栈上检索它的所有参数。没有必要在内存中有一个函数的两份复制。这是因为,如果一个类包括了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);
}
}
在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中,不能做出类的名称事先已经建立的假设。类的默认名称是我们在调用函数时所在的类。