Fork me on GitHub

CLR via C# 中各种基础概念的理解

汉字都认识,但各种术语组成段落后就懵了,根本原因还是很多术语的概念不了解。在这里把总是影响理解的概念和术语总结罗列出来,不断更新、添加、修改。以便读书时更加顺畅。

using System;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("Hello World!");
        }
    }
}

源代码

就是我们在各种文本编辑器里写的那些代码,小白都能认识的if else Console.Write(),源代码对人类友好,对机器不算友好,可以通过编译器编译成机器码让CPU去运行。原代码的另一个好处是同一份源代码,可以编译成不同的机器码,在不同的硬件上执行相同的操作。


PE 可移植的执行体 Portable Executable

桃花雪博客原文裢接 - linux,windows 可执行文件(ELF、PE)

通俗理解

PE翻译成中文是:可移植执行体执行体就是说这个文件可以被运行,可以在物理CPU上跑起来。可移植,是说可以在不同的CPU上运行(64位PE的不能在32位上运行),微软的windows底层帮我们屏蔽了不同CPU硬件的差异(不同的CPU支持的指令集都是不同的,如果程序员还要挨个去适配不同版本代、不同型号的CPU的话,想想就够了)。只要在微软的windows系统上,PE文件就可以跑,不用管装的什么CPU。(Linux系统上对应的是ELF(Executable Linkable Format))。

那PE到底是什么?

简单说PE是一种文件格式,是COFF(Common file format)格式的变种。啥是文件格式? 格式就是一种规范,一种人为的规定。就像早期的邮政信封一样,左上方填收件人邮编,右上方粘邮票,中间上面写收件人地址、姓名,中间下面写寄件人地址、姓名。右下角写寄件人邮编。这就是一种规定的格式。只要寄信的人遵循这个标准,信息都填对,就可以寄出去,而不用操心这封信是坐的飞机还是牛车。如果不遵循这个格式呢? 你在信封上画一幅到达收件人位置的路线地图拿到邮局,工作人员会一个白眼翻到后脑勺:滚!(windows会说:此程序无法在XXX运行
我们程序里写的函数、类、变量等等数据,会被编译成遵循PE格式规范的文件,我也半懂不懂,好像是各种C语言代码:指针、结构体那些东西组成的。

常见的PE文件后缀

常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)
PE文件是指32位可执行文件,也称为PE32。64位的可执行文件称为PE+或PE32+,是PE(PE32)的一种扩展形式(请注意不是PE64)。

更多详细信息参考 kabeor的博文 - PE文件简介
PE文件解析 基础篇 PE文件解析 基础篇


托管代码 IL(中间语言)(Intermediate Language)

参考文章1了解IL语言(Intermediate Language)
参考文章2中间语言MicroSoft Intermediate Language(MSIL)

  • 解释1: IL是.NET框架中中间语言(Intermediate Language)的缩写。使用.NET框架提供的编译器可以直接将源程序编译为.exe或.dll文件,但此时编译出来的程序代码并不是CPU能直接执行的机器代码,而是一种中间语言IL(Intermediate Language)的代码
  • 解释2: MS中间语言在 .Net Framework中有非常重要的作用,面向.Net的所有语言逻辑上都必须支持IL。
  • 解释3:《C#入门经典(第3版)》 中的描述: 在编译使用 .NET Framework 库的代码时,不是立即创建操作系统特定的本机代码,而是把代码编译为Microsoft中间语言(Microsoft intermediate language, MSIL)代码,这些代码不专用于任何一种操作系统,也不专用于C#。其他.NET语言,如Visual Basic .NET 也可以在第一阶段编译为这种语言,当使用VS开发C#应用程序时,编译过程就由VS完成。
  • 解释4: 中间语言是一组独立于CPU的指令集,它可以被即时编译器Jitter翻译成目标平台的本地代码。中间语言代码使得所有Microsoft.NET平台的高级语言C#,VB .NET,VC.NET等得以平台独立,以及语言之间实现互操作。

本机代码(Native Code)

被编译为特定于处理器的机器码的代码。

通俗说: 本机 特指当前运行程序的这台,这台机装的是什么CPU,这个CPU支持什么指令集,代码就是机器码,是不是01二进制不清楚。本机代码,就是一堆可以直接在当前CPU上直接运行的代码。这坨代码放在别的CPU上可能会水土不服


.NET应用程序的执行过程

原文地址: 编译输出的PE文件的执行过程

graph LR
原代码--VS-->IL代码
IL代码--JIT-->本机代码
本机代码--直接执行-->CPU

我们对"HelloWorld.cs"文件用csc.exe命令编译后发生了什么。是的,我们得到了HelloWorld.exe文件。但那仅仅是事情的表象,实际上那个HelloWorld.exe根本不是一个可执行文件!那它是什么?又为什么能够执行?

好的,下面正是回答这些问题的地方。首先,编译输出的HelloWorld.exe是一个由中间语言(IL),元数据(Metadata)和一个额外的被编译器添加的目标平台的标准可执行文件头(比如Win32平台就是加了一个标准Win32可执行文件头)组成的PE(portable executable,可移植执行体)文件,而不是传统的二进制可执行文件--虽然他们有着相同的扩展名。中间语言是一组独立于CPU的指令集,它可以被即时编译器Jitter翻译成目标平台的本地代码。中间语言代码使得所有Microsoft.NET平台的高级语言C#,VB.NET,VC.NET等得以平台独立,以及语言之间实现互操作。元数据是一个内嵌于PE文件的表的集合。元数据描述了代码中的数据类型等一些通用语言运行时(Common Language Runtime)需要在代码执行时知道的信息。元数据使得.NET应用程序代码具备自描述特性,提供了类型安全保障,这在以前需要额外的类型库或接口定义语言(Interface Definition Language,简称IDL)。

这样的解释可能还是有点让人困惑,那么我们来实际的解剖一下这个PE文件。我们采用的工具是 .NET SDK Beta2自带的ildasm.exe,它可以帮助我们提取PE文件中的有关数据。我们键入命令"ildasm /output:HelloWorld.il HelloWorld.exe",一般可以得到两个输出文件:helloworld.il和helloworld.res。其中后者是提取的资源文件,我们暂且不管,我们来看helloworld.il文件。我们用"记事本"程序打开可以看到元数据和中间语言(IL)代码,由于篇幅关系,我们只将其中的中间语言代码提取出来列于下面,有关元数据的表项我们暂且不谈:

class private auto ansi beforefieldinit HelloWorld extends [mscorlib]System.Object
{
  .method public hidebysig static void  Main() cil managed
  {
    .entrypoint
    // Code size       11 (0xb)
    .maxstack  8
    IL_0000:  ldstr      "Hello World !"
    IL_0005:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000a:  ret
  } // end of method HelloWorld::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // Code size       7 (0x7)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  ret
  } // end of method HelloWorld::.ctor

} // end of class HelloWorld

我们粗略的感受是它很类似于早先的汇编语言,但它具有了对象定义和操作的功能。我们可以看到它定义并实现了一个继承自System.Object 的HelloWorld类及两个函数:Main()和.ctor()。其中.ctor()是HelloWorld类的构造函数,可在"HelloWorld.cs"源代码中我们并没有定义构造函数呀--是的,我们没有定义构造函数,但C#的编译器为我们添加了它。你还可以看到C#编译器也强制HelloWorld类继承System.Object类,虽然这个我们也没有指定。关于这些高级话题我们将在以后的讲座中予以剖析。

那么PE文件是怎么执行的呢?下面是一个典型的C#/.NET应用程序的执行过程:
  1. 用户执行编译器输出的应用程序(PE文件),操作系统载入PE文件,以及其他的DLL(.NET动态连接库)。
  2. 操作系统装载器根据前面PE文件中的可执行文件头跳转到程序的入口点。显然,操作系统并不能执行中间语言,该入口点也被设计为跳转到mscoree.dll(.NET平台的核心支持DLL)的CorExeMain()函数入口。
  3. CorExeMain()函数开始执行PE文件中的中间语言代码。这里的执行的意思是通用语言运行时按照调用的对象方法为单位,用即时编译器将中间语言编译成本地机二进制代码,执行并根据需要存于机器缓存
  4. 程序的执行过程中,垃圾收集器负责内存的分配,释放等管理功能。
  5. 程序执行完毕,操作系统卸载应用程序。

清楚的知晓编译输出的PE文件的执行过程是深度掌握C#语言编程的关键,这种过程的本身就诠释着C#语言的高级内核机制以及其背后Microsoft.NET平台种种诡秘的性质。


JIT(Just In Time)

参考1:什么是JIT,写的很好-JAVA
参考2:JIT原理 - 简介 - 知乎
参考3:CLR和JIT的理解、.NET反汇编学习

  1. 动态编译(dynamic compilation)指的是“在运行时进行编译”;与之相对的是事前编译(ahead-of-time compilation,简称AOT),也叫静态编译(static compilation)。
  2. JIT编译(just-in-time compilation)狭义来说是当某段代码即将第一次被执行时进行编译,因而叫“即时编译”。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价;但要注意广义与狭义的JIT编译所指的区别。
  3. 自适应动态编译(adaptive dynamic compilation)也是一种动态编译,但它通常执行的时机比JIT编译迟,先让程序“以某种式”先运行起来,收集一些信息之后再做动态编译。这样的编译可以更加优化。

JIT:即时编译(Just In-Time compile),这是.NET运行可执行程序的基本方式,也就是在需要运行的时候,才将对应的IL代码编译为本机指令。传入JIT的是IL代码,输出的是本机代码,所以部分加密软件通过挂钩JIT来进行IL加密,同时又保证程序正常运行。同解释执行的代码相比,JIT的执行效率要高很多。


托管模块(Managed Module)

托管模块是一个需要CLR才能执行的标准WindowsPE(Portable executable,简称PE)文件。


元数据(MetaData)

参考1: 阮一峰老师的博客-元数据(MetaData)
参考2: 微软官方文档-元数据和自描述组件
参考3: 知乎回答-什么是元数据?为何需要元数据?

首先要知道“”是什么。元(meta),一般被我们翻译成“关于……的……”。元数据:关于数据的信息/数据,元标签:关于标签的信息/数据是用来描述一个东西各个方面的信息表。比如我的个人简历,就是描述我的元数据。招聘网站上有十万个应聘者的简历,他们都要求应聘者提供姓名、性别、年龄等信息。这样的信息表格,就是这些求职者的元数据。这些元数据(简历)的内容,可以描述一个应聘者的画像。

那.Net的元数据,是在描述什么?是关于什么的信息?

.Net的元数据,是描述当前程序集的数据。我们编译一个helloworld.exe的程序,在编译成PE文件的过程中,编译器除了将代码转换为 Microsoft 中间语言 (MSIL)外,还生成了一些数据对当前这个helloworld程序进行描述, 在模块或程序集中定义和引用的每个类型和成员都将在元数据中进行说明。比如会记录helloworld这个程序集定义了哪些类型,有哪些方法,添加了哪些引用等等等等。当执行代码时,运行时将元数据加载到内存中,并引用它来发现有关代码的类、成员、继承等信息。

所以(敲黑板),元数据就是.net让程序有了自描述的能力,我们写的程序在运行的时候,能够让系统、其它程序能够通过我们的元数据(本程序的"简历")知道:我是谁?我在哪?我在干什么?

vs这个宇宙最强编译器,它的智能提示,就利用到了元数据。当我们输入一个类再加一个点,就会自动提示有哪些方法和属性可以使用。这些信息就是从元数据中翻出来给我们的。

元数据】以非特定语言的方式【描述】在【代码中】定义的每一个【类型和成员】。 元数据存储以下信息:

又长又拗口的解释,必须要手工断句一下...
元数据     描述     代码中     的     类型和成员

  • 程序集的说明。
    • 标识(名称、版本、区域性、公钥)。
    • 导出的类型。
    • 该程序集所依赖的其他程序集。
    • 运行所需的安全权限。
  • 类型的说明。
    • 名称、可见性、基类和实现的接口。
    • 成员(方法、字段、属性、事件、嵌套的类型)。
  • 特性。
    • 修饰类型和成员的其他说明性元素。

程序集 Assembly

参考1: C语言中文网-C#/.NET程序集详解

  • 在写完代码之后进行生成(build)时,CLR 将 .NET 应用程序打包为由模块(module)组成的程序集(assembly)。
  • 一个程序集由一或多个托管模块组成,程序代码被编译为 IL 代码,存在于托管模块之中。
  • 程序集是一个可以寄宿于 CLR 中的、拥有版本号的、自解释、可配置的二进制文件,程序集的扩展名为 exe 或 dll。
  • 程序集中的代码可以被其他程序集中的 C# 代码调用,例如几乎所有的 C# 代码都会用到 mscorlib.dll 这个程序集中的对象。
  • 程序集是自解释的,因为它记录了它需要访问的其他程序集(在清单中)。
  • 另外,元数据描述了程序集内部使用的类型的所有信息,包括类型的成员和构造函数等。
  • 程序集可以私有或共享的方式配置,如果以共享方式进行配置,则同一台机器的所有应用程序都可以使用它。
  • 程序集也可以手动进行生成,这需要选择对应语言的编译器。
  • C# 的编译器是csc.exe。可以通过 /t 参数指定编译目标,最常见的几个为:
    • /t:library:目标为一个 dll 文件(需要指定托管模块)。
    • /t:exe:目标为一个可执行 exe 文件,必须指定入口点。
    • /t:module:目标为一个托管模块。

    其中,前两个目标的结果文件都是程序集,而最后一个目标的结果文件是托管模块。

下图简单显示了各种代码和编译器之间的关系。

.Net代码与编译器的关系


CLR(Common Language Runtime)公共语言运行时

参考1: CLR和JIT的理解、.NET反汇编学习

CLR:通用语言运行时(Common Language Runtime)的简称,CLR是.NET框架的核心内容之一,可以把它看为一套标准资源,可以呗任何.NET程序使用。它包括:面向对象的编程模型、安全模型、类型系统(CTS)、所有.NET基类、程序执行及代码管理等。

我们可以这样理解,CLR是托管程序运行的环境,就像Windows是普通的PE程序的运行环境一样。
在Windows中,整个CLR系统的实现基本其实就是几个关键的DLL,比如mscorwks.dll、mscorjit.dll,它们共同的特点就是前缀均为mscor。

在win32中,可执行文件在开始运行时,有操作系统载入到内存中,然后运行文件中的.text代码,结束时有操作系统负责卸载。而在.NET下,这个过程却不大一样。用PE结构查看工具(这里使用PEiD)载入一个实例文件,观察导入表,可以发现整个表只引入了一个mscoree.dll中的一个方法:_CorExeMain。而这个方法,真是该可执行文件在win32意义上的入口点。

和win32程序一样,.NET可执行程序在运行初始,首先有Windows将PE载入内存,然后跳至_CorExeMain中执行。分水岭就在这,当代码跳至_CorExeMain中之后,程序运行进入了.NET的初始化阶段,经过一番准备工作,.NET框架便正式接管程序的运行了。

posted @ 2020-04-24 23:22  meedo  阅读(447)  评论(0)    收藏  举报