学习 CLR via C#(第4版)(1) - CLR的执行模型【2,3略】

CLR的执行模型

*1.1将源代码编译成托管模块

什么是CLR

公共语言运行时(Common Language Runtime)和Java虚拟机一样也是一个运行时环境它负责资源管理(内存分配和垃圾收集等),并保证应用和底层操作系统之间必要的分离

CLR的核心功能

CLR的核心功能包括:内存管理,程序集加载,安全性,异常处理和线程同步。

CLR与使用的编程语言有关吗?

无关。只要编译器是面向CLR的就行。

编译过程

一个程序写完肯定要编译,以前什么C啊什么的都是编译成本机的CPU指令,但是我们的C#不是。

C#,VB.NET都会把它们编译成**托管模块,托管模块在一个标准的可移植的PE文件(Portable Executable) **中。

所谓PE文件(Portable Executable) ,就是可移植执行体简单来讲就是**.EXE.DLL**

**组成:**多个托管模块和资源文件合并成一个包含清单的程序集

托管模块主要包含:

IL(源代码被编译后的代码,运行时会被CLR编译为本地CPU指令,又称中间语言、托管代码)


元数据(两种元数据表,一种描述源代码中定义的类型和成员,另一种描述源代码引用的类型和成员)


PE表头:(描述文件:如果是PE32格式,能运行在32或者64位版本上,PE32+只能运行在64位版本上)

1)标识了文件类型(GUI, CUI, DLL);2)包含一个文件生成时间的时间标记;3)包含与本地CPU代码有关的信息(在该模块包含CPU代码的情况下)。


CLR表头(描述这个对象的整体的一些信息,比如main入口方法)

1)包含需要的CLR版本;2)托管模块入口方法的MethodDef元数据标记;3)模块的元数据,资源,强名称,一些标记以及不太重要的数据项的位置及大小。


元数据(metadata):简单的说就是一个数据表集合,一中描述了模块中定义了什么,比如说类型及其成员,另一种描述了模块引用了什么,比如说导入的类型及其成员。元数据总是保持和IL代码文件的关联和绑定的,并嵌入最终生成的托管模块

元数据的部分应用:

(1)编译器可以直接获取元数据中存放的有关引用类型或者成员的全部信息,避免对原生C/C++头和库文件的需求

(2)Visual Studio,通过智能感知技术能够解析元数据,告诉我们一个类提供了那些方法,属性,事件,和字段,对于方法可以告诉你需要的参数

(3)CLR通过代码验证过程使用元数据确保代码只执行“类型安全”的操作

(4)元数据支持序列化和反序列化

(5)元数据允许垃圾回收器跟踪对象生命周期,使其判断对象的类型,并通过元数据得知哪些字段引用了其他对象

ps:一个PE文件可不只一个托管模块,它可以由几个托管模块组成。编译器会把多个托管模块和资源文件合并成一个包含清单的程序集,这才是最后的PE文件。

运行过程

PE文件并不能直接运行,他需要CLR。

CLR是公共语言运行时,负责资源管理(内存分配和垃圾收集等),核心功能包括:内存管理,程序集加载,安全性,异常处理和线程同步。

我们的电脑上安装了.NET Framework。当你在打开exe程序的时候你的进程的主线程会调用MSCorEE.dll的一个方法,这个方法会初始化CLR,再加载exe程序集,然后调用入口方法即main函数。

**运行过程:**在运行时编译我们的IL代码,早在Main函数执行之前,CLR就会检测Main的代码所引用的所有类型,然后生成了一个内部的数据结构来管理引用类型的访问。

在这个内部结构中,每个类型比如Console的每个方法都会有一个入口,每个入口都有一个地址(这里叫地址A吧),这个地址A就可以找到方法的实现代码。

而在这个内部数据结构初始化的时候,所有的这些入口的地址都会被设置成一个叫JITCompiler的函数。

CLR在执行这句代码的时候会跑进JITCompiler这个函数中,这是内部操作:

  1. 会跑进托管模块,然后根据元数据匹配类名和方法名,获取你要执行的Console这个类里面Writeline这个函数在IL代码里面的地址B。
  2. 然后编译这段IL代码变成本机的CPU指令放到一块内存空间中,地址为C。(在这个编译之前还会进行验证哦,验证这个IL代码是否安全)
  3. 接着修改元数据中地址A的值变为地址C的值。
  4. 最后跳转到地址C开始执行地址C的cpu指令。

那么如果我们第二次去执行Writeline函数呢?此时内部结构中Writeline函数入口的地址指向的已经是编译好的CPU指令的地址了,所以也就不会去执行JITCompiler这个函数。

千万不要认为这样一定会很慢哦,除了第一次运行时JITCompiler的编译可能需要花费掉编译和优化的时间,后来执行的就是本地CPU指令哦。再加上JITCompiler的这个编译会根据你的机器的CPU不同可能会去生成一些专属于本CPU的特殊指令去优化IL代码,有的这种托管程序可能还要比非托管程序快。

现在我们想想为什么要编译两次?

因为这样的话,无论用C#、VB、F#这些东西你都可以生成一样的包含IL代码的托管程序集,然后这个托管程序集在CLR上运行,也就是说可以混合写代码,一个C#代码可以调用VB代码的DLL,用最适合的语言做最适合的事情。

并且在CLR监视之下执行的IL代码因为在执行前会进行安全校验,所以会提高程序的健壮性和可靠性。

*1.2将托管模块合并成程序集

程序集(assembly)

抽象概念,是一个或多个模块/资源的逻辑性分组,是进行重用、版本控制和应用安全性设置的基本单元,在CLR中,程序集相当于组件

在这里插入图片描述

一般编译器会默认将生成的托管模块生成一个程序集,CLR直接打交道的是程序集(assembly),程序集包含一个或多个托管模块,以及资源文件的逻辑组合。组合过程如下:

在这里插入图片描述

左侧为一些托管模块,在经过一些工具的处理后,生成了一个PE文件,也就是程序集。程序集中有一个**清单(manifest)**数据块,用来描述组成程序集的所有文件。此外,程序集还包含它所引用的程序集的信息,这就使得程序集可以实现自描述。这样CLR就直接知道了程序集所需要的所有内容,因此程序集的部署比非托管组件要容易。

*托管模块和程序集之间的关系是什么?

默认情况下,编译器实际会把生成的托管模块转换为程序集,程序集的**清单(mainfaest)**中会指明该程序集仅有一个文件构成。如果项目中只有一个托管模块,没有资源文件或数据文件,那么程序集就是托管模块。如果想将多个文件合并到一个程序集,则需要使用其他工具来实现。

ps:总结一下,PE就是程序集,其中的组成包含多个托管模块,和资源文件、数据文件,最终在程序集中形成一个**清单(manifest)**数据块,用来描述组成程序集的所有文件。是进行重用、版本控制和应用安全性设置的基本单元

1.3加载公共语言运行时

程序要运行,首先确定机器是否安装.NET框架:运行,输入%windir%/system32,查看目标是否存在MSCorEE.dll文件(微软组建对象运行时执行引擎)。

还可以通过工具CLRVer.exe查看机器上装的所有CLR版本。

加载并初始化CLR的过程:

在这里插入图片描述

*1.4执行程序集的代码

IL是一种面向对象的机器语言,高级语言通常只公开CLR全部功能的一个子集,IL汇编语言允许开发人员访问CLR的全部功能。如果迫切需要的一个CLR功能被隐藏,可以换用IL来编写

*方法执行的过程是什么?首次执行和之后执行有区别吗?

执行一个方法需要将程序集中的IL转换为本地CPU指令,这项工作由CLR的JIT(即时)编译器来完成。用如下例子讲解:

static void Main()
{
    Console.WriteLine("Hello");
    Console.WriteLine("GoodBye");
}

在这里插入图片描述

分析准备阶段:

  1. Main函数执行之前,CLR检测Main代码引用的所有类型,此例中为Console类型;

  2. CLR分配一个内部数据结构,用于管理对引用的类型(即Console类型)的访问。Console类型中的所有方法在这个内部结构中都有一个对应的记录项,Entry入口,每个Entry入口都包含一个地址值,根据地址值即可找到对应的方法实现;对该内部结构初始化时,CLR将每个方法的Entry入口都设置成指向CLR内部的一个未编档函数JITCompiler函数;

执行阶段:

  1. Main方法中首次调用WriteLine方法时,JITCompiler函数会被调用,进入托管模块,然后根据元数据匹配类名和方法名,获取你要执行的Console这个类里面Writeline这个方法在IL代码里面的地址,获取地址下的IL代码

  2. 接着验证这个IL代码是否安全,然后JIT编译器把这段IL代码编译成本机的CPU指令放到一块内存空间中

  3. JITCompiler返回最初为Console类型创建的内部结构,找到WriteLine方法的Entry入口,并将之前设置的指向JITCompiler的引用改为指向保存本地CPU指令的内存块;

  4. JITCompiler回到保存本地CPU指令的内存块,并执行该指令。

二次调用:

由于Console.WriteLine方法已经编译为本地CPU指令了,就会跳过JITCompiler函数的繁琐操作,直接执行本地CPU代码。但当应用程序终止后再次启动运行这段代码,或者同时启动应用程序的两个实例(使用两个不同的进程),JIT编译器必须重新将IL编译为本地CPU指令,因为之前是保存在内存块中的,程序终止后内存块会被清空。

ps:除了第一次运行时JITCompiler的编译可能需要花费掉编译和优化的时间,后来执行的就是本地CPU指令。再加上JITCompiler的这个编译会根据你的机器的CPU不同可能会去生成一些专属于本CPU的特殊指令去优化IL代码,有的这种托管程序可能还要比非托管程序快。

为什么要编译两次?

因为这样的话,无论用C#、VB、F#这些东西你都可以生成一样的包含IL代码的托管程序集,然后这个托管程序集在CLR上运行,也就是说可以混合写代码,一个C#代码可以调用VB代码的DLL,用最适合的语言做最适合的事情。

并且在CLR监视之下执行的IL代码因为在执行前会进行安全校验,所以会提高程序的健壮性和可靠性。

IL有什么优势?

IL可以提高应用程序的健壮性和安全性。在将IL编译为本地CPU指令时,CLR会执行验证过程,确保代码做的一切都是安全的(参数数量是否正确,参数类型是否正确,返回值是否被正确使用等等)

1.4.1IL和验证

IL基于栈。它的所有操作指令都要将操作数栈push进一个执行栈,并从中pop(弹出)结果,且IL指令是无类型的(typeless)的,IL提供了add指令将压入栈的最后两个操作数加到一起,add指令不区分32位和64位,他判断栈中的操作数类型,执行对应的操作

验证:

CLR执行一个名为验证的过程,这个过程会检查IL代码的安全,例如:

(1)调用的每个方法是否有正确数量的参数

(2)传给每个方法的每个参数是否都有正确的类型

(3)每个方法的返回值是否得到了正确的使用

(4)每个方法是否有正确的返回语句

托管模块的元数据包含验证过程需要的所有方法及类型信息

1.4.2不安全的代码

C#编译器默认生成的是安全(safe)代码,这种代码是否安全是可验证的。然而,C#编译器也允许开发人员写不安全(unsafe)代码。

不安全代码允许直接操作内存地址,并可操作这些地址处的字节,通常只有在与非托管代码进行互操作,或在提升效率极高的一个算法的性能时,才会这么做。

MicroSoft提供一个名为PEverify.exe的好、程序,它检查一个程序集的所有方法,并报告其中含有不安全代码的方法。

1.5本机代码生成器NGen.exe

  1. NGen.exe工具,可以在一个程序安装到用户计算机时,将IL代码编译成为本地代码。由于代码在安装时已经编译好,所以CLR的JIT编译器不需要再运行时编译IL代码了,这有助于提升程序的性能。

  2. NGen.exe可以加快程序的启动速度,减少程序的工作集。

  3. NGen.exe生成的文件存在以下问题:

    1)没有知识产权保护。在运行时,CLR要求访问程序集的元数据,这就要求同时发布包含IL代码和元数据的程序集。

    2)NGen生成的文件可能失去同步。NGen生成的文件时,会与当前执行环境相适应的,当你改变了先前的执行环境时,NGen生成的文件就不能使用了。

    3)较差的执行时性能。NGen无法像JIT编译器那么对最终执行环境做出许多优化。

1.6 Framework类库

  1. .NET Framework中包含了Framework类库(Framework Class Library,FCL)。

  2. FCL是一组DLL程序集的统称,其中含有数千个类型定义,每个类型公开一些功能。

1.7 通用类型系统

  1. CLR是完全围绕类型展开的。

  2. 类型为应用程序和其他类型公开了功能。通过类型,用一种编程语言写的代码能与另一种语言写的代码沟通。

  3. 由于类型是CLR的根本,所有MicroSoft指制定了一个正式的规范,即"通用类型系统"(Common Type System,CTS),它描述了类型的定义和行为。

  4. CTS规定,一个类型可以包含一个或者多个成员。比如:字段、方法、属性、事件等。

  5. CTS还指定了类型可视性规则以及类型成员的访问规则。如privae、family(C#:protected)、family and assembly(C#:没有)、assembly(C#:internal)、family or assembly(C#:protected internal)、public

  6. CTS规定所有类型最终必须从预定义的System.Object类型继承。

1.8 公共语言规范

  1. MicroSoft定义了一个"公共语言规范"(Common Language Specification,CLS),它详细定义了一个最小的功能集。任何编译器生成的类型要想兼容于其他"符合CLS、面向CLS的语言"所生成的组件,就必须支持这个最小的功能集。

    在这里插入图片描述

  2. CLS定义了所有语言必须支持的一个最小的功能集。

1.9 与非托管代码的互操作性

1.CLR提供了一些机制,允许在应用程序中同时包含托管代码和非托管代码。具体说,CLR支持三种互操作情形。

1)托管代码能调用DLL中俄非托管函数。托管代码采取一种名为P/Invoke(Platform Invoke)的机制来调用DLL中的包含的函数。

2)托管代码可使用现有的COM组件(服务器)。

3)非托管代码可使用托管类型(服务器)。

比较CLR, CTS和CLS.

CLR是一个可以由多种编程语言使用的“运行时”, 它是围绕类型展开的,类型为应用程序和其他类型公开了功能,通过类型,可以用一种编程语言系的代码和另一种编程语言写的代码进行沟通

CTS则是一个”通用类型系统”,它描述了类型的定义和行为。而由于CLR允许一种语言使用另一种语言定义的类型,但各种编程语言存在极大的区别,例如有些语言区分大小写而有些语言不区分

CTS定义了一套通用的对于编译时的数据类型系统
一个简单的例子:
在Vb.Net中对整形的定义为integer,在c#中对整形的定义为int,经过编译前经过CTS后integer和int 统一变为Int32

在这里插入图片描述

CLS是一个 ”公共语言规范”, 它定义了一个最小功能集,用一种语言定义了一种类型时,若想要其他语言可以使用该类型,就不要在该类型的public和protected成员中使用超出CLS的功能。每种编程语言都提供了CLR/CTS的一个子集以及CLS的一个超集。

可简单描述为统一编码规定或者语法规范
利用CLS在编译是将C#或其他.Net平台的语言编译成为IL,实现通用,反编译就可以形成其他的代码

在这里插入图片描述

*关于PE文件,程序集,托管模块,元数据

(1)无论你引用多少个dll,你的源代码中只使用了几个using,那么最终引用的实际上只有包含这几个using的那些dll文件。

(2)

关于PE文件,程序集,托管模块,元数据:

之前我们说了托管PE文件(也就是那些exe,dll)包括4个部分:PE头、CLR头、元数据和IL代码。

其中PE头是windows要求的标准信息,CLR头是托管模块特有的,包括CLR版本号,模块的入口方法,数字签名,模块内部元数据表的大小和偏移。

元数据是由几个表构成的二进制块,有三种元数据表:定义表,引用表和清单表。

  • 所谓定义表,主要就是对本模块内部的一些属性,方法什么的一个描述。
  • 所谓引用表,主要就是对引用的程序集内部的一些属性,方法什么的一个描述。
  • 所谓清单表,主要就是对程序集组成的那部分文件的信息。

程序集是进行重用、版本控制和应用安全性设置的基本单元。允许有将类型和资源文件划分到单独的文件中。(程序需要加载的程序集数量越少,性能越好,因为这样有助于减小工作集,缓解进程空间地址碎片化)

CLR操作的就是程序集,先加载包含清单元数据表的文件,再根据清单来获取程序集中其它文件的名称。

总结

源代码都会生成托管PE文件,也叫托管模块,就是那些dll和exe,而一个托管模块包含:PE头、CLR头、元数据定义表、元数据引用表和IL代码。

而这些托管模块和一些图片啊什么的资源文件,再加上一个元数据清单表就组合成了程序集(程序集是一个逻辑上的概念)。

一个PE文件也可以仅仅只是一个托管模块,也可以是一个程序集,而区分一个PE文件是托管模块还是程序集的关键就是看它有没有元数据清单表。

那么最通俗的讲,a.EXE引用了b.dll,那么a.exe就是一个程序集,但是b.dll引用了c.dll那么b.dll其实也是一个程序集。

同时a,b,c都是托管PE文件,都是托管模块。

生成、打包、部署和管理应用程序及类型

共享程序集和强命名程序集

CLR支持两种程序集:弱命名程序集和强命名程序集。

两者的区别在于强命名程序集使用发布者的公钥和私钥进行签名。由于程序集被唯一性地标识,所以当应用程序绑定到强命名程序集时,CLR可以应用一些已知安全的策略。

程序集可以采用两种方式部署:私有或者全局。弱命名程序集只能以私有方式部署。

在《CLR via C#》的第三章主要讲了私有部署和全局部署的具体内容,以及弱命名程序集和强命名程序集。

但是老实说一般情况下确实用不到这些东西,所以这里就不写了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值