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