<iframe align="top" marginwidth="0" marginheight="0" src="http://www.zealware.com/46860.html" frameborder="0" width="468" scrolling="no" height="60"></iframe>
.NET值類型變量“活”在哪個堆棧中?
——MSIL學習筆記(一)
金旭亮
不管是什么語言編的.NET程序,最后都會被各自的編譯器編譯成MSIL。當程序運行時,.NET JIT編譯器從程序集中讀入IL指令并將其動態編譯為可被本地CPU執行的機器指令再執行。
程序集中的IL代碼以二進制方式存在,人閱讀起來相當不便,正如傳統的Win32程序可以被反匯編成匯編程序,.NET程序集中的IL代碼也可以被反匯編成易于閱讀的IL匯編程序。如果您愿意的話,可以用任意一個文本編輯器直接撰寫IL匯編源代碼,然后使用ilasm.exe程序將其編譯為包含二進制形式的IL指令。CLR只能執行二進制的IL指令。
.NET SDK
的另一個工具ildasm.exe可以用于將一個程序集反匯編為IL程序,在學習.NET時,這個工具非常有用,可以展示出高級語言(如C#和VB.NET)編寫的程序是如何被CLR執行的。
然而,相比C#和VB.NET的資料滿天飛,MSIL的技術資料少得可憐。我能夠查閱的只有MSDN中有關IL指令的文檔(還只是針對Reflection.Emit名字空間中的類的),以及一本由Serge Lidin著的《inside Microsoft .NET IL assembler》, Serge Lidin是匯編器ilasm.exe工具的主要開發者,因此,他的書應具有相當的權威性,然而,這位技術牛人的寫作水平實在不敢恭維,整本書象是一本參考手冊。此書國內引進了中文版,然而翻譯得很不好。幸運的是其光盤中附上了英文原版,實乃國人之大幸。
IL
可以看成是一個“面向對象的匯編語言”,它提供了許多指令直接對對象進行操作,比如newobj指令創建對象,box指令進行裝箱等。
IL
指令的一個最重要特性是它是基于堆棧的。幾乎每一條指令都要與堆棧打交道:或者向堆棧中Push一些數據,或者從中Pop一些數據。
請看以下C#代碼段:
class
Program
{
static
void
Main(
string
[] args)
{
int
i = 100;
int
j = 200;
int
reslut = i + j;
}
}
C#編譯器將生成以下IL指令,其功能我在注釋中有詳細說明:
.method private hidebysig static voidMain(string[] args) cil managed
{
.entrypoint
// 代碼大小
15 (0xf)
.maxstack2
.locals init ([0] int32 i,
[1] int32 j,
[2] int32 reslut)
IL_0000:nop
IL_0001:ldc.i4.s
100 //將100壓入堆棧
IL_0003:stloc.0
//從堆棧中彈出先前壓入的100,傳給局部變量
i
IL_0004:ldc.i4
0xc8 //將200壓入堆棧
IL_0009:stloc.1
//從堆棧中彈出先前壓入的200,傳給局部變量
j
IL_000a:ldloc.0
//將局部變量
i的值壓入堆棧
IL_000b:ldloc.1
//將局部變量
j的值壓入堆棧
IL_000c:add
//連繼彈出兩個整數,相加得300,又壓入堆棧
IL_000d:stloc.2
//從堆棧中彈出結果,保存到局部變量
reslut中
IL_000e:ret
//返回指令
} // end of method Program::Main
可以看到,所有的指令都涉及到堆棧。
然而,我在研究IL匯編程序的時候,卻被“堆棧”兩個字弄糊涂了。
幾乎所有的C#書,都說值類型變量是生存在堆棧中,當函數結束時會自動銷毀。那么,這里的堆棧與上述IL代碼中的堆棧是不是一回事?
請看上述IL程序中有一個MaxStack指令,查看資料,得知其含義是為evaluation stack保留兩個槽(slot),注意,這里的堆棧英文原文是evaluation stack,MSDN中文版譯為“計算堆棧”,slot可用于存放值對象,大小是可變的。換句話說,evaluation stack中的每一個slot可以存放一個值對象(對象引用也可看成是一種“特殊”的值變量,其值代表內存地址)或各種CLR直接支持的基本類型數據。
從上述IL程序中可以很明顯地看到,局部變量i,j和result絕不會生存于evaluation stack,因為它只有2個slot,而我們有3個變量。那它們“活在”在哪兒?
IL程序中引人注目的一句是locals init指令,這提醒我們函數擁有另一塊內存區域專用于存放局部變量,所以,聲明為局部變量的值類型并不“活”在evaluation stack中。那么,為何所有的
C#書(包括大名鼎鼎的Jeffrey Richter所著之《.NET框架程序設計》)都說值類型變量“活”在堆棧中?此堆棧在哪?至少有一點可以肯定,這個堆棧不會指的是
evaluation stack。
用ildasm.exe查看程序集清單(manifest),發現其中有一句:
.stackreserve 0x00100000
上述語句讓CLR在裝入程序集時保存1M的堆棧空間,這個空間供托管進程的托管線程使用,稱為線程堆棧(Thread Stack)。既是線程堆棧,自然與線程相關,由于.NET托管進程可以創建多個托管線程,因此,每個線程也應該有自己的堆棧(Jeffrey Richter說也是1M,查看也是這位老先生寫的《Windows核心編程》,說在Win2000在創建線程時其堆棧大小是可調整的)。
.NET下每個托管線程都對應著一個線程函數,因此函數中定義的局部變量是在它擁有的線程堆棧中分配,而IL程序中的maxstack指令則從這一個1M的線程堆棧中再劃出一塊空間來作為evaluation stack。
考慮一下函數調用的問題。
IL使用call和callvirt兩條指令調用特定類型所提供的方法。這就有一個函數參數傳送的問題。以call指令為例,MSDN說在調用call指令之前,要將所有的實參壓入evaluation stack,然后call指令再將其彈出,之后控制才會轉到被調用的函數,而當被調用的函數執行完畢時,ret指令負責“將函數的返回值”從“被調用者的堆棧”(callee’s
evaluation
stack)復制到“調用者堆棧”(caller
evaluation
stack)中。您看MSDN文檔中居然又出現了兩個堆棧,是否有點暈了嗎?
查看Serge Lidin的書,他給出了這樣一個圖:
如上圖所示:CLR會給每一個被調用的方法分配三塊內存,除了上面講到的兩塊(Evaluation stack和局部變量表Local Variable table),還有一塊是參數表(Argument table)。
問題終于明晰了,call指令完成的工作應該是這樣的:
調用者按要調用函數的參數準備好實參,將它們壓入“自己的”evaluation stack中,然后,call指令執行,它從調用者的evaluation stack彈出這些參數,放入被調用函數的Argument Table中。一切準備工作就緒,這時才開始執行被調用函數的第一條IL指令。
當被調用函數執行完畢,如果有返回值,這個值應該被放在被調用函數自己的evaluation stack中(因為IL指令總是與堆棧打交道),然后,ret指令(每個函數最后一定是這條指令)將其彈出,再壓入調用者的evaluation stack中,完成這一工作之后,執行流程轉回到調用者。
因此,線程每調用一個函數,將導致圖中所示的三塊區域在1M的線程堆棧中分配給調用函數,對于遞歸調用的情況,后調用的函數占用的內存區域將“壓”在其調用者內存區域之上,每執行完一個函數,對應的棧頂指針移動一個位移(大小剛好等于此函數先前所占用的內存),從而導致這些內存被釋放,其中的局部變量不再有效。
分析.NET程序的IL指令還會得到一些有趣的結果,后面我會有更多的文章與網友們進行技術交流。
注:由于手頭的資料不足,
此文所述內容僅是本人對CLR內部運行機理的一個推測,如有錯誤,敬請指正。by the way,望有網友能提供更多的MSIL技術資料信息,在此謝謝了。:-)
轉載請注明作者及出處。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1451065
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
