【深入理解CLR 二】CLR的执行模型(上)

简介: 【深入理解CLR 二】CLR的执行模型(上)

上一篇博文我讲到了如何在整个体系中定位CLR,以及CLR的一些作用,这篇博文是我在消化大佬书第一章之后的一些体会和感悟,为了之后技术分享方便,我会在一些结合我们日常开发的地方,用CLR解释一些现象,方便技术分享的时候大家理解的更加深刻。

博文的行文布局和介绍的部分如下规划

  • 首先我将介绍以下几个概念:源代码,托管模块,程序集
  • 其次我将介绍如下两个过程:Windows如何加载CLR,CLR如何执行程序集
  • 最后我将简单介绍几个相关工具与概念:本机代码生成器,Framework类库,CTS与CLS,与非托管代码的互操作

前两部分是本篇博文核心,详细介绍了CLR的执行模型,第三部分做了一些补充介绍,周边概念相关。另外,文中用到的图片部分来自原书。废话不多说,进入正题。

#CLR的执行模型

类似JAVA,半编译半解释型语言,首先不同种类的源代码会通过不同编译器统一编译为CLR可操作的语言,类似对JVM可操作的class文件一样,然后如同将class文件加载到JVM执行一样,生成的中间文件也会加载到CLR模型执行

所以执行模型的步骤如下:

  1. 第一步当然就是将不同种类的语言编译为中间语言(文件),java是class文件,C#是程序集
  2. 第二步则类似二次编译,将中间语言编译为本机的CPU指令。
    ##编译为CLR可执行
    首先,无论是何种语言,只要是面向CLR的C++/CLI,C#,VB,F#,Iron Python,Iron Ruby,LuaPHPScheme),都可以通过各自的编译器生成托管模块,如下图所示:

    但要知道,CLR并不是面向托管模块,而是面向程序集的,它执行的是程序集文件。所以编译后交给CLR使用的应该是程序集,如下图所示:

以上就是要送交CLR执行之前的整个过程,把握好大局之后,我再来详细介绍下各个概念和特性。

###源代码

源代码,顾名思义,其实就是我们再IDE里敲的一行行代码,是最原始的,没有任何作用,只有通过编译之后才能发挥它们的作用,例如:C#的.cs文件,Java的.java文件,C++的.cpp文件

###托管模块

####文件类型

经过编译之后生成的托管模块是标准的32位Microsoft Windows可移植执行体(PE32)文件或者标准的64位Microsoft Windows可移植执行体(PE32+)文件。

####文件结构

托管模块由以下四部分组成:PE32或PE32+头,CLR头,元数据,IL(中间语言)代码

1,PE32或PE32+头

主要是文件的一些信息,让我们知道这是一个PE32或PE32+文件。

  • 标识文件格式。若头为PE32格式,则文件可以在win32和win64上运行,若为PE32+,则只能在win64上运行,具体原因和使用我之后会在技术分享实例部分详加解释。
  • 标识文件类型。包括GUI(图形用户界面),CUI(个性化用户界面),DLL(动态链接库文件)
  • 标识文件生成时间
  • 对于包含本机CPU代码模块,该头还包括与本机CPU代码有关信息。

注意,本机代码生成的是面向特定CPU架构(x86,x64,ARM)的代码,也就是非托管代码,往往也是不安全代码,关于这两个概念,我后边会提到

2,CLR头

主要是托管模块的一些信息,让我们知道这是一个托管模块。

  • 使此模块成为托管模块信息
  • 要求的CLR版本
  • 一些标志为flag
  • 托管模块入口方法(Main)的MethodDef元数据token以及模块的元数据,资源,强名称,一些标志和不太重要数据项的位置/大小

3,元数据

元数据类似Java里class文件的表结构,感觉这里用元数据描述更加靠谱,描述数据的数据,描述IL代码的数据

  1. 描述源代码中定义的类型和成员的表
  2. 描述源代码引用的类型和成员的表

特别注意,元数据是一些老数据的超集,可以这么理解,元数据超级全面(也只有这么全面的元数据才能描述各种类型吧,个人认为感觉它之所i这么全面就是为了方便CTS使用,这点以及CTS概念后边我会提到。),还有就是,元数据和它所描述的IL代码永远紧密绑定,它们永远不分离,最终被嵌入托管模块。

4,IL(中间语言)代码

中间语言代码,类似java的字节码指令。 编译器编译源代码时生成的代码,运行时被编译为本机CPU指令

####元数据优点

为什么要使用元数据,元数据有哪些优点呢。

  1. 元数据包含了有关引用类型/成员的全部信息,编译器可以直接从托管模块读取元数据。
  2. vs可以通过元数据使用智能感知帮助写代码,代码提示
  3. 元数据允许将对象序列化–传输—反序列化重新生成对象
  4. 元数据允许垃圾回收器跟踪对象生存周期

###程序集

CLR实际上不和模块儿工作,它和程序集工作。

####基本概念

事实上程序集是一个抽象的概念。

1,程序集是一个或多个模块/资源文件的逻辑性分组,特别注意,不是物理分组哦。

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

####组成结构

下图左边的一些托管模块交由工具处理,工具生成代表逻辑分组一个PE32(+)文件。

**注意:**这里说的的是逻辑意义上分组,概念性东西,一组文件可以作为一个单独实体来对待。

程序集由三部分组成:清单数据块,文件或文件集,自描述信息

1,清单数据块

清单也是元数据表的集合,这些表描述了

  • 构成程序集的文件
  • 程序集中文件所实现的公开导出类型(public)
  • 与程序集关联的资源或数据文件

2,文件或文件集

这里的文件或文件集就是清单指出的的构成程序集的文件。为什么有个或呢?

  • 清单指出程序集只由一个文件组成
  • 对于只有一个托管模块,没有资源文件的项目,程序集就是托管模块.
  • 如果需要将一组文件合并到程序集中可以使用程序集链接器(AL.exe)以及其他的一些命令行选项.

3,自描述信息

在程序集的模块中,还包含与引用的程序集有关的信息(版本号、描述等等)。这些信息使程序集能够自描述.也就是说CLR能判断为了执行程序集中的代码,程序集的直接依赖对象是什么.不需要在注册表或者Active Directory Domain Services(ADDS)中注册额外的信息.由于无需额外的信息,所以和非托管组件相比,程序集更容易部署。这也就解释了为什么我们可以通过编译器查看依赖项

####特性与优点

程序集逻辑表示和物理表示区分的好处在于:将很少用到的类型和文件放到单独的文件中,并将这些文件作为程序集的一部分,如果运行时需要,则去下载,这样不仅节省了磁盘空间,还节省了安装时间.通过程序集,可以在不同的地方部署,同时仍然将所有的文件当作一个整体来看待.

具体程序集的工作流程和原理在下一篇博客中我会提到。

##CLR调用执行

上一部分我用较长的篇幅描述了代码如何从源代码变为可由CLR执行的代码。这一部分我会详细说明CLR是如何来面向程序集工作的。结合VS的一些东西来说明问题:

包括我接下来要提到的调试配置(优化代码),平台架构,不安全代码。这些实例设置结合底层的设置来帮助我们了解为什么要做这些设置,后果又是什么,所谓知其然,知其所以然。

###Windows加载CLR

/platform开关选项对于生成的模块影响以及在运行时的影响,下图为对应关系,之后详细解释:

当然,要想用CLR来执行程序集代码,首先得有CLR,CLR由windows加载而来,不同版本的操作系统也需要不同版本的CLR

####创建PE32(+)可执行文件

当我们在编译的时候,可以在vs里平台的地方选择,选择好对应架构会生成对应文件。可以观察上图的左边两列:

选择不同的开关,可以生成对应的托管模块。目前VS好像不提供ARM

####运行可执行文件过程

可执行文件运行的时候分成以下4步:

  1. Windows检查文件头,判断需要32位还是64位地址空间(也就是判断文件是PE32还是PE32+),PE32在32和64位中均可运行,只不过在64位中作为WoW64应用程序运行。PE32+只能在64位版本上运行。这也解释了为什么有的32位程序可以在64位系统上跑了,根源在这儿
    这里解释下:WOW64 (Windows-on-Windows 64-bit)是一个Windows操作系统的子系统, 它为现有的32 位应用程序提供了32 位的模拟,可以使大多数32 位应用程序在无需修改的情况下运行在Windows 64 位版本上
  2. Windows检查头中嵌入的CPU架构信息(x86/x64/ARM),确保当前计算机CPU符合要求。比如说选择ARM开关也生成PE32文件,但它确实不符合32位架构,所以也不能运行

  3. 确定好本机CPU架构和程序作为何种应用程序(32/64)运行后。作出创建何种进程的决定,会在进程空间地址加载对应MSCorEE.dll。
  4. 然后进程的主线程调用MSCorEE.dll定义的一个方法,初始化CLR,加载EXE程序集,调用入口方法,随后托管应用程序启动并允许。所以说当我们点了exe之后,是CLR开启了整个活动。

###CLR执行程序集代码

按照上一小节的流程,我们已经初始化完了CLR,也就是说CLR就位,待使用的托管程序集就位,接下里就是执行过程,也是重头戏。按照书中内容举例,简单如下代码的执行流程:

####执行流程

对接上一节CLR从应用程序的入口程序MAIN方法来执行。我总结了如下流程

CLR初始化类型,做准备工作。

  1. CLR检测Main代码里引用的所有类型。每个引用类型分配一个内部结构,此处为Console类型。
  2. Console类型定义的每个方法都有一个对应的记录项每个记录项都有一个地址,初始化时,每个记录项都被设置为指向CLR内部的一个未编档函数—JITCompiler

JITCompiler函数开始工作(首次调用该方法的时候)

  1. Main方法首次调用WriteLine方法的时候,该JITCompiler函数从实现类型(Console)的程序集的元数据中查找被调用的方法(WriteLine)
  2. 从元数据中查找到被调用方法的IL代码(并且要验证IL代码
  3. 分配内存
  4. 将IL代码编译为本机CPU指令,存储到3中内存------这里将存储本机代码
  5. 在Type表中修改与方法对应的条目,使它指向3分配的内存块
  6. 跳转到内存块中的本机代码

以下为我绘制的流程草图,详细执行流程还得之后学习后再说,这里是个大概流程。

跳过执行 JITCompiler函数(二次调用该方法的时候)

第二次调用该方法的时候,不需要再次编译,直接跳过JIT编译

编译好的代码被丢弃

因为分配的内存是动态内存,所以在以下两种情况的时候,编译好的代码会被丢弃

  • 程序终止,重新运行应用程序
  • 同时启动应用程序的两个实例(使用两个不同的操作系统进程)
相关文章
|
XML 开发框架 JSON
成功实现C++调用C#写的库(CLR),我的个人心得与总结
成功实现C++调用C#写的库(CLR),我的个人心得与总结
2128 0
|
开发框架 .NET Java
【深入理解CLR 一】Net体系结构及CLR在何处
【深入理解CLR 一】Net体系结构及CLR在何处
184 0
|
开发框架 人工智能 自然语言处理
【深入理解CLR 二】CLR的执行模型(下)
【深入理解CLR 二】CLR的执行模型(下)
115 0
|
开发框架 安全 .NET
【深入理解CLR 五】类型基础
【深入理解CLR 五】类型基础
233 0
|
开发框架 .NET C++
|
.NET 开发框架 Windows
|
.NET C# 编译器
[CLR via C#]8. 方法
原文:[CLR via C#]8. 方法 一、实例构造器和类(引用类型)   类实例构造器是允许将类型的实例初始化为良好状态的一种特殊的方法。   类实例构造器方法在"方法定义元数据表"中始终叫.ctor(代表constructor)。
794 0
|
.NET C# 编译器
[CLR via C#]9. 参数
原文:[CLR via C#]9. 参数 一、可选参数和命名参数   在设计一个方法的参数时,可为部分或全部参数分配默认值。然后,调用这些方法的代码时可以选择不指定部分实参,接受默认值。此外,调用方法时,还可以通过指定参数名称的方式为其传递实参。
838 0