通過MSIL了解CLR的運行原理 作者: Wednesday, April 17 2002 2:12 PM
作為.NET最低層次的公共基礎,微軟中介語言(MSIL或IL)對一般開發者具有非常重要的意義。除了好奇心以外,仔細研究應用程序的IL能讓你更為清楚地了解到公共語言運行時(CLR)執行高級C#或VB.NET代碼的基本原理,從而有助于你發現和解決一些比較細微的問題。
在這篇文章里,我將引領讀者了解IL,學習有關的一些關鍵指令,同時對CLR的操作機理做一點基礎性解釋。我不打算教你用IL編程,而是分析一些IL語法和語句使你對IL有更多了解。
ILDASM簡介 微軟的IL拆卸實用程序Ildasm.exe(通常位于\Program Files\Microsoft.Net\FrameworkSDK\Bin目錄下)可以析構.NET assembly(裝配)、根據你的要求從程序中抽取IL代碼。對某一assembly調用該使用程序后,ILDASM會給出該assembly中所有類和名稱空間的一個視圖,如圖A所示:
/article/UploadPic/200671911658800.gif 圖A
ILDASM瀏覽assembly
當你進到某個類的成員或其方法,ILDASM就會為你顯示該成員的IL代碼。如果之前你曾經看到過匯編器或J++字節碼,那么IL可能在你看來會覺得有點眼熟。在另一方面,如果你僅對抽象的高級程序語言有所了解,那么IL看起來更像是胡言亂語。
好,現在你知道如何窺視assembly的IL代碼了,但這些代碼都意味著什么呢?在回答這個問題之前,首先讓我們先來了解下CLR的有關知識。
虛擬CPU 對.NET程序來說,.NET CLR在功能上就如同一塊虛擬的CPU,它執行IL代碼、操作數據。CLR和真實的CPU類似之處在于它們都不直接操作內存中的變量而是使用程序變量的臨時拷貝,CLR把這些程序變量存放在堆棧上。從內存拷貝某個變量到堆棧的行為稱做裝載(loading),而從堆棧拷回某個變量到內存的行為則被稱做存儲(storing)。
所以把兩個數字相加的過程應該是這樣的:
1.裝載第1個數字并把它推入堆棧。
2.裝載第2個數字并把它推入堆棧。
3.從堆棧中取出這兩個數字并把它們相加。
4.把結果存儲到內存。
什么是堆棧? 理解IL的關鍵是知道堆棧的工作原理。堆棧是一種抽象數據結構,其操作機理是后進先出。當你把新條目推進堆棧時,已經在堆棧內的任何條目都會壓到堆棧的深處。同樣的,把一個條目從堆棧移出則會讓堆棧內的其他條目都向堆棧的頂部移動。只有堆棧最頂端的條目能從堆棧中取出,條目離開堆棧的順序和它們被推進堆棧的順序一樣。你不妨回想下自動售貨機的裝貨和取貨過程就明白了。
重要的IL語句 既然你已經明白了CLR操作的基礎知識,下面我們就接著討論你面前的那些代碼。怎么?沒有看到什么代碼?那么請你看看這里列出的IL代碼。
Sidebar A: Sample IL listing .method private instance voidListen() cil managed { // Code size 169 (0xa9) .maxstack4 .locals init ([0] unsigned int8[] buff, [1] class [System]System.Net.Sockets.NetworkStream ChatStream, [2] class [System]System.Net.Sockets.TcpClient RemoteClient, [3] string strHandle, [4] class Chat.ChatServer/TextRelayer 'handler', [5] class [mscorlib]System.Threading.Thread t) IL_0000:nop IL_0001:nop .try { IL_0002:br IL_0091 IL_0007:ldarg.0 IL_0008:ldfldclass [System]System.Net.Sockets.TcpListener Chat.ChatServer::Listener IL_000d:callvirt instance class [System]System.Net.Sockets.TcpClient [System]System.Net.Sockets.TcpListener::AcceptTcpClient() IL_0012:stloc.2 IL_0013:ldloc.2 IL_0014:callvirt instance class [System]System.Net.Sockets.NetworkStream [System]System.Net.Sockets.TcpClient::GetStream() IL_0019:stloc.1 IL_001a:ldc.i4.s 26 IL_001c:newarr [mscorlib]System.Byte IL_0021:stloc.0 IL_0022:ldloc.1 IL_0023:ldloc.0 IL_0024:ldc.i4.0 IL_0025:ldc.i4.s 24 IL_0027:callvirt instance int32 [System]System.Net.Sockets.NetworkStream::Read(unsigned int8[], int32, int32) IL_002c:pop IL_002d:call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_ASCII() IL_0032:ldloc.0 IL_0033:callvirt instance string [mscorlib]System.Text.Encoding::GetString(unsigned int8[]) IL_0038:stloc.3 IL_0039:ldarg.0 IL_003a:ldfldclass Chat.ChatServer/IncomingChatRequest Chat.ChatServer::RequestConnect IL_003f:ldloca.s strHandle IL_0041:callvirt instance bool Chat.ChatServer/IncomingChatRequest::Invoke(string&) IL_0046:brtrue.s IL_0051 IL_0048:ldloc.2 IL_0049:callvirt instance void [System]System.Net.Sockets.TcpClient::Close() IL_004e:nop IL_004f:br.s IL_008f IL_0051:nop IL_0052:ldloca.s strHandle IL_0054:ldarg.0 IL_0055:ldflda class Chat.ChatServer/IncomingText Chat.ChatServer::RelayText IL_005a:ldloca.s ChatStream IL_005c:newobj instance void Chat.ChatServer/TextRelayer::.ctor(string&, class Chat.ChatServer/IncomingText&, class [System]System.Net.Sockets.NetworkStream&) IL_0061:stloc.s'handler' IL_0063:ldloc.s'handler' IL_0065:dup IL_0066:ldvirtftninstance void Chat.ChatServer/TextRelayer::HandleChat() IL_006c:newobj instance void [mscorlib]System.Threading.ThreadStart::.ctor(object, native int) IL_0071:newobj instance void [mscorlib]System.Threading.Thread::.ctor(class [mscorlib]System.Threading.ThreadStart) IL_0076:stloc.st IL_0078:ldarg.0 IL_0079:ldfldclass [System]System.Collections.Specialized.ListDictionary Chat.ChatServer::ChatThreads IL_007e:ldloc.3 IL_007f:ldloc.st IL_0081:callvirt instance void [System]System.Collections.Specialized.ListDictionary::Add(object, object) IL_0086:nop IL_0087:ldloc.st IL_0089:callvirt instance void [mscorlib]System.Threading.Thread::Start() IL_008e:nop IL_008f:nop IL_0090:nop IL_0091:ldc.i4.1 IL_0092:brtrue IL_0007 IL_0097:leave.sIL_00a6 }// end .try finally { IL_0099:nop IL_009a:ldstr"Listener Thread Aborted." IL_009f:call void [mscorlib]System.Console::WriteLine(string) IL_00a4:nop IL_00a5:endfinally }// end handler IL_00a6:nop IL_00a7:nop IL_00a8:ret } // end of method ChatServer::Listen
你首先看見的是對當前方法的IL聲明,其中包括方法的名字,返回類型、參數列表以及附著于該方法的其他修飾關鍵詞(static/shared、public、virtual等等)。對象構造器則被賦給一個特殊的名字:.ctor。
在IL中,方法參數按照它們在參數列表中的位置依次被引用。如果方法是靜態或共享方法,那么參數0則是參數列表中的第1個參數。而對實例方法來說,參數0則是指向該方法所在類的實例的指針(Me或者this)。方法中的所有局部變量都在.locals標記的段落中以同樣的方式聲明。
在聲明所有的局部變量以后,程序的實際正文才開始。每條IL指令,或opcode都可以根據你的喜好以一個IL_ 標記作為代碼行開頭。我們接下來再了解些更重要的IL指令。
變量用法 以LD開頭的指令把變量從內存裝載到堆棧供其操作。裝載指令有若干條,每一條裝載指令都操作特定類型的變量。以下就是其中的一些裝載指令:
LDC把一個數字常數裝入堆棧。這條指令有兩個修飾詞。第一個是類型標識符,第二個是實際的數值。 LDLOC把一個局部變量裝入堆棧。另外還有一條LDLOCA指令把一個局部變量的地址(而非變量的內容)裝入堆棧。變量由它們在.locals節的位置標識。這些指令裝載位置4及以后位置使用不同的語法,但是索引號會出現在指令中。 LDARG裝載成員的一個參數,而LDARGA指令則裝載參數的地址。變量由它們在.locals節中的位置標識。這些指令裝載位置4及以后位置使用不同的語法,但是索引號仍然出現在指令中。 LDELEM把數組元素裝入堆棧而且通常先于表示這個索引的其他裝載語句之前使用。 LDLEN把一個數組的長度裝入堆棧。 LDFLD和LDSFLD把類域(成員變量)和靜態類域裝入堆棧。域由一個全名識別。 每一條裝載指令都有對應的一條存儲指令,后者以ST開頭,負責把一個條目存入內存。例如,STLOC就負責把堆棧最頂端的條目存入一個局部變量。存儲指令指定變量的句法規則通常和它們對應的裝載指令類似。
比較操作 如果你不能比較兩個值而且根據其比較結果做出決定,那么許多問題都無法用任何程序語言來解決。IL有一套比較操作符,它們都以C字母開頭,比較堆棧中的值。通常,如果比較結果為真則會把1推入堆棧否則就推入0。
大多數這類指令都很容易由它們的名字區分出來。例如,CEQ比較兩個值是否相等,而CGT則確定堆棧最頂端的值是否比第二個最頂端值更大。 CLT類同于CGT,不過執行的是小于比較操作。
Goto 通常,在對兩個值進行比較之后會根據比較的結果結果實施一些操作。IL分支指令(以BR開頭)根據堆棧最頂端的條目中的內容跳到其他指令。BRTRUE 和BRFALSE彈出堆棧最頂端的條目,然后根據該項為真(1)還是為假(0)而分別跳到指定的代碼行。如果沒有執行指令跳躍則繼續執行下一條指令。另外還有一個無條件分支操作符BR,它總是跳到指定的代碼行。
你會發現分支操作就好像源代碼中的if語句以及顯式執行的Goto操作。IL中的分支命令同樣具有高級流程控制結構的對等體,比如:if, case, while, for等等。
創造新對象和調用其他代碼 CALL和CALLVIRT指令調用其他方法和函數。CALL通常表示被調用的方法是靜態的或共享的,而CALLVIRT則用于實例方法。就兩種指令來說,方法的名字都會在指令中包括。被送到方法的任何參數都會被彈出堆棧而且要在方法被調用之前裝載。
因為創建一個新對象需要調用構造器,所以IL的對象創建也類似于其他的方法調用。參數首先被裝載到堆棧,然后執行NEWOBJ指令,它調用對象的構造器同時把對象的索引放回堆棧。指令中得有對象的名字。
以上就是大致的IL語法操作。除了滿足你內心的求知欲望以外,我希望你能從我的闡述中得到足夠的信息來理解IL代碼的真實含義。
|