CLR执行模型

前言   《CLR via C#》(Jeffrey Richter著)——.NET 界的经典之作,相读”恨晚”,读的过程写点笔记跟大家分享:

我也推荐大家看英文版,能够直接领会原意

认识CLR


个被多种编程语言使用的运行时。核心功能包括:内存管理,程序集加载,安全性,异常处理,以及线程同步。这些核心功能能够被所有以它作为目标平台的语言使
用,实际上,在运行时,CLR并不关心程序员使用哪一种语言编写源码的。微软开发了很多以CLR作为目标平台的语言编译器,


如:C++/CLI,C#,VB,F#,Iron Python,Iron
Ruby,以及IL汇编。另外还有很多其他的公司,学校开发了相应的编译器,如:Ada,APL,Caml,COBOL,Fortran,Lua等等。下
面的图展示了编译源文件的过程:


图可知,我们不用去考虑使用什么编译器,因为结果都是托管模块。托管模块是标准的PE32/32+文件(微软Windows可移植可执行文件,PE32表
示32位,32+表示64位),它需要CLR执行。托管程序集一直采用了数据执行保护(Data Execution
Prevention,DEP)和在Windows下的地址空间布局随机化(ASLR),这两个功能提升了整个系统的安全性。

托管模块的组成部分

1.PE32/PE32+ header:标准的PE文件头,类似通用对象文件格式头(Common Object File Format,COFF).
如果文件头是PE32格式:可以运行在32位/64位Windows系统。
PE32+格式:运行在64位系统。该文件头还指明了文件类型,如:GUI, CUI,  DLL并且包含文件创建的时间戳。
对于仅仅包含IL代码的模块这些在PE32(+)文件头里面的信息会被忽略。对于包含本地CPU代码,
该文件头包含本地CPU代码的信息
2.CLR header:包含托管模块的信息:需要CLR的版本,一些标志信息,MethodDef元数据(获取托管模块入口方法(Main方法)),以及模块的元素据,
资源,强命名,一些标志的位置/尺寸
3.元数据:每一个托管模块包含元数据表。有两种主要的类型:1.描述在源码中定义成员的类型。2.描述在源码中引入的成员类型

4.IL 代码:编译器编译源码产生的代码,在运行时,CLR将IL编译为本地CPU指令

本地代码编译器会以具体的CPU架构为目标产生代码,比如X86,X64,或IA64。所有符合CLR的编译器都会编译源码生成IL代码,也称为托管代码,因为由CLR管理IL的执行。

什么是元数据?

简言之,元数据是描述在模块里面定义了什么的这样数据表集合,比如成员的类型,比如引用了其他的什么类型或成员。

元数据一直作为代码嵌入在同名的EXE/DLL文件里面,使得它们不可能分开。因为编译器是同时生成元数据和IL代码并绑定到托管模块。

元数据的用途

1.编译不需要依赖于原生C/C++头及库文件,因为可以直接从托管模块读取元数据

2.智能提示

3.CLR用元数据进行代码审核,确保类型安全

4.允许序列化一个对象的字段为一个内存块,发送到其他机器,然后被反序列化,重建对象状态。

5.允许GC跟踪对象的生命周期

 

认识程序集

CLR不直接运行模块,而是程序集。程序集是一个抽象的概念:

1.程序集是一个或多个模块或资源文件的逻辑分组。

2.程序集是重用,安全性,版本控制的最小单元

根据我们使用的编译器或工具,可以生成一个或多个文件的程序集。在CLR世界里,一个程序集就是我们通常所说的组件。

下面的图具体说明什么是程序集:

一些托管模块和资源文件通过一个工具进行处理,然后生成一个代表文件逻辑分组的PE32(+)文件。这个PE32(+)文件包含了数据块——称为清单,该清单是另一种简单的元数据集表,这些表描述了组成程序集的文件。

默认情况下,编译器实际做的工作就是将托管模块转换为程序集,也就是说编译器会生成一个包含清单的托管模块。对于只有一个托管模块没有资源或数据文件的项目而言,程序集就是托管模块,创建的过程中也不需要额外的步骤。如果想把文件组合到程序集中,很多工具能够实现。

程序集允许我们可重用的,安全的,版本控制组件的逻辑和物理概念分离开来。至于怎么分离代码跟资源文件完全取决于自己。一个程序集的模块可以包含引用的另外的程序集的信息(版本号等)——程序集的自我描述,换句话说,CLR可以决定程序集执行的直接依赖的顺序。  

运行一个可执行文件需要做的工作?

如果一个非托管的程序调用LoadLibrary载入一个托管程序集,Windows会加载并初始化CLR(如果没有加载),当然前提是进程已经启动。

什么是LoadLibrary?

1.仅仅在桌面应用程序才有   2.载入指定的模块到调用进程的地址空间。指定的模块可能引起其他的模块也被载入

 

认识IL

托管程序集包含元数据和IL。IL是一种独立CPU的机器语言,它是微软在咨询了很多商业和学术语言/编译器的作者后创造的。

IL比大多数CPU机器语言高级。Why?

1.IL可以访问并操作对象的类型    2.IL具有创建和初始化对象的指令   3.IL可以调用对象的虚方法   4.IL可以直接操作数组元素  

5.IL具有处理错误异常的指令

可以认为IL是一种面向对象的机器语言

通常我们使用向C#这样的高级语言开发,编译器会生成IL。像其他的机器语言一样,IL可以使用汇编语言编写,微软提供了IL汇编语言编译器——ILAsm.exe,
以及反汇编编译器——ILDasm.exe

不同于C#仅仅暴露CLR提供的功能的一个子集,IL汇编语言运行开发者访问所有CLR的功能。所以这里更新一个误区:不要以为C#提供给我们的功能就是CLR的全部功能。

了解CLR的JIT(即时编译)

下图展示当方法首次调用时发生的事情


Main方法执行之前,CLR会检测被Main方法引用的所有类型并且分配一个管理访问引用的类型的内部数据结构。上面例子Main方法中引用了
Console,所以CLR会分配一个内部的数据结构。这个数据结构包含了定义在Console类里面的每一个方法的入口,每一个入口保存的方法可以找到对应方法的地址。当这个内部结构初始化时,CLR设置每一个入口到一个内部的,未公开的CLR里面的函数,这个函数称为JITCompiler。

了解JITCompiler函数


Main方法使Console第一次调用WriteLine方法时,JITCompiler函数会被调用。JITCompiler负责将一个方法的IL代
码编译为本地CPU指令,因为IL就是在”即时编译”时被编译的,CLR的这个组件通常被称为JITer或JIT compiler。

当调用时,JITCompiler知道哪一个方法被调用并且定义在方法里面的类型是什么。

1.接着JITCompiler函数搜索调用方法的IL定义在程序集的元数据

2.再接下来就是审核并编译该IL为本地CPU指令。本地CPU指令会保存在一个动态分配的内存块里面

3.然后,JITCompiler回到CLR创建的内部数据结构的该方法的入口处,用编译成的CPU指令保存的内存地址替换掉开始时推该方法的引用

4.最后,JITCompiler函数跳转到放在内存里面的代码处,该代码实现了WriteLine方法(重载获取一个string参数的)。当这个方法返回时回到Main并继续往下执行。

往下执行,Main方法会第二次调用WriteLine方法,这一次,WriteLine的IL代码已经被审核和编译过了,不会调用
JITCompiler,而是直接跳转到内存里存放该代码的地方,在WriteLine方法执行完成返回Main。下图展示了第二次调用
WriteLine方法的过程:

我知道很多朋友都必备了这本书,希望路过的朋友多留言或讨论,或指正。

注   《CLR via C#》(Jeffrey Richter著)——.NET 界的经典之作,读的过程写点笔记跟大家分享,我也推荐大家看英文版,能够直接领会原意 

作者:张雪飞
出处:https://zhangxuefei.site/p/119
版权说明:欢迎转载,但必须注明出处,并在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

发表评论

电子邮件地址不会被公开。 必填项已用*标注