首页 > 代码库 > CLR的执行模型

CLR的执行模型

文章导读

1.将源代码编译为托管模块

2.将托管模块合并为程序集

3.加载CLR

4.执行程序集代码

什么是CLR

简单的翻译过来:公共语言运行时。

这家伙跟使用那种编程语言无关,只要你的编译器是面向CLR的就行,他跟随.net framework 一起安装。

当然了,只有托管模块的运行才需要CLR,非托管代码生成的模块,那就另当别论了!

目前,微软已经编写出了多种面向CLR的语言编译器,比如:微软C/C++,C#,VB,F#,以及IL。

好了,知道了什么是CLR,接下来就开始探索程序背后的秘密吧!

出于个人原因,本系列文章中的代码语言,以及开发工具均为C#,VisualStudio 2010

将源代码编译为托管模块

我们用自己擅长的语言完成代码的编写之后,编译器首先要把源代码编译成一个托管模块。

无论使用哪种编译器,最终的结果都是一个托管模块

那么什么是托管模块呢?

托管模块:简单来说就是一个标准的windows PE文件,32位程序就是PE32,64位程序就是PE32+。

托管模块包含的内容:

  PE32或PE32+头,这个头标识了文件是GUI,CUI还是DLL,以及和本地CPU代码有关的信息;

  CLR头,这个头里面包含了使一个模块成为一个托管模块所需的信息,他包含CLR版本,Main方法的MethodDef元数据标记等信息;

  元数据,元数据就是一组数据表的集合,主要包含两种表:描述模块中定义的类型和成员,描述模块引用的类型和成员;

  IL代码(注意:IL代码存在于托管模块中),编译器将IL代码编译成本地的CPU指令;

注意:本地代码编译器生成的是面向特定CPU架构的代码;面向CLR的编译器生成的都是IL代码。

通常情况下,编译器生成的元数据总是嵌入到和IL代码相同的EXE/DLL文件中,这样就保证了这两种数据总是保持同步。

说到这里,我们顺便再说一下[元数据]的作用:

  首先,有了元数据,编译器可以直接从托管模块中读取元数据,消除了对本地C/C++头和库文件的需求;

  然后,也是我们最常用的一个功能:VisualStudio的智能感知,通过解析元数据,可以指出一个类型提供了哪些的字段,属性,方法,如果是方法的话还能指出方法所需的参数类型;

  还有,CLR的代码验证也是通过元数据来确保你的代码只能执行类型安全的操作;

  再者,有了元数据,类型中的字段也可以被序列化到内存块儿中,通过网络发送到另外一台机器,反序列化之后重建类型对象;

  最后,元数据还允许垃圾回收器跟踪对象的生命周期,以确定何时进行垃圾回收;

题外话:在所有的面向CLR的编译器中,只有C/C++编译器最特殊,因为他既能编译托管代码,也能编译非托管代码;

将托管模块合并为程序集

前面说了那么多托管模块,其实,CLR并不直接和托管模块关联,CLR直接面向的是程序集。

程序集是一个抽象的感念,简单来说,他就是对模块和资源文件进行的逻辑分组;同时,程序集也是程序重用,安全和版本控制的最小单元;在CLR的世界里,程序集就是一个组件。

编译器先是把源代码编译生成托管模块,然后再把托管模块合并为一个程序集文件,这个程序集文件其实也是一个标准的PE文件(没错,我们前面说过,托管模块也是一个PE文件,这是正确的!!!),与托管模块类似,程序集文件也包含一组数据块儿,称作[清单],这个清单就是另外一组简单的元数据表,这些表记录了组成这个程序集的文件信息(托管模块)以及这些文件中定义的公共类型。然后,这些清单文件还记录了与这个程序集相关联的资源或数据文件信息。

注意了:托管模块是由编译器编译而来的,程序集是由托管模块文件以及资源文件等[合并]而来的;

加载CLR

每个程序集都被生成为一个可执行应用程序,或是DLL文件(包含一组被可执行应用程序使用的类型文件),CLR的任务就是管理这些程序集中的代码执行,所以,在执行这些文件之前,我们必须确保.net framework已经在我们的机器上正确安装。

在继续探索之前,我们先了解一下32位系统和64位系统。如果程序集中只包含类型安全的托管代码,那么,这些代码文件将能够在任何安装了.net framework的操作系统中运行。

但是,有时候,我们写的代码只希望在特定的windows版本中运行,为了实现这个目标,C#编译器提供了一个/platform命令行开关(在属性,生成,目标平台中),通过这个开关,我们就能指定我们的程序只能在特定的平台(CPU架构)中运行;

windows在执行一个可执行文件时,会先查看文件的头信息,以确定程序是要32位还是64位的地址空间,同时windows还会检查头文件中的CPU架构信息,以确保计算机的CPU架构与程序的CPU类型匹配。

在64位的windows操作系统中,还提供了一种称作“WoW64”(windows on windows64)的技术,这种技术通过模拟x86指令集,使那些包含了x86本地代码的32位应用程序运行在Itanium机器上,但是这样做的的性能损失也是比较大的!

在检查完程序集的头信息之后,windows会创建用于该程序的进程,windows加载MSCorEE.dll程序集到进程的地址空间中,然后进程的主线程会调用定义在MSCorEE.dll中的方法,该方法初始化(如果CLR为初始化)CLR,加载exe程序集,接着调用其入口函数:Main,此时,托管应用程序开始执行!

总结来说,一个应用程序的执行过程可以分为以下几个步骤:

  1.windows检测EXE文件头,是32?64?WoW64?;

  2.创建对应版本的windows进程;

  3.在(2)中创建的进程里面加载对应版本的MSCoreEE.dll;

  4.由(2)创建的进程主线程调用MSCoreEE.dll中定义的方法,暂时称为MethodA(注意:真实dll中并不存在此方法名);

  5.MethodA初始化CLR,加载EXE程序集;

  6.调用Main()方法;

注意:使用x86开关编译的dll无法在非64位windows系统的64位进程中运行,但是可以在64位windows系统中的64位进程中作为WoW64应用程序运行;

执行程序集代码

为了执行一个方法,必须把该方法的IL代码转换成本地CPU指令,而这一功能是由CLRJIT编译器(记着这个NB的[编译器])提供的;

在执行Main方法之前,CLR会检测Main方法中的所有引用类型,并为这些类型分配一个内部数据结构,该数据结构用于管理对所有引用类型的访问;

举例来说,如果Main方法中调用了Console类的WriteLine(string message)方法,那么WriteLine方法是这样执行的:

  1.CLR分配[内部数据结构],暂且称其为ObjectA吧,Console类型中的每个方法在ObjectA中都有一个对应的记录项,每个记录项都指向一个[CLR内部未文档化的函数:  JITCompiler],这个函数就是前面说的NB的[编译器]

  2.WriteLine方法第一次调用时,JITCompiler函数会被调用,JITCompiler函数在程序集的元数据中查找WriteLine方法的IL代码;

  3.JITCompiler函数验证IL代码,将IL代码编译为本地CPU指令,并且将这些CPU指令保存到一个动态分配的内存块中;

  4.JITCompiler函数返回ObjectA,找到WriteLine方法对应的记录项,修改其最初对JITCompiler函数的指向,让它现在指向(3)中的内存块的地址;

  5.JITCompiler函数跳转到内存块中的代码,执行这些代码;

  6.代码执行完成,JITCompiler函数重新返回到Main方法中,继续执行下面的代码;

在同一个应用程序进程中,一个方法只有在首次执行的时候才会调用JITCompiler函数,JITCompiler函数将本地CPU代码保存到内存块中,以后对该方法的重复调用都会使用同一份本地CPU代码,但是,一旦应用程序终止,编译好的CPU代码也会被丢弃!

《未完待续......》

CLR的执行模型