若是你真的想要理解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?![](http://static.javashuo.com/static/loading.gif)
![](http://static.javashuo.com/static/loading.gif)
.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函數使用。