C# 编译机器码过程原理之再谈反射

一、引言

我们知道在Java中有虚拟机,代码运行时虚拟机把Java语言编译成与机器无关的字节码,然后再把字节码编译成机器指令执行,那么在.NET中程序是如何运行的呢?

其实运行原理是一样的,.NET中的虚拟机是CLR(公共语言运行时),无论是C#程序还是VB程序,首先会由CLR编译成与平台无关的中间语言IL,然后由公共语言运行时CLR的

即时编译器JIT编译成机器代码,再由CPU去执行它。所以说.NET程序也是需要二次编译才能运行,其中涉及的相关术语解释如下:

  1. IL/MSIL (Microsoft Intermediate Language) :微软中间语言 ,IL是MSIL的缩写,译为中间语言,.NET程序下的所有语言都会编译成中间语言,所以他们之间可以相互调用,与语言无关;
  2. CTS (Common Type System):通用的数据类型系统,比如C#调用VS语言程序,虽然他们各自的数据类型定义不一样,但是最终都会转化为通用类型,比如c#中的int,VB语言中的integer,在CLS中都会转化为System.int32,所以这两者之间的程序可以相互调用;
  3. CLS(Common Language Specification):公共语言规范;
  4. CLR (Common Language Runtime):公共语言运行时,也有的叫公共语言运行库;
  5. JIT (Just in time):即时编译器。

 对于计算机来讲,它只认识01010101之类的二进制代码,人类写的高级语言(如C#、JAVA等)计算机是没法识别的,所以需要将高级语言转化为01让计算机可以识别的二进制编码,中间是有一个过程的。就拿C#来讲,VS编译器会将编写好的代码进行编译,编译后会生成exe/dll文件,.Net Core里面已经不生成exe了,都是dll。dll和exe还需要CLR/JIT的即时编译成字节码,才能最终被计算机执行。有伙伴就会问为什么要编译2次呢,先编译到dll,再编译到字节码01呢,为什么不能一次性编译成字节码呢?因为我们写的是C#语言,但是真实运行的机器有很多种,可能是32位,也可能是64位,操作系统可能是windows、linux、unix等,不同的计算机不同的操作系统识别字节码的可能是不一样的,但是从高级语言编译成exe/dll这一步是一样的。所以只要在不同运行环境的计算机上安装对应的不同的CLR/JIT,就可以运行我们同一个exe/dll了。这里就大概讲下这样一个过程,后面会有章节详细讲解程序如何被计算机执行的。现在我们先关注编译生成的exe/dll,它包含2部分,分别是中间语言IL和源数据元数据metadata。IL里面包含我们写的大量的代码,比如说方法、实体类等。元数据metadata不是我们写的代码,它是编译器在编译的时候生成的描述,它可能是把命名空间、类名、属性名记录了一下,包括特性。

CLR VS JVM

以 Java 为例,Java 编译后产生的并不是一个可执行的文件,而是一个 ByteCode (字节码)文件,里面包含了从 Java 源代码转换成等价的字节码形式的代码。Java 虚拟机(JVM)负责执行这个文件。

虚拟机执行中间代码的方式分为 2 种:解释执行和 JIT(即时编译)。解释执行即逐条执行每条指令,JIT 则是先将中间代码在开始运行的时候编译成机器码,然后执行机器码。由于执行的是中间代码,所以,在不同的平台实现不同的虚拟机,都可以执行同样的中间代码,也就实现了跨平台

C# 和 Java 类似,C# 会编译成一个中间语言(CIL,Common Intermediate Language,也叫 MSIL),CIL 也是一个高级语言,而运行 CIL 的虚拟机叫 CLR(Common Language Runtime)。通常我们把 C#、CIL、CLR,再加上微软提供的一套基础类库称为 .Net Framework。

C# 源代码到机器码过程:

1、源代码——2、编译器(vs自带的csc.exe,还有mono的mcs.exe,【java编译器javac.exe】)——3、IL中间语言字节码——4、CLR启动JIT即时编译——5、将IL编译为可以真正在CPU上运行的机器码。

编译时【工具:编译器】:步骤1 到 步骤3,编译源代码

运行时【工具:CLR】:步骤3到步骤5。编译IL

运行时就是代码跑起来了.被装载到内存中去了.(你的代码保存在磁盘上没装入内存之前是个死家伙.只有跑到内存中才变成活的).

中间语言IL

中间语言【IL】有时也称为公共中间语言(CIL:Common Intermediate Language) 或 Microsoft中间语言(MSIL),也可叫做字节码。

之所以叫公共的,可以理解为 就是可以被不同平台【操作系统】上识别,所以才可移植。

IL我们是看不懂的,因为他是二进制的,

我们说的学习并分析IL并不是IL,而是由IL这种汇编语言【相当于.Net平台的汇编语言,类似于Windows平台的汇编语言】编写的代码,我们可以看懂,JIT不能直接运行我们使用IL汇编语言写的代码,JIT运行的是IL汇编语言编译成的IL代码【二进制字节码】,编译器不是编译源码的csc.exe,也不是编译IL为机器码的JIT即时编译器而是,ilasm.exe即IL汇编程序,对应的还有ildasm.exe IL反汇编程序

PE文件

PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)

托管代码

托管代码(C#源代码,VB源代码,F#等,C/C++是非托管)

为什么叫托管呢?

是因为可被编译器编译为中间语言,是受管于CLI式的内存管理和线程安全管理的。

那为什么CLR可以管理呢?

是由CLR启动的JIT编译IL为机器码,这样,CLR就 能确切的知道代码的作用,并可以有效的管理代码。

非托管代码

非托管代码,直接编译成目标计算机码,在公共语言运行库环境的外部,由操作系统直接执行的代码,代码必须自己提供垃圾回收,类型检查,安全支持等服务。如需要内存管理等服务,必须显示调用操作系统的接口,通常调用Windows SDK所提供的API来实现内存管理。

托管代码和非托管代码的区别:

  1、托管代码是一种中间语言,运行在CLR上;非托管代码被编译为机器码,运行在机器上。

  2、托管代码独立于平台和语言,能更好的实现不同语言平台之间的兼容;非托管代码依赖于平台和语言。

  3、托管代码可享受CLR提供的服务(如安全检测、垃圾回收等),不需要自己完成这些操作;非托管代码需要自己提供安全检测、垃圾回收等操作。

字节码和机器码

字节码(Byte-code)是一种包含执行程序,由一序列 op 代码/数据对组成的二进制文件,是一种中间码。字节码文件是一种和任何具体机器环境及操作系统环境无关的中间代码,所以可以移植不同平台执行。

例如:java的虚拟机编译的java字节码,以.class结尾的文件。IL,相当于java的字节码,也是一种中间语言。

机器码

计算机直接使用的程序语言,其语句就是机器指令码,机器指令码是用于指挥计算机应做的操作和操作数地址的一组二进制数。 机器码就是诸如0101010100......的二进制数

1,机器码本质就是一个二进制数位:"0"和“1”。

2,8位单片机如十六制数0x55,机器码:01010101。

3,16位单片机如十六制数0xaaaa,机器码:1010 1010 1010 1010。

4,32位ARM如十六制数0x5555aaaa,机器码:

01010101010101010101010101010101

————————————————

二进制格式【字节码】文件:DB 0F 49 40

【机器码】:1010等二进制数。

CLI

每种变成语言都有一组内置的类型,用来表示如整数、浮点数和字符等之类的对象。过去,这些类型的特征因变成语言和平台的不同而不同。例如,组成整数的位数对于不同的语言和平台就有很大差别。

  然而,这种同一性的缺乏使我们难以让使用不同语言编写的程序及库一起良好协作。为了有序协作,必须有一组标准。

  CLI(Common Language Infrastructure,公共语言基础结构)就是这样一组标准,它把所有.NET框架的组件连结成一个内聚的、一致的系统。它展示了系统的概念和架构,并详细说明了所有软件都必须坚持的规则和约定。

DotNet程序执行流程

.NET 应用程序源代码被编译为 Microsoft 中间语言 (MSIL),也称为中间语言 (IL) 或通用中间语言 (CIL)。.NET 和非 DOTNET 应用程序在编译应用程序时生成程序集。通常,程序集具有 的扩展 .DLL 或 .EXE 基于我们编译的应用程序类型。
例如,如果我们在 .NET 中编译窗口或控制台应用程序,我们会获得类型的程序集.EXE,而当我们在 .NET 中编译 Web 或类库项目时,我们得到一个类型的程序集.Dll。

.NET 和 NON-DOTNET 程序集的区别在于 DOTNET 程序集是中间语言格式,而非 .NET程序集是本机代码格式。

NON.NET 应用程序可以直接在操作系统上运行,因为 NON-DOTNET 程序集包含本机代码,
而 .NET应用程序在称为通用语言运行时 (CLR)的虚拟环境的顶部运行。
CLR 包含一个名为"实时编译器 (JIT)"的组件,该组件将中间语言转换为本机代码,基础操作系统可以理解该代码。

DotNet程序执行步骤

在 .NET 中,应用程序执行包含 2 个步骤
在步骤1中,相应的语言编译器将源代码编译为中间语言(IL),在第二步中,CLR中的JIT编译器将中间语言(IL)代码转换为本机代码,然后可由底层操作系统执行本机代码。 下图显示了上述过程。

由于 .NET 程序集采用中间语言 (IL) 格式,而不是本机代码或计算机代码格式,因此只要目标平台具有通用语言运行时 (CLR),.NET 程序集便可移植到任何平台。目标平台的 CLR 将中间语言代码转换为基础操作系统可以理解的本机代码或计算机代码。

中间语言代码也称为托管代码。这是因为 CLR 管理在它内部运行的代码。例如,在 VB6 程序中,开发人员负责取消分配对象消耗的内存。如果程序员忘记去分配内存,则可能会发生内存异常。另一方面,.NET 程序员不必担心取消分配对象消耗的内存。自动内存管理也称为垃圾回收,由 CLR 提供。由于 CLR 正在管理和执行中间语言,因此它 (IL) 也称为托管代码。

.NET 支持不同的编程语言,如 C#、VB、J# 和C++。C# 、VB 和 J# 只能生成托管代码 (IL),而 C++可以生成托管代码 (IL) 和非托管代码(本机代码)。

关闭程序后,本机代码不会永久存储在任何地方,本机代码将被扔掉。当我们再次执行程序时,将再次生成本机代码。

.NET 程序类似于 java 程序执行。在 Java 中,我们有字节码和 JVM(Java 虚拟机),而在 .NET 中,我们有中间语言和 CLR(通用语言运行时)。

反射加载dll,读取module、类、方法、特性

讲上面程序的编译过程跟反射有什么关系呢?我们反射就是读取metadata里面的数据的,然后去使用它。接下来通过反射的方式去实现调用DSqlServerHelper类中的查询方法Query:

 我们想在Program的Main方法中调用DB.SqlServer类库中SqlServerHelper类中的查询方法Query。以往实现这种功能都是在MyReflection的控制台程序中引用DB.SqlServer和DB.Interface类库,才能调用Query方法

现在我们不引用通过反射的方式

 可以从图中结果看出利用反射成功调用了DSqlServerHelper类下的查询方法Query。对比下最开始添加引用直接new SqlServerHelper()对象然后调取Query方法写的代码更少些,而利用反射需要多写好几行代码,貌似反射更麻烦些,那什么还要用反射呢?如果把using DB.SqlServer注释掉,就会发现new SqlServerHelper()会报错,而不会影响反射写的代码。也就是说反射在不添加引用,不new一个对象的情况下,可以动态的调取对象中的方法,这就是反射的好处。另外觉得这里反射的代码太多了,那是因为没封装,接下来封装下再去调用代码就少很多了。

原文地址:https://www.cnblogs.com/netlock/p/14177564.html