第1章 CLR的执行模型
第1章 CLR的执行模型
NyxX第一章 CLR的执行模型
本章内容:
- 将源代码编译成托管模块
- 将托管模块合并成程序集
- 加载公共语言运行时
- 执行程序集的代码
- 本机代码生成器:NGen.exe
- Framework类库入门
- 通用类型系统
- 公共语言规范(CLS)
- 与非托管代码的互操作性
1.1 将源代码编译成托管模块
公共语言运行时(CLR)是一个可由多种编程语言使用的“运行时”,提供内存管理、程序集加载、安全性、异常处理和线程同步等核心功能。
托管模块
可用支持CLR的任何语言创建源代码文件,然后用对应的编译器检查语法和分析源代码。无论选择哪个编译器,结果都是托管模块(managed module)。托管模块是标准的32位Microsoft Windows可移植执行体(PE32)文件,它们都需要CLR才能执行。
托管模块的各个部分
| 组成部分 | 说明 |
|---|---|
| PE32或PE32+头 | 标准Windows PE文件头,类似于“公共对象文件格式”(Common Object File Format,COFF)头。如果这个头使用PE32格式,文件能在Windows的32位或64位版本上运行。如果这个头使用PE32+格式,文件只能在Windows的64位版本上运行。这个头还表示了文件类型,包括GUI,CUI或者DLL,并包含一个时间标记来指出文件的生成时间。对于只包含IL代码的模块,PE32(+)头的大多数信息会被忽视。如果是包含本机(native)CPU代码的模块,这个头包含与本机CPU代码有关的信息 |
| CLR头 | 包含使这个模块成为托管模块的信息(可由CLR和一些实用程序进行解释)。头中包含要求的CLR版本,一些标志(flag),托管模块入口方法(Main方法)的MethodDef元数据token以及模块的元数据、资源、强名称、一些标志及其他不太重要的数据项的位置/大小 |
| 元数据 | 每个托管模块都包含元数据表。主要有两种表:一种表描述源代码中定义的类型和成员,另一种描述源代码引用的类型和成员 |
| IL(中间语言)代码 | 编译器编译源代码时生成的代码。在运行时,CLR将IL编译成本机CPU指令 |
元数据的用途
- 元数据避免了编译时对原生C/C++头和库文件的需求,因为在实现类型/成员的IL代码文件中,已包含有关引用类型/成员的全部信息。编译器直接从托管模块读取元数据。
- Microsoft Visual Studio用元数据帮助你写代码。“智能感知”(IntelliSense)技术会解析元数据,告诉你一个类型提供了那些方法、属性、事件和字段。对于方法,还能告诉你需要的参数。
- CLR的代码验证过程使用元数据确保代码只执行“类型安全”的操作。(稍后就会讲到验证。)
- 元数据允许将对象的字段序列化到内存块,将其发送给另一台机器,然后反序列化,在远程机器上重建对象状态。
- 元数据允许垃圾回收器跟踪对象生存期。垃圾回收器能判断任何对象的类型,并从元数据知道那个对象中的哪些字段引用了其他对象。
1.2 将托管模块合并成程序集
程序集
程序集 = 一个或多个托管模块 + 一个或多个资源文件 + 一个清单文件
清单是元数据表的集合。这些表描述了构成程序集的文件、程序集中的文件所实现的公开导出的类型、以及与程序集关联的资源或数据文件。
在程序集的模块中,还包含与引用的程序集有关的信息(包括他们的版本号)。这些信息使程序集能够自描述(self-describing)。
1.3 加载公共语言运行时
要知道是否已安装.NET Framework, 只需检查%SystemRoot%\System32 目录中的MSCorEE.dll文件。存在该文件,表明.NET Framework 已安装。
Windows根据EXE文件头决定进程的位数,在相应的目录加载MSCorEE.dll的版本。然后,进程的主线程调用MSCorEE.dll的一个方法,初始化CLR,加载EXE程序集,并调用其入口方法(Main)。这样,托管应用程序会启动和运行。
1.4 执行程序集的代码
一个方法首次调用时发生的事情
在方法执行之前,CLR会检测出的代码引用的所有类型。这导致CLR分配一个内部数据结构来管理对引用类型的访问。在这个内部数据结构中,类型定义的每个方法都有一个对应的记录项。每个记录项都含有一个地址,根据此地址即可找到方法的实现。对这个结构初始化时,CLR将每个及录项都设置成(指向)包含在CLR内部的一个未编档函数。我将该函数称为JITCompiler。
方法首次调用时,JITCompiler 会在定义(该类型的)程序集的元数据中查找被调用方法的IL。接着,JITCompiler 验证IL代码,并将IL 代码编译成本机CPU指令。本机CPU指令保存到动态分配的内存块中。
然后,JITCompiler 回到CLR 为类型创建的内部数据结构,找到与被调用方法对应的那条记录,修改最初对JITCompiler 的引用,使其指向内存块(其中包含了刚才编译好的本机CPU指令)的地址。
最后,JITCompiler 函数跳转到内存块中的代码。代码执行完毕并返回时,会回到方法中的代码,并象往常一样继续执行。
IL和验证
IL 基于栈。这意味着它的所有指令都要将操作数压入(push)一个执行栈,并从栈弹出(pop)结果。
IL的安全性:将IL 编译成本机CPU 指令时,CLR执行一个名为验证(verification) 的过程。这个过程会检查高级IL 代码,确定代码所做的一切都会安全的。例如,会核实调用的每个方法都有正确数量的参数,传给每个方法的每个参数都有正确的类型,每个方法的返回值都得到了正确的使用,每个方法都有一个返回语句,等等。
1.6 Framework 类库
FCL 是一组 DLL 程序集的统称,其中含有数千个类型定义,每个类型都公开了一些功能。
下面列举了应用程序开发人员可以利用这些程序集创建的一部分应用程序。
- Web 服务(Web service)
- 基于 HTML 的 Web 窗体/MVC 应用程序(网站)
- “富” Windows GUI 应用程序
- Windows 控制台应用程序
- Windows 服务
- 数据库存储过程
- 组件库
1.7 通用类型系统
CTS 规范定义,一个类型可以包含零个或者多个成员。
- 字段(Field)
- 方法(Method)
- 属性(Property)
- 事件(Event)
System.Object 类型允许做下面这些事情。
- 比较两个实例的相等性。
- 获取实例的哈希码。
- 查询一个实例的真正类型。
- 执行实例的浅(按位)拷贝。
- 获取实例对象当前状态的字符串表示。
1.8 公共语言规范
要创建很容易从其他编程语言中访问的类型,只能从自己的语言中挑选其他所有的语言都支持的功能。为了在这个方面提供帮助,Microsoft 定义了“公共语言规范”(Common Language Specification, CLS),它详细定义了一个最小功能集。任何编译器只有支持这个功能集,生成的类型才能兼容由其他符合 CLS、面向 CLR 的语言生成的组件。
1.9 与非托管代码的互操作性
CLR提供了一些机制,允许在应用程序中同时包含托管和非托管代码。具体地说,CLR 支持三种互操作情形。
托管代码能调用DLL 中的非托管函数 托管代码通过 P/Invoke(Platform Invoke)机制调用 DLL 中的函数。毕竟,FCL 中定义的许多类型都要在内部调用从 Kernel32.dll、User32.dll 等导出的函数。许多编程语言都提供了机制方便托管代码调用 DLL 中的非托管函数。例如,C# 应用程序可调用从 Kernel32.dll 导出的 CreateSemaphore 函数。
托管代码可以使用现有 COM 组件(服务器) 许多公司都已经实现了大量非托管 COM 组件。利用来自这些组件的类型库,可创建一个托管程序集来描述 COM 组件。托管代码可像访问其他任何托管类型一样访问托管程序集中的类型。这方面的详情可以参考 .NET Framework SDK 提供的 TlbImp.exe 工具。有时可能没有类型库,或者想对 TlbImp.exe 生成的内容进行更多控制。这时可在源代码中手动构建一个类型,使 CLR 能用它实现正确的互操作性,例如可从 C# 应用程序中使用 DirectX COM 组件。
非托管代码可以使用托管类型(服务器) 许多现有的非托管代码要求提供 COM 组件来确保代码正确工作。使用托管代码可以更简单地实现这些组件,避免所有代码都不得不和引用计数以及接口打交道。例如,可用C# 创建 ActiveX 控件或 shell 扩展。这方面的详情可以参考 .NET Framework SDK 提供的 TlbExp.exe 和 RegAsm.exe 工具。





