详解CLR中Jit编译发生的过程

开发 后端
这里将介绍CLR中Jit编译发生的过程,通过笔者深入的探讨,希望能对大家了解Jit的发生过程是怎么样的有所了解。
理解在CLR中Jit编译发生的过程,了解这个首先要从CLR谈起。CLR(公共语言运行时,Common Language Runtime)和Java虚拟机一样也是一个运行时环境,它负责资源管理(内存分配和垃圾收集),并保证应用和底层操作系统之间必要的分离。

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

以如下代码为例:

  1. Example  
  2. using System;  
  3.  
  4. namespace CLRTesing  
  5. {  
  6. class Program  
  7. {  
  8. static void Main(string[] args)  
  9. {  
  10. System.Console.WriteLine("Hello World!");  
  11. Console.ReadKey();  
  12.  
  13. new P().Display();  
  14.  
  15.  
  16. }  
  17.  
  18. Program()  
  19. {  
  20. Console.WriteLine("Constructor.");  
  21. Console.ReadKey();  
  22. }  
  23.  
  24. static Program()  
  25. {  
  26. Console.WriteLine("Static constructor.");  
  27. Console.ReadKey();  
  28. }  
  29. }  
  30.  
  31. class P  
  32. {  
  33. public void Display()  
  34. {  
  35. System.Console.WriteLine("P!");  
  36. Console.ReadKey();  
  37.  
  38. new Q().Display();  
  39. Console.ReadKey();  
  40. }  
  41. }  
  42.  
  43. class Q  
  44. {  
  45. public void Display()  
  46. {  
  47. System.Console.WriteLine("Q!");  
  48. Console.ReadKey();  
  49. }  
  50. }  

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

dumpbin工具

从图中可以看到,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编译器的调用。

执行上述代码,

[[5737]]

用Windbg 查看,如下:

  1. Windbg1  
  2. 0:000> !name2ee *!CLRTesing.Program  
  3. Module: 790c2000 (mscorlib.dll)  
  4. --------------------------------------  
  5. Module: 00a72c3c (Hello.exe)  
  6. Token: 0x02000002  
  7. MethodTable: 00a73048  
  8. EEClass: 00a7129c  
  9. Name: CLRTesing.Program  
  10.  
  11. 0:000> !name2ee *!CLRTesing.P  
  12. Module: 790c2000 (mscorlib.dll)  
  13. --------------------------------------  
  14. Module: 00a72c3c (Hello.exe)  
  15. Token: 0x02000003  
  16. MethodTable:   
  17. EEClass:   
  18. Name: CLRTesing.P  
  19.  
  20. 0:000> !dumpmt -md 00a73048  
  21. EEClass: 00a7129c  
  22. Module: 00a72c3c  
  23. Name: CLRTesing.Program  
  24. mdToken: 02000002(D:\test\Hello\bin\Debug\Hello.exe)  
  25. BaseSize: 0xc  
  26. ComponentSize: 0x0  
  27. Number of IFaces in IFaceMap: 0  
  28. Slots in VTable: 7  
  29. --------------------------------------  
  30. MethodDesc Table  
  31.  Entry MethodDescJIT Name  
  32. 79371278 7914b928 PreJIT System.Object.ToString()  
  33. 7936b3b0 7914b930 PreJIT System.Object.Equals(System.Object)  
  34. 7936b3d0 7914b948 PreJIT System.Object.GetHashCode()  
  35. 793624d0 7914b950 PreJIT System.Object.Finalize()  
  36. 00a7c011 00a73030 NONE CLRTesing.Program.Main(System.String[])  
  37. 00a7c015 00a73038 NONE CLRTesing.Program..ctor()  
  38. 00da0070 00a73040JIT CLRTesing.Program..cctor() 

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

#p#

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

入口方法Main开始执行

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

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

Jit编译

我们可以看到Main方法的RVA是0x00002050,于是去PE文件的.Text section中的Raw Data中查找image base+RVA这个位置处的IL代码,接着Jit编译器会对这段IL代码进行验证,验证过程通过调用CheckIL方法来实现,这个方法的签名可以是这样的:

  1. CHECK CheckIL(RVA il);  
  2. CHECK CheckIL(RVA il, COUNT_T size); 

验证结束后把这段IL代码编译成本地CPU指令,将编译后后的CPU指令存到内存并修改Main方法的MethodDesc结构中m_CodeOrIL和Stub的值,让它们指向这个新的内存地址,当这个方法被再次调用的时候就会直接通过这个地址访问到本地CPU指令而不会触发第二次编译。对于这个过程大家的看法呢?用Windbg查看各对象情况:

  1. Windbg2  
  2. 0:000> !name2ee *!CLRTesing.Program  
  3. Module: 790c2000 (mscorlib.dll)  
  4. --------------------------------------  
  5. Module: 00a72c3c (Hello.exe)  
  6. Token: 0x02000002  
  7. MethodTable: 00a73048  
  8. EEClass: 00a7129c  
  9. Name: CLRTesing.Program  
  10.  
  11. 0:000> !name2ee *!CLRTesing.P  
  12. Module: 790c2000 (mscorlib.dll)  
  13. --------------------------------------  
  14. Module: 00a72c3c (Hello.exe)  
  15. Token: 0x02000003  
  16. MethodTable: 00a730b8  
  17. EEClass: 00a71730  
  18. Name: CLRTesing.P  
  19.  
  20. 0:000> !name2ee *!CLRTesing.Q  
  21. Module: 790c2000 (mscorlib.dll)  
  22. --------------------------------------  
  23. Module: 00a72c3c (Hello.exe)  
  24. Token: 0x02000004  
  25. MethodTable:   
  26. EEClass:   
  27. Name: CLRTesing.Q  
  28.  
  29. 0:000> !dumpmt -md 00a73048  
  30. EEClass: 00a7129c  
  31. Module: 00a72c3c  
  32. Name: CLRTesing.Program  
  33. mdToken: 02000002(D:\test\Hello\bin\Debug\Hello.exe)  
  34. BaseSize: 0xc  
  35. ComponentSize: 0x0  
  36. Number of IFaces in IFaceMap: 0  
  37. Slots in VTable: 7  
  38. --------------------------------------  
  39. MethodDesc Table  
  40.  Entry MethodDescJIT Name  
  41. 79371278 7914b928 PreJIT System.Object.ToString()  
  42. 7936b3b0 7914b930 PreJIT System.Object.Equals(System.Object)  
  43. 7936b3d0 7914b948 PreJIT System.Object.GetHashCode()  
  44. 793624d0 7914b950 PreJIT System.Object.Finalize()  
  45. 00da00b0 00a73030JIT CLRTesing.Program.Main(System.String[])  
  46. 00a7c015 00a73038 NONE CLRTesing.Program..ctor()  
  47. 00da0070 00a73040JIT CLRTesing.Program..cctor()  
  48.  
  49. 0:000> !dumpmt -md 00a730b8  
  50. EEClass: 00a71730  
  51. Module: 00a72c3c  
  52. Name: CLRTesing.P  
  53. mdToken: 02000003(D:\test\Hello\bin\Debug\Hello.exe)  
  54. BaseSize: 0xc  
  55. ComponentSize: 0x0  
  56. Number of IFaces in IFaceMap: 0  
  57. Slots in VTable: 6  
  58. --------------------------------------  
  59. MethodDesc Table  
  60.  Entry MethodDescJIT Name  
  61. 79371278 7914b928 PreJIT System.Object.ToString()  
  62. 7936b3b0 7914b930 PreJIT System.Object.Equals(System.Object)  
  63. 7936b3d0 7914b948 PreJIT System.Object.GetHashCode()  
  64. 793624d0 7914b950 PreJIT System.Object.Finalize()  
  65. 00a7c04c 00a730a8 NONE CLRTesing.P.Display()  
  66. 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编译发生的过程

【编辑推荐】

  1. C#treeview递归操作数据库浅析
  2. C#递归树实现实例简析
  3. C#打开记事本实现实例解析
  4. C#调用记事本实例浅析
  5. C#日期格式化方法简析
责任编辑:彭凡 来源: 博客园
相关推荐

2009-10-23 09:36:25

.Net Compac

2009-08-24 11:36:27

CLR加载过程

2023-10-31 11:46:32

编译器托管CLR

2022-05-18 07:58:21

Linux程序编译代码

2020-11-09 14:41:58

iOS 14.2苹果JIT

2024-11-27 16:25:54

JVMJIT编译机制

2009-09-18 10:40:05

CLR存储过程

2009-09-17 19:19:17

CLR存储过程

2009-09-18 14:09:57

SQL CLR存储过程

2009-10-22 18:06:31

CLR存储过程

2009-10-22 13:02:47

SQL CLR存储过程

2009-10-22 14:05:55

CLR存储过程

2011-08-17 17:29:32

Windows编译MySQL

2009-09-18 10:55:17

CLR存储过程

2009-10-19 14:25:16

静态构造函数

2009-03-11 10:29:23

代码契约.NETCLR

2011-05-03 10:31:59

喷墨打印机注墨误区

2009-10-22 15:09:40

CLR存储过程

2022-04-10 10:57:06

eBPFJIT即时编译

2009-05-15 09:33:52

开发线程冲突lock
点赞
收藏

51CTO技术栈公众号