第一章 CLR的执行模型
本章内容:
- 将源代码编译成托管模块
- 将托管模块合并成程序集
- 加载公共语言运行时
- 执行程序集的代码
- Framework类库入门
- 通用类型系统
- 公共语言规范(CLS)
- 非托管代码的互操作性
1.1 将源代码编译成托管模块
1、公共语言运行时(CLR)是一个可由多种编程语言使用的“运行时”,其核心功能(比如内存管理、程序集加载、安全性、异常处理和线程同步)可由面向CLR的所有语言使用。
2、托管模块(managed module)是标准的32位Microsoft Windows可移植执行体(PE32)文件,或者是标准的64位Windows可移植执行体(PE32+)文件,它们都需要CLR才能执行。托管程序集总是利用Windows的数据执行保护(Data Execution Prevention,DEP)和地址空间布局随机化(Address Space Layout Randomization,ASLR),这两个功能旨在增强整个系统的安全性。
3、托管模块的各个部分:
- PE32或PE32+头,标准Windows PE文件头。
- CLR头,包含使这个模块成为托管模块的信息。包含要求的CLR版本,一些标志(flag),托管模块入口方法(Main方法)的MethodDef元数据token以及模块的元数据,资源、强名称、一些标志及其他不太重要的数据项的位置/大小。
- 元数据,每个托管模块都包含元数据表。主要有两种表:一种描述源代码中定义的类型和成员,另一种描述源代码应用的类型和成员。
- IL(中间语言)代码,编译器编译源代码时生成的代码。在运行时,CLR将IL编译成本机CPU指令。
4、本机代码编译器生成的是面向特定CPU架构的代码,而每个面向CLR的编译器生成的都是IL代码(托管代码managed code)。
1.2 将托管模块合并成程序集
1、程序集(assembly):首先,程序集是一个或多个模块/资源文件的逻辑性分组;其次,程序集是重用、安全性以及版本控制的最小单元。在CLR的世界中,程序集相当于“组件”。CLR是与程序集工作,而不是模块。
2、编译器默认将生成的托管模块转换成程序集,也就是说,C#编译器生成的是含有清单的托管模块。清单指出程序集只由一个文件构成。所以,对于只有一个托管模块而且无资源文件的项目,程序集就是托管模块,生成过程中无需执行任何额外的步骤。但是,如果希望将一组文件合并到程序集中,就必须用工具及其命令行选项。
1.3 加载公共语言运行时
1、生成的每个程序集既可以是可执行应用程序,也可以是DLL。最终是由CLR管理这些程序集中的代码的执行。
2、Windows检查EXE文件头,决定是创建32位还是64位进程之后,会在进程地址空间加载MSCorEE.dll的x86,x64或ARM版本。然后,进程的主线程调用MSCorEE.dll中定义的一个方法。这个方法初始化CLR,加载EXE程序集,在调用其入口方法(Main)。随即,托管应用程序启动并运行。
1.4 执行程序集的代码
1、下图展示了方法首次调用时发生的事情。
就在Main方法执行之前,CLR会检测出Main的代码引用的所有类型。这导致CLR分配一个内部数据结构来管理对引用类型的访问。如图中的Main方法引用了一个Console类型,导致CLR分配一个内部结构。在这个内部数据结构中,Console类型定义的每一个方法都有一个对应的记录项。每个记录项都含有一个地址,根据此地址即可找到方法的实现。对这个结构初始化时,CLR将每个记录项都设置成包含在CLR内部的一个未编档函数。我将该函数称为JITCompliler。
Main方法首次调用WriteLine时,JITCompiler函数会被调用。JITCompiler函数负责将方法的IL代码编译成本机CPU指令。由于IL是“即时”(just in time)编译的,所以通常将CLR的这个组件称为JITter或者JIT编译器。
JITCompiler函数被调用时,它知道要调用的是哪个方法,以及具体是什么类型定义了该方法。然后,JITComplier会在定义(该类型的)程序集的元数据中查找被调用方法的IL。接着,JITCompiler验证IL代码,并将IL代码编译成本机CPU指令。本机CPU指令保存到动态分配的内存块中。然后,JITCompiler回到CLR为类型创建的内部数据结构,找到与被调用方法对应的那条记录,修改最初对JITCompiler的引用,使其指向内存块(其中包含了刚才编译好的本机CPU指令)的地址。最后,JITCompiler函数跳转都内存块中的代码。这些代码正是WriteLine方法(获取单个String参数的那个版本)的具体实现。代码执行完毕并返回时,会回到Main中的代码,并像往常一样继续执行。
当Main要第二次调用WriteLine时,由于已对WriteLine的代码进行了验证和编译,所以会直接执行内存块中的代码,完全跳过JITCompiler函数。WriteLine方法执行完毕后,会再次回到Main。下图展示了第二次调用WriteLine时发生的事情。
2、IL基于栈,它的所有指令都要将操作数压入(push)一个执行栈,并从栈弹出(pop)结果。由于IL没有提供操作寄存器的指令,所以可以很容易地创建新的语言和编译器,生成面向CLR的代码。
IL指令是“无类型”(typeless)的。
CLR在将IL编译成本机CPU指令时,会执行一个名为验证(verification)的过程,这个过程会检查高级IL代码,确定代码所做的一切都是安全的。
1.5 Framework类库
.NET Framework包含Framework类库(Framework Class Library,FCL)。FCL是一组DLL程序集的统称,其中含有数千个类型定义,每个类型都公开了一些功能。
1.6 通用类型系统
1、通用类型系统(Common Type System,CTS):CLR一切都是围绕类型展开的,类型向应用程序和其他类型公开了功能,类型是CLR的根本。
2、CTS规范规定,一个类型可以包含零个或者多个成员:字段、方法、属性、事件。
3、CTS还指定了类型可见性规则及类型成员的访问规则:private、family、famliy and assembly、assembly、family or assembly、public。
4、除此之外,CTS还为类型继续、虚方法、对象生存期等定义了相应的规则。
5、另一条CTS规则:所有类型最终必须从预定义的System.Object类型继承。
1.7 公共语言规范
1、不同语言创建的对象可通过COM相互通信。CLR则集成了所有语言,用一种语言创建的对象在另一种语言中,和用后者创建的对象具有相同的地位。之所以能实现这样的集成,是因为CLR使用了标准类型集、元数据以及公共执行环境。
2、公共语言规范(Common Language Specification,CLS):它详细的定义了一个最小功能集。任何编译器只有支持这个功能集,生成的类型才能兼容由其他符合CLS、面向CLR的语言生成的组件。
在CLR中,类型的每个成员要么是字段(数据),要么是方法(行为)。这意味着每一种编程语言都必须能访问字段和调用方法。字段和方法以特殊或通用的方式使用。
1.8与非托管代码的互操作性
CLR支持三种互操作情形:①托管代码能调用DLL中的非托管函数;②托管代码可以使用现有COM组件(服务器);③非托管代码可以使用托管类型(服务器)。