- A+
从编程开发的角度来简单来说,CLR就相当于“执行/运行”我们所编写程序的“环境/服务”。这就好比如我们组装了一个赛车,我们的赛车需要依赖“跑道”作为一个环境,赛车才能进行飞驰。而这个“跑道”就类似于CLR。在Java平台中程序员要向一台电脑部署软件时,要确保软件运行,电脑上就要按照JVM(Java虚拟机),那么这里的“Java虚拟机”就相当于我们.NET平台的CLR(Common Language Runtime,公共语言运行时)。
对CLR整体概况来说,CLR是一个软件层面的代理服务,它负责管理.NET程序集的执行/运行,其中主要包括:
- 管理应用程序域
- 加载和运行程序集
- 安全检查
- 线程管理
- 将CIL代码即时编译为机器代码
- 异常处理机制
- 对象析构
- 垃圾回收
注意,这些执行项目并非发生在编译时,而是发生在程序运行的过程中,也因此名称中包含了“运行时”一说。
CLR是.NET框架中最核心的部分,从上述的讲解中我们只能对CLR有个简单的印象,通过几句话或者某些概念也并非能够对CLR有一个深刻的理解,如果你想要牢记它,就必须知道它的作用以及它的工作流程,接下来开始随着本文开始更进一步的学习。
1.读取程序集
CLR在对CIL语言代码进行编译前,需要先将编译的环境运行起来并对程序集进行一个读取过程,这个读取过程其实也就描述出了程序集中的一个基本构造。
读取过程如下:
1.1.文件格式
操作系统首先检查文件是否是一个可执行文件,对于Windows操作系统而言可执行文件就是:“PE/COFF(windows可移植可执行/通用对象文件格式)”。在检查完文件是否是可执行文件后,操作系统会在检查文件格式中是否包含“CLR头”,如果存在“CLR头”,那么操作系统就知道该文件是一个.NET程序集,需要区别于其他类型的可执行程序。
1.2.清单
在读取到“CLR头”确认文件是一个程序集之后,会读取程序集中包含的清单(manifest)。这个清单相当于一个目录,描述了程序集自身的基本信息。这其中包括:程序集的标识(名称和版本)、描述构成程序集的所有文件、程序集所有外部依赖项的列表等。
1.3.元数据
清单之后就是元数据,在.NET中查看元数据的过程也叫做反射。如果说清单描述了程序集自身的信息,那么元数据则描述了程序集包含了哪些内容项。这些内容项包括:程序集包含的模块、类型、类型的成员、类型和类型成员的可见性等。另外要强调的是,元数据并不包含其中内容项的具体实现。
1.4.CIL和资源
在接下来就是:已经被编译器转换的CIL代码和一个资源文件。CIL代码也就是元数据中所有类型的具体实现,例如包括:方法体、字段等。资源文件代表程序集所嵌套包含的文件,其中会包含一些如:jpg、xml、txt等文件。
2.非托管的CLR
托管代码之所以被称为托管代码,是因为它是由CLR进行托管的,所以间接的可以猜测到CLR肯定不是托管代码编写的。这就是好比如警察负责捉小偷,总不能找个小偷去捉小偷吧。因为CLR本身是用于管理托管代码的,因此它是由非托管代码C++编写的。另外我们可以通过命令行工具在本机上查看对应CLR运行时的版本,如图:
3.CLR运行工作流程
操作系统首先会检查程序集文件中是否包含PE头和CLR头,如果存在则会加载mscoree.dll组件,mscoree.dll是CLR中最重要的一个组件,又称公共对象运行库执行引擎。它在加载之后,会调用它其中的_CorExeMain函数,该函数会根据程序集而加载合适版本的CLR。
在CLR运行之后,程序的执行权就叫给了CLR。CLR会找到程序入口点,通常是Main()方法,然后执行它。这个执行过程包含以下内容:
3.1.加载类型 Class loader(类加载程序)
在执行“程序入口点”之前,CLR首先要找到“程序入口点”的所在的类型并且加载这个类型。
CLR中一个名为Class loader(类加载程序)的组件负责这项工作。它会从GAC、配置文件、程序集元数据中寻找这个类型,然后将它的类型信息加载到内存中。在Class loader找到并加载完这个类型之后,它的类型信息会被缓存起来,这样就无需再次进行相同的过程。在加载这个类以后,还会为它的每个函数插入一个存根。
另外,在类型加载是时候,CLR不光会加载我们自定义的程序集,这其中还会与.NET的基础类库中的类型进行交互。也就是对mscorlib.dll这个程序集进行类型加载,这个程序集封装了各种我们常用的编程类库和核心的数据类型。
3.2.验证
在CLR中,还存在一个验证程序(verfier),该验证程序的工作是运行时确保代码是类型安全的。注意验证两个方面,一个是元数据是否正确,另一个是CIL代码必须是类型安全的,类型的签名必须正确。例如,代码不允许以允许内存位置溢出的方式访问对象的字段、验证检查代码以确定是否已正确生成 MSIL等等。
3.3.即时编译JIT
运行CIL (公共中间语言/中间代码) 前,必须通过CLR(公共语言运行时)将其编译为目标计算机基础结构的本机代码,这个编译就是由CLR中的即时编译器(JIT)来完成的。
即时编译只有在函数的第一次调用时发生。因为在Class loader(类加载程序)执行的时候会为每个函数插入一个存根。那么在调用函数时,CLR会检查函数对应的存根,如果没有相应的存根,则JIT才会执行编译,并将该函数被编译后的本地机器代码地址写入到函数对应的存根中。当第二次对同一函数进行调用时,会再次检查这个存根,如果发现其保存了本地机器代码的地址,则直接跳转到本地机器代码进行执行,而无需再次进行JIT编译。
另外基于这一现象,我门也从侧面知道函数存根中存储的就是,函数对应的本地机器代码的地址。
通过结合上面文字描述后,我们将其整理绘制出,CLR运行整体工作流程图:
总结
在实际的生活当中,往往工作了几年的程序员都没对.NET框架有一个基本的认识,有的时候还会在面试中被这个方面的问题“卡过喉咙”。而我个人认为作为一个合格的.Net程序员,理解.NET框架是最基本的素质之一,这也是为了走向更远的奠基石。
如果要深入了解更多关于CLR部分的内容,可以浏览下方官方地址:
https://docs.microsoft.com/zh-cn/dotnet/standard/clr