第一章:CLR的执行模型
CLR
CLR(Common Language Runtime),即公共语言运行时是一个由多种编程语言使用的“运行时”,它的核心功能(比如内存管理、程序集加载、异常处理和线程同步)可由
面向CLR的所有语言使用。任何一种实现了“运行时”的语言编译器,编程人员就可以使用该语言来开发代码。
面向“运行时”的语言编译器包括:C#,VB、IL(Intermediate Language,中间语言)汇编器等。当编程人员用一种CLR的语言来创建源代码文件时,由对应的编译器检查语法
和分析源代码,然后生成一个托管模块(managed module)。托管模块是一个标准的32位可移植执行体PE32文件(Portable Executable),或者是一个标准的64位windows可移
植执行体(PE32+)文件,然后由(只能由)CLR来执行。一句话,先由面向CLR的编译器将源代码编译成托管模块(托管模块的重要组成部分有CLR头,元数据,IL代码。详解见
下),再由CLR执行将各托管模块合并成程序集。
CLR头:包含了需要的CLR版本,一些标志,托管模块入口方法(Main方法)的MethodDef元数据标记(token),以及模块的元数据,资源,强名称等;
元数据(一组数据表):每个托管模块都包括两种类型的元数据表,一种类型的表描述源代码中定义的类型和成员;另一种类型的表描述源代码引用的类型和成员;
IL代码:编译器编译源代码时生成的代码。在运行时,CLR将IL编译成本地CPU指令;
本地代码编译器(native code compiler)生成的是面向特定CPU架构(比如x86,x64或IA64)的代码。相反,每个面向CLR的编译器生成的都是IL代码。IL代码有时称为托管
代码,因为CLR要管理它的执行。
前面提到,元数据其实是一组数据表,元数据允许将一个对象的字段序列化到一个内存块中,将其发送给另一台机器,然后反序列化,在远程机器上重建对象的状态。元数据也
允许垃圾回收器GC跟踪对象的生存周期。
程序集
程序集是重用、安全性及版本控制的最小单元。在CLR的世界中,程序集相当于一个组件,那么程序集到底是什么呢?程序集是一个或多个模块、资源文件的逻辑性分组,在程
序集中,由清单描述若干个托管模块和资源文件。我们知道,编译器编译源代码后生成了托管模块,然后CLR将不同的托管模块(这里,还包括资源文件)合并成程序集,然后由
CLR中的JIT(just-in-time)即时编译器执行。下面简要说下在VS下执行一个控制台应用程序时编译器和CLR所做的一些事情,比如以下代码:
public static void Main()
{
Console.WriteLine("Hello,World");
Console.WriteLine("Goodbye");
}
当运行在32位系统下的C#编译器编译完这段源代码后,生成一个程序集(默认情况下编译器会把生成的托管模块转换成程序集),然后由CLR来运行它,运行(在windows
32下)之前,windows检查EXE文件头,决定是创建32位、64位还是WoW64进程,这里是创建32位进程,之后在进程的地址空间中加载mscoree.dll的x86版本,mscoree.dll
的x86版本在C:\Windows\System32目录中。然后,进程的主线程调用mscoree.dll中定义的一个方法。这个方法初始化CLR,加载EXE程序集,然后调用其入口方法(Main)。随
即,托管的应用程序将启动运行。
启动时,是有CLR的JIT编译器执行程序集(IL和元数据—托管模块、资源文件等的合成)。在Main方法执行之前,CLR会检测出Main的代码引用的所有类型,这就会导致CLR
分配一个内部数据结构,用于管理对所引用的类型的访问。在上面这段代码中,Console类型定义的每个方法都有一个对应的记录项(entry)。每个记录项都容纳了一个地址,根据
这个地址可以找到方法的实现。在对这个结构时,CLR会将每个记录项都设置成(指向)包含在CLR内部的JITComplier函数。Main方法首次调用WriteLine时,JITCompiler函数
会被调用,然后JITCompiler函数负责将writeline方法的IL代码编译成本地CPU指令。在将IL编译成本地CPU指令的过程中,JIT编译器会验证IL代码,比如验证这个方法是否有正确
数量的参数,是否有返回值等。编译成的本地CPU指令被保存到一个动态分配的内存块中。然后,JITCompiler返回为CLR为类型创建的内部数据结构,找到与被调用的方法对应的
那一条记录,修改最初对JITCompiler的引用,让它现在指向内存块(其中包含了刚才编译好的本地CPU指令)的地址。最后,JITCompiler函数跳转到内存块中的代码。这些代码
就是第一条语句的具体实现,执行完毕后,会返回到Main中的代码,继续执行下一条语句。现在,Main要第二次调用WriteLine,这一次,由于已对writeline的代码进行了验证和编
译,所以会直接执行内存块中的代码,完全跳过JITCompiler函数,执行完毕后,再次返回Main。由上面的执行过程我们知道,一个方法只有在首次调用时才会造成一些性能损失
(验证,开辟内存),以后对该方法的所有调用都以本地代码的形式全速运行,无需重新验证IL并把它编译成本地代码。综上,JITCompiler函数功能如下:
(1)在负责实现类型(Console)的程序集的元数据中查找被调用的方法WriteLine;
(2)从元数据中获取该方法的IL;
(3)分配内存块;
(4)将IL编译成本地CPU指令,然后将这些本地代码存储到步骤(3)分配的内存中;
(5)在类型Type表中修改与方法对应的记录项,使它指向步骤3分配的内存块
(6)跳转到内存块中的本地代码