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

    CLR 是如何找到托管代码的入口方法并对其 Jit 的呢? Jit 的发生过程是怎么样的呢? Jit 编译器和 Metadata 表又有什么关系呢?本文试图寻找出答案,在此之前,不妨先了解一下 CLR Header 的大致结构。
    
以如下代码为例:
ContractedBlock.gif ExpandedBlockStart.gif 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:运行时版本,包含两部分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 查看,如下:
ContractedBlock.gif ExpandedBlockStart.gif 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: <not loaded yet>
EEClass: <not loaded yet>
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 MethodDesc      JIT 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   00a73040      JIT CLRTesing.Program..cctor()

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

ContractedBlock.gif ExpandedBlockStart.gif 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: <not loaded yet>
EEClass: <not loaded yet>
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 MethodDesc      JIT 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   00a73030      JIT CLRTesing.Program.Main(System.String[])
00a7c015   00a73038     NONE CLRTesing.Program..ctor()
00da0070   00a73040      JIT 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 MethodDesc      JIT 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,我觉得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 

转载于:https://www.cnblogs.com/vivounicorn/archive/2009/09/03/1559488.html

猜你喜欢

转载自blog.csdn.net/weixin_34411563/article/details/93642152
今日推荐