托管程序的启动过程(.NET CLR 寄宿)
大家都知道 C# 等托管语言编写的代码都会被编译成托管程序集(*.exe 或 *.dll),这些托管程序集最终都会托管给 CLR(公共语言运行时)来执行。那么,托管程序的启动过程是怎样的?CLR 又是如何寄宿到宿主程序中的?为了回答以上问题,本文将首先介绍托管程序集的生成过程;然后介绍托管程序的启动过程,以及该过程中 CLR 的加载流程。
一个托管应用程序首先被操作系统启动,然后由操作系统调用 CLR 来托管该程序。那么 .NET 框架到底以什么方式让操作系统认识 CLR 并且可以启动它呢?微软实际将 CLR 作为 COM 服务器实现在一个 DLL 中,并提供了标准的 COM 接口。既然是 COM 服务,也就意味着普通的非托管程序也可以调用 CLR 来运行托管代码,我们把这种调用方式叫做寄宿,把调用 CLR 的非托管程序叫做宿主。宿主程序不仅可以调用 CLR 来执行托管程序集,还可以通过它来进行内存管理、垃圾回收管理、策略管理、事件管理以及线程控制等高级管理。
![](https://i-blog.csdnimg.cn/blog_migrate/03b1c6d399b1c5cd5098703b5d785377.png)
如上图所示,托管语言编写的源代码文件会被编译成托管模块,多个托管模块连同一些资源文件会被合并成托管程序集。托管程序集通常有 EXE 和 DLL 两类,其中前者(EXE)能被操作系统直接启动,后者(DLL)可以被前者调用。托管 EXE 被启动后,操作系统会调用 CLR 来托管它。CLR 启动后,会创建 AppDomain 来加载并运行托管程序集。
- 托管模块已经是标准的 PE 文件了,为了部署和版本管理的方便,编译器会将多个托管模块连同相关的资源合并成一个 PE 文件,合并生成的 PE 文件被称为程序集。
- PE 文件的全称是 Portable Executable,意为可移植执行体(文件)。常见的 EXE、DLL、OCX、SYS、COM 都是 PE 文件,PE 文件是微软 Windows 操作系统上的程序文件(可能是间接被执行,如 DLL 文件)。
托管程序集的生成
如上所述,托管程序集的生成要经历两个步骤,首先是将源代码编译成托管模块,然后再将多个托管模块及资源(或数据)文件合并成程序集。
1. 将源代码编译成托管模块
如上图所示,源代码可以由多种托管语言编写,然后由相应的编译器编译成统一的托管模块。托管模块是标准的 PE 文件,其中除了包含中间语言(IL)代码和元数据(Metadata)外,还包含一些头信息。
- PE32 或 PE32+ 头 :标准的 Windows PE 文件头,其标记了文件运行的系统版本、文件类型(GUI\CUI\DLL)、生成时间等。PE32 文件能在 Windows 32 位或 64 位上运行,PE32+ 文件只能在 64 位版本上运行。
- CLR 头 :包含了一些托管信息,比如 CLR 版本、托管模块入口方法的元数据标记,以及模块的强名称等。
- 元数据 :主要包含两种类型的表,一种类型的表描述源代码中定义的类型和成员;另一种类型的表描述源代码引用的类型和成员。
- IL 代码 :编译器编译源代码时生成的代码,这些代码将在运行时被 CLR (JIT) 编译成 CPU 指令。
2. 将托管模块合并成程序集
CLR 实际不和模块一起工作。相反,它是和程序集一起工作的。如上图所示,程序集是由多个托管模块(PE 文件)和资源(或数据)文件合并而成的单个 PE 文件。合并生成的 PE 文件中包含一个名为 “清单(Manifest)” 的数据块,其描述了构成程序集的文件,由程序集中的文件实现的公开类型,以及与程序集关联在一起的资源或数据文件。
- 默认情况下,编译器会把生成的托管模块转为程序集,即使只有一个托管模块。
- 将托管模块合并为程序集,主要是方便于文件部署及版本管理。
托管程序的启动
前面介绍了托管程序集(PE 文件)的生成过程,托管程序集要么是 EXE 文件,要么是 DLL 文件。Windows 操作系统可以直接启动 EXE 文件,我们来看看托管 EXE 的启动过程是怎样的。
CLR 介绍
在介绍托管程序的启动过程之前,我们先来了解一下 CLR 。CLR(Common Language Runtime)是公共语言运行时,它可以加载并执行托管模块(将模块中的 IL 代码编译成 CPU 指令,并执行)。除此之外,CLR 还提供了以下功能:
- 内存管理
- 程序集加载
- 安全性
- 异常处理
- 线程同步
CLR 被定义为 COM 服务,MSCorWks.dll(.NET Framework 1.0 | 2.0)和 Clr.dll(.NET Framework 4.0) 实现了此 COM 服务,这两个文件位于 %SystemRoot%\Microsoft.NET\Framework(64) 下的相应子目录中。
虽然 CLR 是 COM 服务,但是在创建 CLR 实例时并不直接使用的 CoCreateInstance 方法,而是使用了另一个被称为 “垫片” 的 MSCorEE.dll 文件,由它去决定创建哪个版本的 CLR 实例。这个文件位于如下位置:
- %SystemRoot%\System32
- %SystemRoot%\SysWow64
.NET Framework 4.0 支持单个进程中同时运行多个版本的 CLR,可使用 ClrVer.exe 来检查某个进程中的 CLR 版本。
CLR 加载流程
![](https://i-blog.csdnimg.cn/blog_migrate/7b99717d719c91277d510ea7dcd07ba7.png)
运行一个托管 EXE 文件时,Windows 会检查 EXE 文件头,决定创建 32 位、64 位还是 WOW64 进程,在进程地址空间中加载 MSCorEE.dll 的相应版本。然后,进程主线程调用 MSCorEE.dll 中定义的一个方法。这个方法初始化 CLR ,加载 EXE 程序集,再调用其入口方法(Main),随即,托管应用程序启动并执行。
首先,托管 EXE 的文件头中包含 JMP _CorExeMain
指令,该指令指向了 MSCorEE.dll 文件,因此该文件被启动。MSCorEE.dll 始终是最新版,它随最新的 CLR 一起部署在 %SystemRoot%\System32 (SysWow64) 目录中。
然后,MSCorEE.dll 会调用其内部的 CLRCreateInstance 来创建 CLR 实例。在创建实例过程中会从 EXE 中提取 CLR 的版本信息,应用程序也可通过配置文件中的 requiredRuntime & supportedRuntime 来为该过程指定 CLR 版本。
最后,CLR 接管宿主程序的主线程,加载托管模块并编译执行。
参考资料
本文主要参考了 《CLR via C#》 一书的以下两个章节:
- 第 1 章. CLR 的执行模型
- 第 22 章. CLR 寄宿和 AppDomain