詳解CLR中Jit編譯發(fā)生的過程
CLR是如何找到托管代碼的入口方法并對其Jit的呢?Jit編譯的發(fā)生過程是怎么樣的呢?Jit編譯器和Metadata表又有什么關(guān)系呢?本文試圖尋找出答案,在此之前,不妨先了解一下CLR Header的大致結(jié)構(gòu)。
以如下代碼為例:
- Example
- using System;
- namespace CLRTesing
- {
- class Program
- {
- static void Main(string[] args)
- {
- System.Console.WriteLine("Hello World!");
- Console.ReadKey();
- new P().Display();
- }
- Program()
- {
- Console.WriteLine("Constructor.");
- Console.ReadKey();
- }
- static Program()
- {
- Console.WriteLine("Static constructor.");
- Console.ReadKey();
- }
- }
- class P
- {
- public void Display()
- {
- System.Console.WriteLine("P!");
- Console.ReadKey();
- new Q().Display();
- Console.ReadKey();
- }
- }
- class Q
- {
- public void Display()
- {
- System.Console.WriteLine("Q!");
- Console.ReadKey();
- }
- }
- }
編譯后通過dumpbin工具的到其CLR Header,如圖所示:
從圖中可以看到,CLR Header由以下幾個部分組成:
1、CB:表示CLR Header的大小,單位是byte;
2、Run time version:運行時版本,包含兩部分MajorRuntimeVersion和MinorRuntimeVersion;
3、Metadata Directory:指出Metadata table的RVA和其大??;
4、Flag:這個標(biāo)識主要是供加載器使用,flag值為0x00000001表示當(dāng)前runtime image僅由IL代碼組成并且對CPU沒有特殊要求;值為0x00000002表示image只能被加載到32位機(jī)中,值為0x00010000表示運行時和jit編譯器需要追蹤方法的調(diào)試信息;
5、EntryPointToken:Metadata 表中標(biāo)記為EntryPoint的方法的MethodDef;
6、Resources Directory:CLR的資源,也就是托管資源的RVA和大小,注意與PE文件中存儲Win32資源的section不同;
7、StrongNameSignature Directory:PE文件中供CLR加載器使用的哈希值所處RVA和大?。?
8、CodeManagerTable Directory:Code Manager 表的RVA和其大小;
9、VTableFixups Directory:由非托管C++類型中虛方法的指針組成的數(shù)組;
10、ExportAddressTableJumps Directory:跳轉(zhuǎn)地址表的RVA和大??;
11、ManagedNativeHeader Directory:一般情況下為0。
以上結(jié)構(gòu)可以從CorHdr.h文件中看出,如果裝的是Visual Studio 2005,這個文件在\Microsoft Visual Studio 8\SDK\v2.0\include\。
查看托管PE文件的工具有很多,不用很復(fù)雜的,就園子里的大牛Anders Liu寫的CliPeViwer就很好用,用Reflector可以偷窺其代碼哦。
那么在上面這個結(jié)構(gòu)中我最關(guān)心的是Metadata directory和EntryPointToken,Metadata directory存提供了原數(shù)據(jù)所在內(nèi)存地址的范圍,EntryPointToken告訴我們在原數(shù)據(jù)表中哪個token標(biāo)識的方法是入口方法,這里一定是方法,所以這個token是以6開頭的一個數(shù)。
回到主題,我們從CLR已經(jīng)被載入內(nèi)存、mscorwks.dll中的_CorExeMain2方法接管主線程開始說起:
1、_CorExeMain2方法會調(diào)用System Domain中的SystemDomain::ExecuteMainMethod方法,然后由此方法再去調(diào)用其它方法(具體什么方法參見深入了解CLR的加載過程一文中的第8步), 通過MetaData表提供的接口查找包含.entrypoint的類型,接著返回入口方法(在C#中這個入口方法一定是Main方法)的一個MethodDesc類型的實例;獲取MethodDesc類型實例的這個過程我認(rèn)為是:CLR通過讀取MetaData表,定位入口方法所屬的類型,將包含該類型的Module載入,然后建立這個類型的EECLASS(EECLASS結(jié)構(gòu)中包含重要信息有:指向當(dāng)前類型父類的指針、指向方法表的指針、實例字段和靜態(tài)字段等)和這個類型所包含方法的Method Table(方法表由一個個Method Descripter組成,具體到內(nèi)存中就是指向若干MethodDesc類型實例的地址),通過EEClass::FindMethod方法找到并返回入口方法的MethodDesc類型實例。
MethodDesc這個類型很有意思,它有兩個重要的部分,一個部分叫做m_CodeOrIL,用來存儲編譯好的MSIL在內(nèi)存中的地址,初值為ffffffffffffffff,另一個部分叫做Stub,如果當(dāng)前代碼沒有被編譯為本地CPU指令,那么通過這個Stub會觸發(fā)對Jit編譯器的調(diào)用。
執(zhí)行上述代碼,
用Windbg 查看,如下:
- Windbg1
- 0:000> !name2ee *!CLRTesing.Program
- Module: 790c2000 (mscorlib.dll)
- --------------------------------------
- Module: 00a72c3c (Hello.exe)
- Token: 0x02000002
- MethodTable: 00a73048
- EEClass: 00a7129c
- Name: CLRTesing.Program
- 0:000> !name2ee *!CLRTesing.P
- Module: 790c2000 (mscorlib.dll)
- --------------------------------------
- Module: 00a72c3c (Hello.exe)
- Token: 0x02000003
- MethodTable:
- EEClass:
- Name: CLRTesing.P
- 0:000> !dumpmt -md 00a73048
- EEClass: 00a7129c
- Module: 00a72c3c
- Name: CLRTesing.Program
- mdToken: 02000002(D:\test\Hello\bin\Debug\Hello.exe)
- BaseSize: 0xc
- ComponentSize: 0x0
- Number of IFaces in IFaceMap: 0
- Slots in VTable: 7
- --------------------------------------
- MethodDesc Table
- Entry MethodDescJIT Name
- 79371278 7914b928 PreJIT System.Object.ToString()
- 7936b3b0 7914b930 PreJIT System.Object.Equals(System.Object)
- 7936b3d0 7914b948 PreJIT System.Object.GetHashCode()
- 793624d0 7914b950 PreJIT System.Object.Finalize()
- 00a7c011 00a73030 NONE CLRTesing.Program.Main(System.String[])
- 00a7c015 00a73038 NONE CLRTesing.Program..ctor()
- 00da0070 00a73040JIT CLRTesing.Program..cctor()
CLRTesing.Program類型的靜態(tài)構(gòu)造函數(shù)執(zhí)行時,入口方法Main和CLRTesing.Program的實例構(gòu)造函數(shù)還沒有被Jit,Main方法中引用到的CLRTesing.P類型也沒有被加載,所以它的Method Table和EEClass結(jié)構(gòu)也沒有建立起來。
#p#
2、在Windbg中detach debuggee,隨便敲一個字符讓程序繼續(xù)運行;接著,入口方法Main開始執(zhí)行,
因為Main方法第一次執(zhí)行,所以通過Stub,Jit編譯器會被喚起,由于Main方法引用了CLRTesing.P類型,那么在執(zhí)行前會將CLRTesing.P類型載入,并建立Method Table和其EEClass結(jié)構(gòu),當(dāng)然這個建立過程也要去查找MetaData表,我認(rèn)為這個過程是這樣的:
Main方法被調(diào)用,由于它沒有被Jit過,CLR會通過Main方法的MethodDesc結(jié)構(gòu)的Stub對Jit編譯器進(jìn)行調(diào)用,CLR通過MetaData表的接口找到Main方法對應(yīng)的Token,如下:
我們可以看到Main方法的RVA是0x00002050,于是去PE文件的.Text section中的Raw Data中查找image base+RVA這個位置處的IL代碼,接著Jit編譯器會對這段IL代碼進(jìn)行驗證,驗證過程通過調(diào)用CheckIL方法來實現(xiàn),這個方法的簽名可以是這樣的:
- CHECK CheckIL(RVA il);
- CHECK CheckIL(RVA il, COUNT_T size);
驗證結(jié)束后把這段IL代碼編譯成本地CPU指令,將編譯后后的CPU指令存到內(nèi)存并修改Main方法的MethodDesc結(jié)構(gòu)中m_CodeOrIL和Stub的值,讓它們指向這個新的內(nèi)存地址,當(dāng)這個方法被再次調(diào)用的時候就會直接通過這個地址訪問到本地CPU指令而不會觸發(fā)第二次編譯。對于這個過程大家的看法呢?用Windbg查看各對象情況:
- Windbg2
- 0:000> !name2ee *!CLRTesing.Program
- Module: 790c2000 (mscorlib.dll)
- --------------------------------------
- Module: 00a72c3c (Hello.exe)
- Token: 0x02000002
- MethodTable: 00a73048
- EEClass: 00a7129c
- Name: CLRTesing.Program
- 0:000> !name2ee *!CLRTesing.P
- Module: 790c2000 (mscorlib.dll)
- --------------------------------------
- Module: 00a72c3c (Hello.exe)
- Token: 0x02000003
- MethodTable: 00a730b8
- EEClass: 00a71730
- Name: CLRTesing.P
- 0:000> !name2ee *!CLRTesing.Q
- Module: 790c2000 (mscorlib.dll)
- --------------------------------------
- Module: 00a72c3c (Hello.exe)
- Token: 0x02000004
- MethodTable:
- EEClass:
- Name: CLRTesing.Q
- 0:000> !dumpmt -md 00a73048
- EEClass: 00a7129c
- Module: 00a72c3c
- Name: CLRTesing.Program
- mdToken: 02000002(D:\test\Hello\bin\Debug\Hello.exe)
- BaseSize: 0xc
- ComponentSize: 0x0
- Number of IFaces in IFaceMap: 0
- Slots in VTable: 7
- --------------------------------------
- MethodDesc Table
- Entry MethodDescJIT Name
- 79371278 7914b928 PreJIT System.Object.ToString()
- 7936b3b0 7914b930 PreJIT System.Object.Equals(System.Object)
- 7936b3d0 7914b948 PreJIT System.Object.GetHashCode()
- 793624d0 7914b950 PreJIT System.Object.Finalize()
- 00da00b0 00a73030JIT CLRTesing.Program.Main(System.String[])
- 00a7c015 00a73038 NONE CLRTesing.Program..ctor()
- 00da0070 00a73040JIT CLRTesing.Program..cctor()
- 0:000> !dumpmt -md 00a730b8
- EEClass: 00a71730
- Module: 00a72c3c
- Name: CLRTesing.P
- mdToken: 02000003(D:\test\Hello\bin\Debug\Hello.exe)
- BaseSize: 0xc
- ComponentSize: 0x0
- Number of IFaces in IFaceMap: 0
- Slots in VTable: 6
- --------------------------------------
- MethodDesc Table
- Entry MethodDescJIT Name
- 79371278 7914b928 PreJIT System.Object.ToString()
- 7936b3b0 7914b930 PreJIT System.Object.Equals(System.Object)
- 7936b3d0 7914b948 PreJIT System.Object.GetHashCode()
- 793624d0 7914b950 PreJIT System.Object.Finalize()
- 00a7c04c 00a730a8 NONE CLRTesing.P.Display()
- 00a7c058 00a730b0 NONE CLRTesing.P..ctor()
我們可以發(fā)現(xiàn)Main方法已經(jīng)被Jit,且它引用的CLRTesing.P類型的相關(guān)結(jié)構(gòu)也已經(jīng)建立起來了,而CLRTesing.P類型的Display方法所引用的CLRTesing.Q類型沒有被載入。
總結(jié)一下,Jit編譯針對的對象總是方法,不論是入口方法還是其他方法的Jit過程都類似上述過程,Metadata這這里的作用不言而喻,可以說沒有Metadata的支持就無法進(jìn)行Jit,我覺得Meatadata在Jit編譯期間的作用至少有三個:
1、Jit編譯器通過查找Metadata來找到入口方法;
2、Jit編譯器通過查找Metadata來定位待編譯方法并利用其RVA找到存儲于PE文件中的IL代碼在內(nèi)存中的實際地址;
3、Jit編譯器在找到IL代碼并準(zhǔn)備編譯為本地CPU指令前所進(jìn)行的IL代碼驗證同樣會用到Metadata,例如,驗證方法的合法性需要去核實方法參數(shù)數(shù)量是正確的、傳給方法的每個參數(shù)是否都有正確的類型、方法返回值是否正確等等。
文中是一些我通過Shared Source Common Language Infrastructure(SSCLI)看到的和感覺到的東西,希望能給大家理解Jit提供一點幫助,如果有錯誤的地方也請大家指出,大家一起學(xué)習(xí)。
最后要說明的是,SSCLI里東西僅作為理解CLR使用,與MS真正實現(xiàn)CLR的過程可能不一樣。最后,大家在看SSCLI的時候可以使用Source Insight,個人感覺還挺好用。
SSCLI的下載地址是:http://www.microsoft.com/downloads/details.aspx?FamilyId=8C09FD61-3F26-4555-AE17-3121B4F51D4D&displaylang=en。
本文來自Leo Zhang的博客園文章《深入了解Jit編譯發(fā)生的過程》
【編輯推薦】