-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);
}
}
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函數使用。