下图展示了编译源代码文件的过程。如图所示,可用支持 CLR 的任何一种语言创建源代码文件。然后,用一个对应的编译器检查语法和分析源代码。无论选用哪一个编译器,结果都是一个托管模块(managedmodule)。托管模块是一个标准的 32 位 Microsoft Windows 可移植执行体(PE32)文件 6 ,或者是一个标准的 64 位Windows 可移植执行体(PE32+)文件,它们都需要 CLR 才能执行。顺便说一句,托管的程序集总是利用了 Windows 的数据执行保护(Data Execution Prevention,DEP)和地址空间布局随机化(Address SpaceLayout Randomization,ASLR);这两个功能旨在增强整个系统的安全性。
托管模块的组成部分
PE32 或 PE32+头:标准 Windows PE 文件头,类似于“公共对象文件格式(Common Object File Format,COFF)”头。如果这个头使用 PE32 格式, 文件能在Windows的 32 位或 64 位版本上运行。如果这个头使用 PE32+格式,文件只能在 Windows 的 64 位版本上运行。这个头还标识了文件类型,包括 GUI,CUI 或者 DLL,并包含一个时间标记来指出文件的生成时间。对于只包含 IL 代码的模块,PE32(+)头的大多数信息会被忽视。对于包含本地 CPU代码的模块,这个头包含了与本地 CPU 代码有关的信息
CLR 头:包含使这个模块成为一个托管模块的信息(可由 CLR 和一些实用程序进行解释)。头中包含了需要的 CLR 版本,一些 flag,托管模块入口方法(Main 方法)的 MethodDef 元数据 token,以及模块的元数据、资源、强名称、一些 flag 以及其他不太重要的数据项的位置/大小
元数据:每个托管模块都包含元数据表。主要有两种类型的表:一种类型的表描述源代码中定义的类型和成员,另一种类型的表描述源代码引用的类型和成员
IL(中间语言)代码:编译器编译源代码时生成的代码。在运行时,CLR 将 IL 编译成本地 CPU指令。
本地代码编译器(native code compilers)生成的是面向特定 CPU 架构(比如 x86,x64 或 IA64)的代码。相反,每个面向 CLR 的编译器生成的都是 IL(中间语言)代码。IL 代码有时称为托管代码,因为 CLR 要管理它的执行。
除了生成 IL,面向 CLR 的每个编译器还要在每个托管模块中生成完整的元数据。简单地说,元数据(metadata)是一组数据表。其中一些数据表描述了模块中定义的内容,比如类型及其成员。还有一些元数据表描述了托管模块引用的内容,比如导入的类型及其成员。元数据是一些老技术的超集。这些老技术包括 COM 的“类型库(Type Library)”和“接口定义语言(Interface Definition Language,IDL)”文件。要注意的是,CLR 元数据远比它们完整。另外,和类型库及 IDL 不同,元数据总是与包含 IL 代码的文件关联。事实上,元数据总是嵌入和代码相同的 EXE/DLL 文件中,这使两者密不可分。由于编译器同时生成元数据和代码,把它们绑定一起,并嵌入最终生成的托管模块,所以元数据和它描述的 IL 代码永远不会失去同步。元数据有多种用途,下面仅列举一部分。
* 编译时,元数据消除了对本地 C/C++头和库文件的需求,因为在负责实现类型/成员的 IL 代码文件中,已包含和引用的类型/成员有关的全部信息。编译器可直接从托管模块读取元数据。
* Microsoft Visual Studio 使用元数据帮助你写代码。它的“智能感知(IntelliSense)”技术可以解析元数据,指出一个类型提供了哪些方法、属性、事件和字段。如果是一个方法,还能指出方法需要什么参数。
* CLR 的代码验证过程使用元数据确保代码只执行“类型安全”的操作。(稍后就会讲到验证。)。
* 元数据允许将一个对象的字段序列化到一个内存块中,将其发送给另一台机器,然后反序列化,在远程机器上重建对象的状态。
* 元数据允许垃圾回收器跟踪对象的生存期。垃圾回收器能判断任何对象的类型,并从元数据知道那个对象中的哪些字段引用了其他对象
将托管模块合并成程序集
CLR 实际不和模块一起工作。相反,它是和程序集一起工作的。程序集(assembly)是一个抽象的概念,初学者往往很难把握它的精髓。首先,程序集是一个或多个模块/资源文件的逻辑性分组。其次,程序集是重用、安全性以及版本控制的最小单元。取决于你对于编译器或工具的选择,既可以生成单文件程序集,
也可以生成多文件程序集。在 CLR 的世界中,程序集相当于一个“组件”。