C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动

本文是《CLR via C#》(第四版)第1章的要点总结。

下一篇:读书笔记(2)—— 程序集的强名称、部署与查找

目录

1 托管模块与程序集的概念

1.1 托管模块的概念与构成

1.2 程序集

2 CLR 与 .Net Framework 的关系

3 CLR的启动过程

4 程序集的执行

4.1 IL代码的执行

4.2 JIT编译与执行

4.3 安全性验证

5 CTS,CLI,CLS,FCL等的含义与区别

5.1 FCL 与 BCL

5.2 CTS

5.3 CLS

5.4 CTS 与 CLS 的区别

5.5 CLI

5.6 .NET Framework 

5.7 .NET Standard

参考资料


1 托管模块与程序集的概念

1.1 托管模块的概念与构成

所有面向CLR的编译器,都将其对应代码文件编译成一个托管模块(managed module),不管是用那种语言编写的。一个托管模块就是一个标准的 PE32/PE32+ 文件。托管模块只能在CLR环境中执行。

托管模块的结构如下图所示:

C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动_第1张图片 托管模块结构

元数据本质上就是一系列的数据表,描述了托管模块定义了什么(例如类及其成员)、引用了什么(导入的类型及其成员)等信息。元数据包含了模块所有的结构性信息,因此,我们说托管模块是自描述的,不需要C或C++中的头文件,也不需要注册表来保存额外的信息。实际上,反射或者VS 的 智能提示就是利用了元数据读取程序集的内部信息。

1.2 程序集

CLR 的执行单元并不是托管模块,而是程序集。程序集是个逻辑概念,是模块/资源文件的分组。它可以是一个单独的模块文件,也可以由多个托管模块文件组成,这取决于编译器或工具。在CLR世界中,程序集就是一个组件,实现程序的重用、安全性以及版本控制。

程序集也是一个(或多个)PE32(+) 文件,与托管模块不同的是,程序集的元数据中包含一个 清单(manifest)表。清单表描述了构成程序集的文件(托管模块、资源文件)以及各个文件中定义的 public 的类型,以及与程序集关联的资源或数据文件。一般情况下,编译器默认将生产的托管模块转成一个程序集

托管模块扩展名为.netmodule,程序集一定包含一个主托管模块,主托管模块名称为.exe或.dll,其中可以没有代码,但必须有清单(也只有主模块有清单,CLR加载程序集时,首先找到含有manifest的文件,再根据manifest内容找到其他模块或资源)

例如,对于只含有一个托管模块的程序集来说,其结构如下:

C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动_第2张图片 主托管模块的结构

 然而对于一个包含多个托管模块的程序集 C.dll 来说,其结构为:

C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动_第3张图片 一个包含多个托管模块和资源文件的程序集结构

 C.dll 清单表中拥有指向各个组成模块以及资源文件的token。注意 C.dll 本身也是一个托管模块,即为程序集的主模块。

2 CLR 与 .Net Framework 的关系

如今,CLR 通常作为 .Net Framework 的一部分提供,但 CLR 和 .Net 并不是一回事儿。CLR 是一个运行时环境,而 .Net Framework 是一个软件包。要运行一个托管程序,必须安装CLR,而不一定需要 .Net Framework。不过如今的 windows 都预装了 .Net ,于是CLR 也有了,我们不必操心运行时环境了。

.Net Framework 除了提供 CLR以外,最重要的是为我们开发人员提供了一些基础类库(BCL)和语言编辑器、调试器等工具。

                                                                 .Net Framework = CLR + BCL + 语言编辑器调试器等工具

CLR 主要提供内存管理、程序集加载、及时编译(将IL编译成平台专用代码)、安全性、异常处理、线程同步等通用功能。因此,可以通俗总结:.Net Framework 提供类库、工具帮助我们写代码并编译成程序集,CLR负责程序集的加载、JIT、运行管理。

C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动_第4张图片 CLR 是 .NET Framework 的一部分

CLR 与 .NET 的版本对照关系如下:

.NET 与 CLR 版本对照关系

3 CLR的启动过程

一个托管程序集被OS加载到进程中之后,是如何启动CLR虚拟机来运行它的呢?Windows 系统有一个关键的文件对CLR的加载起到了重要作用,他就是 mscoree.dll。这个程序集全名是 Microsoft .Net Runtime Execution Engine,顾名思义他就是加载CLR的引擎。

大致步骤如下:

(1)进程的创建:Windows 检查 exe 文件头,来决定创建 32 位或者 64 位的进程。

(2)加载 mscoree.dll:面向 CLR 的程序集都会依赖这个 mscoree.dll 程序集。Windows 在进程地址空间中加载对应的 mscoree.dll 的x86/x64/ARM版本。如果 Windows 是 x86 或ARM 版,mscoree.dll 在 %SystemRoot%\System32 下;如果 Windows 是64版,64位版本的 mscoree.dll 也在上述目录下以向后兼容,而 x86 版的在 %SystemRoot%\SysWow64 目录中(64位Windows采用 WindowsOnWindows64 技术运行 32 位的程序)。

(3)主线程调用 mscoree.dll 的 _CorExeMain 方法,这个方法是整个程序集的入口方法[参考1]。该方法负责初始化CLR。

(4)CLR的加载:CLR也是一系列的DLL文件。系统可安装多个.Net版本,因此也会有多个CLR。.Net 位于 %SystemRoot%\Microsoft.NET\Framework 或%SystemRoot%\Microsoft.NET\Framework64 路径下。_CorExeMain 方法根据注册表的相关信息(CLR本质是个COM)找到最合适的 CLR版本,调用.net路径下的 MSCorWks.dll(.NET Framework 1.0 | 2.0)或 Clr.dll(.NET Framework 4.0)或 MSCORSVR.DLL(Server版)。这几个程序集就是CLR的主程序。

(5)CLR启动后,创建AppDomain,加载 这个exe 文件,找到其入口方法(main方法),启动并运行这个托管程序。

C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动_第5张图片 CLR 的加载,来源见水印​​​​​

 

4 程序集的执行

4.1 IL代码的执行

CLR启动后加载托管程序集,读取其中的IL代码并编译成本机CPU指令。大致步骤如下:

(1)验证 Verification:CLR检查 IL 代码的安全特性,确保程序集不会出现不安全的操作;

(2)分配内存空间;

(3)把IL发送给JIT,把其中一部分翻译成本机代码。其他的IL在需要时被编译,编译后就会存在缓存中,因此只需编译一次。这个编译过程称为“即时(JIT)编译”;

(4) CLR负责监视程序集的运行,进行垃圾回收,数组边界检查,异常处理等工作。

C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动_第6张图片 托管代码的执行

4.2 JIT编译与执行

一个IL代码块具体的编译与执行流程:

案例:

C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动_第7张图片 代码块的首次执行

过程:

(1)CLR检测 main 代码引用的所有类型(上图中 Console 类型);

(2)加载引用的类型(程序集),并为其创建一个内部结构来存储其方法地址(初始时方法地址都是无效的);

(3)获取调用方法的 IL 并为它分配内存;

(4)调用 JITCompiler 函数,将IL编译成CPU指令,保存在刚才动态分配的内存中;

(5)修改类型的内部结构中该方法引用地址使其指向刚才编译的内存块;

(6)JITCompiler 函数跳转至刚才的内存块执行其中的代码;

(7)执行完返回 main 中的代码,按照相同的流程执行下面的代码。

若一个方法第二次执行,则不必再编译,因此首次执行时编译的本机代码会缓存下来,可以直接使用。因此,虽然托管程序的JIT编译会造成一定的性能损失,但由于只编译一次,这部分的性能损失相对较小。此外,JIT编译会带来一些优势:由于是在运行时编译,运行环境已知,CLR会根据CPU等环境优化编译后的本机代码,从而实现效率的提升。

4.3 安全性验证

IL代码和 CLR 一起可构建一个安全的应用程序,保证程序运行时的鲁棒性(健壮性)。可靠性是指系统的正确性,即在确定的输入情况下,有稳定的可预测的输出;而所谓鲁棒性,是指系统对参数变化不敏感,不会受到外界错误的影响。例如,一个程序再运行完后能否正确释放内存,是否对系统外的组件产生影响,这是鲁棒性的一个重要因素。

CLR通过验证(verification)机制,在运行前检查IL代码是否是安全的,由此便可确保程序不会错误地访问内存,从而可以放心地让多个托管应用程序在同一个进程中运行,进而减少进程数量,提高系统效率。这就是AppDomain能够诞生的基础,也是CLR的一个亮点。

5 CTS,CLI,CLS,FCL等的含义与区别

5.1 FCL 与 BCL

FCL是指 Framework Class Library,即Framework类库。它是指.Net Framework提供的一组DLL文件,其中包含很多定义的类库,例如一些命名空间:

C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动_第8张图片 FCL部分类库

 FCL中最基础的部分称为基础类库(BCL)。

5.2 CTS

CLR 一切都围绕着类型运转,CTS就是类型运转的标准。在.NET中,一切数据类型要么是一个 Class(引用类型),要么是一个 Structure(值类型)

CTS 即通用类型系统,提供了通用类型的定义,描述了类型的定义和行为。这些规范包括:

(1)类型的成员,包括 字段,方法,属性,事件等;

(2)类型的可见性规则和成员的访问规则;

(3)类型的继承、虚方法、对象生存期等其他规则;

(4)制定了基本数据类型等等。

具体可参考[2]。

CTS 定义了如何在 CLR 中声明、使用和管理一个类型,从而实现跨语言交互。CTS 的主要工作包括:

(1)建立了一个规范框架,帮助我们进行跨语言集成、确保类型安全、提高代码运行性能;

(2)提供了一个大多数语言都能完整支持的 面向对象模型;

(3)定义了所有语言都必须遵守的规范,从而实现对象的跨语言交互;

(4)提供了一个基础数据类型库(例如 Boolean, Byte, Char, Int32, and UInt64)。

CTS 定义了两种类型:值类型和引用类型。任何以.NET平台作为目标的语言必须建立它的数据类型与CTS的类型间的映射。

C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动_第9张图片 CTS定义的类型单继承层次结构

5.3 CLS

CLS即公共语言规范。它是一个以.NET平台为目标的语言实现语言互操作性的最小的功能集,是CTS的一个子集。特点:

(1)一种语言不是必须遵守CLS

(2)但若要实现语言的互操作,必须在public/protected成员中遵守CLS规范(即CLS兼容)。

(3)可用[assembly:CLSCompliant(true)]特性来让编译器检查代码是否是CLS兼容的。

(4)一些语言相关的委托,构造器,终结器,操作符重载等必须转换成基本字段或方法才能使其他语言访问。例如事件本质是字段,属性本质是方法。面向CLR的编译器会处理这个方面的转换。

C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动_第10张图片 CLS兼容的基本​​​​数据类型

具体参考[4]。

5.4 CTS 与 CLS 的区别

CLR 是对CTS 的实现,一个高级语言要想在 .NET/CLR 中运行,必须要遵守CTS。CLR 支持非常多的特性,但并不是每种高级语言都能完整覆盖CLR所提供的特性(但IL可以,所以真正的高手可用IL实现丰富的特性)。如下图所示,不同高级语言覆盖的CLR 特性不同(一个典型例子,CTS中的某些基本数据类型在一些高级语言中不支持)。但要想实现不同语言的互操作行,必须制定一个各个语言都能支持的更小的特性集,即CLS。例如CLS中进一步缩小了基本数据类型的范围(参考[4])。

C#与CLR学习笔记(1)—— 程序集的结构与CLR的启动_第11张图片 CTS,各语言,CLS之间的关系

总之,遵守 CTS 规范可以让不同语言在 .net 平台上顺利运行,然而 若要实现语言间的完全互操作性则必须遵守 CLS 规范。关于语言独立性和语言的互操作性参见[5]。

5.5 CLI

CLI即公共语言架构,包括CTS、CLS、CIL(即IL)、文件结构、元数据、底层平台访问方式等的综合标准。.Net Framework是CLI在Windows平台下一个完整实现。Mono、.Net Core也是CLI的实现,他们都是为跨平台而生的。

5.6 .NET Framework 

上文中提到,.NET Framework 是 CLI 在 Windows 平台中的一个实现,它提供了 CLR,以及一组内置的标准类库FCL。因此,可以认为 .NET Framework = CLR + FCL。其中,CLR提供了如下功能:

(1)IL 的解析。根据 CTS 规范,CLR 负责读取 .NET 程序中的 IL,并识别其中的模块、类型、成员、方法等;

(2)JIT。IL语言的及时编译,在运行时将可执行文件中的IL实时编译为与硬件相关的机器代码(Native Code);

(3)异常处理机制

(4)线程管理。CLR 对原生的线程、同步对象进行了封装,称为“托管线程”;

(5)内存管理与GC

(6)类型安全。CLR 保证了类型安全。

5.7 .NET Standard

不同平台的 .NET 实现中,提供的 FCL 会有差异,这样不利于代码的移植。因此,微软制定了 .NET Standard 规范,以后所有的 .NET 实现都将遵循 .NET Standard 。说白了,.NET Standard 定义了一组通用类库的接口 API,但是接口具体的实现仍由不同的 .NET平台来负责。这样,只要代码遵循了 .NET Standard,就可以不加修改地运行在支持相同版本 .NET Standard 的所有CLR平台上。我们在 Visual Studio 中可以将项目的目标架构设为 .NET Standard。

更多信息,请参考官方文档。

参考资料

[1] .NET中的幕后英雄:MSCOREE.DLL

[2] CTS & CLS

[3] CTS

[4] CLS

[5] 语言独立组件与跨语言互操作性

[6] .NET Standard

你可能感兴趣的:(读书笔记,.NET,CLR,.net)