CLR是如何找到托管代码的入口方法并对其Jit的呢?Jit编译的发生过程是怎么样的呢?Jit编译器和Metadata表又有什么关系呢?本文试图寻找出答案,在此之前,不妨先了解一下CLR Header的大致结构。
以如下代码为例:
- 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:这个标识主要是供加载器使用,flag值为0x00000001表示当前runtime image仅由IL代码组成并且对CPU没有特殊要求;值为0x00000002表示image只能被加载到32位机中,值为0x00010000表示运行时和jit编译器需要追踪方法的调试信息;
5、EntryPointToken:Metadata 表中标记为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++类型中虚方法的指针组成的数组;
10、ExportAddressTableJumps Directory:跳转地址表的RVA和大小;
11、ManagedNativeHeader Directory:一般情况下为0。
以上结构可以从CorHdr.h文件中看出,如果装的是Visual Studio 2005,这个文件在\Microsoft Visual Studio 8\SDK\v2.0\include\。
查看托管PE文件的工具有很多,不用很复杂的,就园子里的大牛Anders Liu写的CliPeViwer就很好用,用Reflector可以偷窥其代码哦。
那么在上面这个结构中我最关心的是Metadata directory和EntryPointToken,Metadata directory存提供了原数据所在内存地址的范围,EntryPointToken告诉我们在原数据表中哪个token标识的方法是入口方法,这里一定是方法,所以这个token是以6开头的一个数。
回到主题,我们从CLR已经被载入内存、mscorwks.dll中的_CorExeMain2方法接管主线程开始说起:
1、_CorExeMain2方法会调用System Domain中的SystemDomain::ExecuteMainMethod方法,然后由此方法再去调用其它方法(具体什么方法参见深入了解CLR的加载过程一文中的第8步), 通过MetaData表提供的接口查找包含.entrypoint的类型,接着返回入口方法(在C#中这个入口方法一定是Main方法)的一个MethodDesc类型的实例;获取MethodDesc类型实例的这个过程我认为是:CLR通过读取MetaData表,定位入口方法所属的类型,将包含该类型的Module载入,然后建立这个类型的EECLASS(EECLASS结构中包含重要信息有:指向当前类型父类的指针、指向方法表的指针、实例字段和静态字段等)和这个类型所包含方法的Method Table(方法表由一个个Method Descripter组成,具体到内存中就是指向若干MethodDesc类型实例的地址),通过EEClass::FindMethod方法找到并返回入口方法的MethodDesc类型实例。
MethodDesc这个类型很有意思,它有两个重要的部分,一个部分叫做m_CodeOrIL,用来存储编译好的MSIL在内存中的地址,初值为ffffffffffffffff,另一个部分叫做Stub,如果当前代码没有被编译为本地CPU指令,那么通过这个Stub会触发对Jit编译器的调用。
执行上述代码,
用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类型的静态构造函数执行时,入口方法Main和CLRTesing.Program的实例构造函数还没有被Jit,Main方法中引用到的CLRTesing.P类型也没有被加载,所以它的Method Table和EEClass结构也没有建立起来。
#p#
2、在Windbg中detach debuggee,随便敲一个字符让程序继续运行;接着,入口方法Main开始执行,
因为Main方法第一次执行,所以通过Stub,Jit编译器会被唤起,由于Main方法引用了CLRTesing.P类型,那么在执行前会将CLRTesing.P类型载入,并建立Method Table和其EEClass结构,当然这个建立过程也要去查找MetaData表,我认为这个过程是这样的:
Main方法被调用,由于它没有被Jit过,CLR会通过Main方法的MethodDesc结构的Stub对Jit编译器进行调用,CLR通过MetaData表的接口找到Main方法对应的Token,如下:
我们可以看到Main方法的RVA是0x00002050,于是去PE文件的.Text section中的Raw Data中查找image base+RVA这个位置处的IL代码,接着Jit编译器会对这段IL代码进行验证,验证过程通过调用CheckIL方法来实现,这个方法的签名可以是这样的:
- CHECK CheckIL(RVA il);
- CHECK CheckIL(RVA il, COUNT_T size);
验证结束后把这段IL代码编译成本地CPU指令,将编译后后的CPU指令存到内存并修改Main方法的MethodDesc结构中m_CodeOrIL和Stub的值,让它们指向这个新的内存地址,当这个方法被再次调用的时候就会直接通过这个地址访问到本地CPU指令而不会触发第二次编译。对于这个过程大家的看法呢?用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()
我们可以发现Main方法已经被Jit,且它引用的CLRTesing.P类型的相关结构也已经建立起来了,而CLRTesing.P类型的Display方法所引用的CLRTesing.Q类型没有被载入。
总结一下,Jit编译针对的对象总是方法,不论是入口方法还是其他方法的Jit过程都类似上述过程,Metadata这这里的作用不言而喻,可以说没有Metadata的支持就无法进行Jit,我觉得Meatadata在Jit编译期间的作用至少有三个:
1、Jit编译器通过查找Metadata来找到入口方法;
2、Jit编译器通过查找Metadata来定位待编译方法并利用其RVA找到存储于PE文件中的IL代码在内存中的实际地址;
3、Jit编译器在找到IL代码并准备编译为本地CPU指令前所进行的IL代码验证同样会用到Metadata,例如,验证方法的合法性需要去核实方法参数数量是正确的、传给方法的每个参数是否都有正确的类型、方法返回值是否正确等等。
文中是一些我通过Shared Source Common Language Infrastructure(SSCLI)看到的和感觉到的东西,希望能给大家理解Jit提供一点帮助,如果有错误的地方也请大家指出,大家一起学习。
最后要说明的是,SSCLI里东西仅作为理解CLR使用,与MS真正实现CLR的过程可能不一样。最后,大家在看SSCLI的时候可以使用Source Insight,个人感觉还挺好用。
SSCLI的下载地址是:http://www.microsoft.com/downloads/details.aspx?FamilyId=8C09FD61-3F26-4555-AE17-3121B4F51D4D&displaylang=en。
本文来自Leo Zhang的博客园文章《深入了解Jit编译发生的过程》
【编辑推荐】