托管执行过程包括以下步骤,本主题稍后将详细介绍这些步骤:
- 选择编译器。 若要获取公共语言运行时提供的优势,必须使用面向运行时的一个或多个语言编译器。
- 将代码编译为中间语言。 编译源代码会转换为公共中间语言(CIL),并生成所需的元数据。
- 将 CIL 编译为本机代码。 在执行时,实时 (JIT) 编译器将 CIL 转换为本机代码。 在此编译过程中,代码必须通过验证过程来检查 CIL 和元数据,以确定代码是否可以确定为类型安全。
- 运行代码。 公共语言运行时提供基础结构,使执行能够进行,以及在执行期间可以使用的服务。
选择编译器
若要获取公共语言运行时 (CLR)提供的优势,必须使用面向运行时的一个或多个语言编译器,例如 Visual Basic、C#、Visual C++、F# 或许多第三方编译器之一(如 Eiffel、Perl 或 COBOL 编译器)。
由于它是多语言执行环境,因此运行时支持各种数据类型和语言功能。 你使用的语言编译器确定哪些运行时功能可用,并使用这些功能设计代码。 编译器(而不是运行时)建立代码必须使用的语法。 如果组件必须由以其他语言编写的组件完全可用,则组件的导出类型必须仅公开公共语言规范(CLS)中包含的语言功能。 可以使用该 CLSCompliantAttribute 属性来确保代码符合 CLS。 有关详细信息,请参阅 语言独立性和独立于语言的组件。
编译为 CIL
编译到托管代码时,编译器会将源代码转换为公共中间语言(CIL),这是一组独立于 CPU 的指令,可以有效地转换为本机代码。 CIL 包括有关对对象加载、存储、初始化和调用方法的说明,以及有关算术和逻辑作、控制流、直接内存访问、异常处理和其他作的说明。 在运行代码之前,CIL 必须转换为特定于 CPU 的代码,通常由 实时 (JIT) 编译器转换。 由于公共语言运行时为它支持的每台计算机体系结构提供一个或多个 JIT 编译器,因此可以在任何受支持的体系结构上编译并运行同一组 CIL。
当编译器生成 CIL 时,它还会生成元数据。 元数据描述代码中的类型,包括每种类型的定义、每种类型成员的签名、代码引用的成员以及运行时在执行时使用的其他数据。 CIL 和元数据包含在一个可移植可执行文件(PE)中,该文件基于已发布的 Microsoft PE 和通用对象文件格式(COFF),并对此进行了扩展,这些格式历来用于可执行内容。 容纳 CIL 或本机代码以及元数据的这种文件格式使操作系统能够识别公共语言运行时映像。 文件中元数据的存在以及 CIL 使代码能够描述自身,这意味着无需类型库或接口定义语言(IDL)。 运行时会根据需要在执行过程中查找和提取文件中的元数据。
将 CIL 编译为本机代码
在运行公共中间语言(CIL)之前,必须针对公共语言运行时将其编译为目标计算机体系结构的本机代码。 .NET 提供了两种方法来执行此转换:
- .NET 实时 (JIT) 编译器。
- Ngen.exe(本机映像生成器)。
由 JIT 编译器进行编译
在加载和执行程序集的内容时,JIT 编译在应用程序运行时按需将 CIL 转换为本机代码。 由于公共语言运行时为每个受支持的 CPU 体系结构提供 JIT 编译器,因此开发人员可以生成一组 CIL 程序集,这些程序集可以进行 JIT 编译并在具有不同计算机体系结构的不同计算机上运行。 但是,如果托管代码调用特定于平台的本机 API 或特定于平台的类库,它将仅在该作系统上运行。
JIT 编译考虑到在执行期间可能永远不会调用某些代码的可能性。 与其使用时间和内存将 PE 文件中的所有 CIL 转换为本机代码,不如在执行期间根据需要转换 CIL,并将生成的本机代码存储在内存中,以便在该过程的上下文中可供后续调用访问。 加载程序在加载和初始化类型时,会创建一个存根并将其附加到类型中的每个方法。 首次调用方法时,存根会将控件传递给 JIT 编译器,该编译器将该方法的 CIL 转换为本机代码,并将存根修改为直接指向生成的本机代码。 因此,对 JIT 编译方法的后续调用直接转到本机代码。
使用 NGen.exe 生成安装时代码
由于 JIT 编译器在调用该程序集中定义的单个方法时将程序集的 CIL 转换为本机代码,因此它在运行时对性能产生不利影响。 在大多数情况下,性能下降是可以接受的。 更重要的是,JIT 编译器生成的代码绑定到触发编译的进程。 它不能跨多个进程共享。 为了允许生成的代码在应用程序的多个调用或跨共享一组程序集的多个进程之间共享,公共语言运行时支持预先编译模式。 这种预先编译模式使用 Ngen.exe(本机映像生成器) 将 CIL 程序集转换为本机代码,就像 JIT 编译器所做的那样。 但是,Ngen.exe 的运作与 JIT 编译器的运作在三个方面有不同:
- 它在运行应用程序之前执行从 CIL 到本机代码的转换,而不是在应用程序运行时执行转换。
- 它一次编译整个程序集,而不是一次编译一个方法。
- 它将本机映像缓存中生成的代码保留为磁盘上的文件。
代码验证
作为其本机代码编译的一部分,CIL 代码必须通过验证过程,除非管理员建立了允许代码绕过验证的安全策略。 验证会检查 CIL 和元数据,以确定代码的类型是否安全,这意味着它仅访问它有权访问的内存位置。 类型安全性有助于隔离对象彼此,并帮助防止对象无意或恶意损坏。 它还保证可以可靠地强制实施对代码的安全限制。
运行时基于以下语句对于可验证类型安全代码为 true 这一事实:
- 对类型的引用与所引用的类型严格兼容。
- 仅对对象调用适当定义的操作。
- 标识与声称的要求一致。
在验证过程中,会检查 CIL 代码,试图确认代码只能通过正确定义的类型访问内存位置和调用方法。 例如,代码不能允许以允许溢出内存位置的方式访问对象的字段。 此外,验证会检查代码以确定 CIL 是否已正确生成,因为不正确的 CIL 可能会导致违反类型安全规则。 验证过程通过一组定义完善的类型安全代码,并且仅传递类型安全的代码。 但是,由于验证过程的一些限制,某些类型安全代码可能无法通过验证,而某些语言的设计不会生成可验证的类型安全代码。 如果安全策略需要类型安全代码,但代码不通过验证,则运行代码时将引发异常。
运行代码
公共语言运行时提供启用要发生的托管执行的基础结构以及执行期间可使用的服务。 必须先将方法编译为特定于处理器的代码,然后才能运行该方法。 首次调用 CIL 时,生成 CIL 的每个方法都是 JIT 编译的,然后运行。 下次运行该方法时,将运行现有的 JIT 编译本机代码。 JIT 编译,然后运行代码的过程将重复,直到执行完成。
在执行期间,托管代码接收垃圾回收、安全性、与非托管代码的互作性、跨语言调试支持以及增强的部署和版本控制支持等服务。
在 Microsoft Windows Vista 中,操作系统加载程序通过检查 COFF 标头中的一个位检查托管模块。 被设置的比特表示一个托管模块。 如果加载程序检测到托管模块,它将加载 mscoree.dll,并在_CorValidateImage
_CorImageUnloading
加载和卸载托管模块映像时通知加载程序。
_CorValidateImage
执行以下作:
- 确保代码是有效的托管代码。
- 将映像中的入口点更改为运行时中的入口点。
在 64 位 Windows 上, _CorValidateImage
通过将映像从 PE32 转换为 PE32+ 格式来修改内存中的图像。