Leo Zhang

A simple man with my own ideal

深入了解Jit编译发生的过程

    CLR是如何找到托管代码的入口方法并对其Jit的呢?Jit的发生过程是怎么样的呢?Jit编译器和Metadata表又有什么关系呢?本文试图寻找出答案,在此之前,不妨先了解一下CLR Header的大致结构。
    
以如下代码为例:
Example

    编译后通过dumpbin工具的到其CLR Header,如图所示:

    从图中可以看到,CLR Header由以下几个部分组成:
      1
CB:表示CLR Header的大小,单位是byte
      2
Run time version:运行时版本,包含两部分MajorRuntimeVersionMinorRuntimeVersion
      3
Metadata Directory:指出Metadata tableRVA和其大小;
      4
Flag:这个标识主要是供加载器使用,flag值为0x00000001表示当前runtime image仅由IL代码组成并且对CPU没有特殊要求;值为0x00000002表示image只能被加载到32位机中,值为0x00010000表示运行时和jit编译器需要追踪方法的调试信息;
      5
EntryPointTokenMetadata 表中标记为EntryPoint的方法的MethodDef
      6
Resources DirectoryCLR的资源,也就是托管资源的RVA和大小,注意与PE文件中存储Win32资源的section不同;
      7
StrongNameSignature DirectoryPE文件中供CLR加载器使用的哈希值所处RVA和大小;
      8
CodeManagerTable DirectoryCode Manager 表的RVA和其大小;
      9
VTableFixups Directory:由非托管C++类型中虚方法的指针组成的数组;
      10
ExportAddressTableJumps Directory:跳转地址表的RVA和大小;
      11
ManagedNativeHeader Directory:一般情况下为0
      
以上结构可以从CorHdr.h文件中看出,如果装的是vs2005,这个文件在\Microsoft Visual Studio 8\SDK\v2.0\include\
      
查看托管PE文件的工具有很多,不用很复杂的,就园子里的大牛Anders Liu写的CliPeViwer就很好用,用Reflector可以偷窥其代码哦。

      
那么在上面这个结构中我最关心的是Metadata directoryEntryPointTokenMetadata 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

CLRTesing.Program类型的静态构造函数执行时,入口方法MainCLRTesing.Program的实例构造函数还没有被JitMain方法中引用到的CLRTesing.P类型也没有被加载,所以它的Method TableEEClass结构也没有建立起来。

 

     2、在Windbgdetach debuggee,随便敲一个字符让程序继续运行;接着,入口方法Main开始执行,


    因为Main方法第一次执行,所以通过StubJit编译器会被唤起,由于Main方法引用了CLRTesing.P类型,那么在执行前会将CLRTesing.P类型载入,并建立Method Table和其EEClass结构,当然这个建立过程也要去查找MetaData表,我认为这个过程是这样的:

     Main方法被调用,由于它没有被Jit过,CLR会通过Main方法的MethodDesc结构的StubJit编译器进行调用,CLR通过MetaData表的接口找到Main方法对应的Token,如下:

    我们可以看到Main方法的RVA0x00002050,于是去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_CodeOrILStub的值,让它们指向这个新的内存地址,当这个方法被再次调用的时候就会直接通过这个地址访问到本地CPU指令而不会触发第二次编译。对于这个过程大家的看法呢?Windbg查看各对象情况: 

Windbg2

 

我们可以发现Main方法已经被Jit,且它引用的CLRTesing.P类型的相关结构也已经建立起来了,而CLRTesing.P类型的Display方法所引用的CLRTesing.Q类型没有被载入。

总结一下,Jit编译针对的对象总是方法,不论是入口方法还是其他方法的Jit过程都类似上述过程,Metadata这这里的作用不言而喻,可以说没有Metadata的支持就无法进行Jit,我觉得MeatadataJit编译期间的作用至少有三个:

1Jit编译器通过查找Metadata来找到入口方法;

2Jit编译器通过查找Metadata来定位待编译方法并利用其RVA找到存储于PE文件中的IL代码在内存中的实际地址;

3Jit编译器在找到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 

posted on 2009-09-03 13:46  Leo Zhang  阅读(2589)  评论(7编辑  收藏  举报

导航