《CLR via C#》 CLR基础

CLR的执行模型

公共语言运行时(Common Language Runtime, CLR)是一个可由多种编程语言使用的“运行时”。CLR的核心功能(比如内存管理、程序集加载、安全性、异常处理和线程同步)可由面向CLR的所有语言使用。

不同编程语言的意义?可将编译器视为语法检查器和“正确代码”分析器。它们检查代码,确定写的一切都有意义,并输出对其意图进行描述的代码。但是无论任何用哪个编译器,最终编译的结果都是托管模块(managed module)。托管模块是标准的32位Microsoft Windows可移植执行(Portable Executable, PE32)或64位(PE32+)文件。

高级语言(例如C#、F#等)通常只公开的CLR全部功能的一个子集。然而,IL汇编语言允许开发人员访问CLR的全部功能。

托管模块的组成部分

  • PE32或PE32+头部:标准Windows PE文件头,类似于“公共对象文件格式”(Common Oject File Format, COFF)。
  • CLR头部:包含使这个模块成为托管模块的信息(可由CLR和一些实用程序进行解释)。包含要求的CLR版本,一些标志(flag),托管模块入口方法(Main方法)的MethodDef元数据token以及模块的元数据、资源、强名称、一些标志及其他不太重要的数据项的位置/大小。
  • 元数据:每个托管模块都包含元数据表。主要由两种表:一种描述源代码中定义的类型和成员,另一种描述源代码引用的类型和成员。
    • 用途:
      • 避免编译时对原生C/C++头和库文件的需求,因为在实现类型或成员的IL代码文件中,已包含有关引用类型或成员的全部信息。编译器直接从托管模块读取元数据。
      • “智能感知”(IntelliSense)技术会解析元数据,告诉你一个类型提供了哪些方法、属性、事件和字段。对于方法,还能告诉其参数。
      • CLR的代码验证过程使用元数据确保代码只执行“类型安全”的操作。
      • 允许将对象的字段序列化和反序列化。
      • 允许垃圾回收器跟踪对象生存期。
  • IL(中间语言)代码:编译器编译源代码时生成的代码。在运行时,CLR用本机代码编译器(native code compolers)将IL编译成面向本机特定CPU架构的指令代码。
    • IL指令是一种基于栈的指令集(跟Lua一样)。操作压入栈,并从让结果从栈弹出。
    • IL指令还是无类型(typeless)的。

将托管模块组合成程序集

程序集(assembly) 是一个或多个模块或资源文件的逻辑性分组。程序集是重用、安全性以及版本控制的最小单元。CLR实际与程序集一起工作,程序集相当于它的“组件”。

清单(manifest) 是元数据表的集合。这些表描述构成程序集的文件、程序集中的文件所实现的public类型以及与程序集关联的资源或数据文件。

程序集的自描述(self-describing) 利用程序集模块中包含的引用程序集有关的信息(版本号),CLR能判断为了执行程序集中的代码,程序集的直接依赖对象(immediate dependency)。

托管程序集总是利用Windows的数据执行保护(Data Execution Prevention, DEP)和地址空间布局随机化(Address Space Layout Randomization, ASLR)来增强系统安全性。

如果只有一个托管模块而且无资源(或数据)生成的程序集就是托管模块本身,生成无需额步骤。但如果希望将一组文件合并到程序集中,就必须用AL(程序集连接器)等工具生成。

程序集的执行

分析以下程序的执行

1static void Main() {
2    Console.WriteLine("Hello World!");
3    Console.WriteLine("Goodbye!");
4}
  1. 在Main方法执行之前,CLR会检测出Main方法的代码引用的所有类型(本例为Console)。并分配一个内部数据结构来管理对引用类型的访问。内部数据结构中对类型定义的每个方法都有一个对应的记录项。每个记录项都含有一个地址,根据此地址即可找到方法的实现。对这个结构初始化时,CLR将每个记录项的指针都指向包含在CLR内部的一个未编档函数(本书定义为JITCompiler)。
  2. 第一次执行Console.WriteLine方法时,JITCompiler函数会被调用。它负责将方法的IL代码编译成本机CPU指令。由于IL是“即时”(just in time)编译的,所有通常将CLR的这个组件叫做JIT编译器。JITCompiler函数的执行方式如下:
    1. 在负责实现类型(Console)的程序集的元数据中查找被调用的方法(WriteLine)。
    2. 从元数据中获取该方法的IL代码。并验证
    3. 动态分配内存块。
    4. 将IL代码编译成本机CPU指令然后将本机代码存储到步骤3分配的内存中。
    5. 在Type表(Main函数指向Console.WriteLine的记录项)中修改与方法对应的条目,使它指向步骤3分配的内存块。
    6. 跳转到内存块中的本机代码,并执行。
    7. 执行完后返回到Main方法中继续执行。
  3. 当第二次执行Console.WriteLine方法时,由于已经对其方法的代码进行了验证和编译,所以会直接执行内存块中的代码,完全跳过JITCompiler函数。

提示

托管代码的缺点:

  • 本机CPU指令都存储在动态内存中,一旦应用程序终止,编译好的代码也会被丢弃。
  • 将本机CPU指令都存储在动态内存中会增加内存消耗。
  • 首次调用函数时会验证代码和编译代码所以有性能损失。

提示

托管代码的优点:

  • JIT编译器能够判断是否运行在特定CPU中,从而进行特殊指令优化。而非托管代码通常只能针对最小功能及和的CPU来编译。
  • JIT编译器能判断一个特定的if语句测试在它运行的机器上是否总是失败。从而不会为其生成任何CPU指令。使其代码边得更小。(写代码时应该避免写出这样的代码)
  • CLR可评估代码的运行,并将IL重新编译成本机代码。重新编译的代码可以重新组织,根据观察到的执行模式,减少不正确的分支预测(作者意淫的)

元数据

元数据是由几个表构成的二进制数据块。有三种表:定义表(definition table)、引用表(reference table)和清单表(manifest table)

定义表

模块自己定义的数据

元数据定义表名称 说明
ModuleDef 包含对模块进行标识的一个记录项。每个记录项包含模块的文件名和扩展名,以及模块版本ID
TypeDef 模块定义的每个类型在这个表都有一个记录项。每个记录项包含类型的名称、基类型、一些标志(public,private等)以及一些索引 ,这些索引指向MethodDef表中该类型的方法、FieldDef表中该类型的字段、PropertyDef表中该类型的属性以及EventDef表中该类型的事件
MethodDef 模块定义的每个方法在这个表中都有一个记录项 。每个记录项都包含方法的名称、一些标志(private, public, virtual, abstract, static, final等)、签名以及方法的IL代码在模块中的偏移量。并还引用了ParamDef表中的一个记录项为方法对应的参数。
FieldDef 模块定义的每个字段在这个表都有一个记录项 。每个记录项都包含标志(private, public)、类型和名称。
ParamDef 模块定义的每个参数在这个表都有一个记录项 。每个记录项都包含标志(in, out, retval等)、类型和名称。
PropertyDef 模块定义的每个属性在这个表都有一个记录项 。每个记录项都包含标志、类型和名称。
EventDef 模块定义的每个事件在这个表都有一个记录项 。每个记录项包含标志和名称。

引用表

模块引用的数据

元数据引用表名称 说明
AssemblyRef 模块引用的每个程序集在这个表中都有一个记录项。每个记录项都包含绑定该程序集所需的信息:名称(不含路径和扩展名)、版本号、语言文化以及公钥token。还有一些标志和一个哈希值(校验用,已弃用)
ModuleRef 实现该模块所引用的类型的每个PE模块在这个表中都有一个记录项。每个记录项包含模块的文件名和扩展名。别的模块实现了你需要的类型,这个表就是建立同那些类型的绑定关系
TypeRef 模块引用的每个类型在这个表中都有一个记录项。每个记录项包含类型的名称和一个引用(指向类型的位置)。如果类型在另一个类型中实现,则引用指向一个TypeRef记录项。在同一个模块中实现,引用指向一个ModeuleDef记录项。在调用程序集内的另一个模块中实现,引用指向一个ModuleRef记录项。在不同的程序集中实现,指向一个AssemblyRef记录项。
MemberRef 模块引用的每个成员(字段和方法,以及属性方法和事件方法)在这个表中都有一个记录项。每个记录项包含成员的名称和签名,并指向对成员进行定义的那个类型的TypeRef记录项

清单表

清单表中主要包含程序集组成部分的那些文件的名称。此外,还描述了程序集的版本、语言文化、发布者、公开导出的类型以及构成程序集的所有文件。

元数据清单表名称 说明
AssemblyDef 如果模块标识的是程序集,这个元数据就包含单一的记录项列出程序集的名称(不含路径和扩展名)、版本、语言文化、一些标志、哈希算法以及发布者公钥(可null)
FileDef 作为程序集一部分的每个PE文件和资源文件在这个表中都有一个记录项(清单本身所在的文件除外,该文件在AssemblyDef中列出)。如果只包含自己的文件,则无记录
ManifestResourceDef 作为程序集一部分的每个资源在这个表中都有一个记录项
ExportedTypesDef 从程序集的所有PE模块中导出的每个public类型在这个表中都有一个记录项。