《NET CLR via C#》笔记---第一章(CLR,托管模块,程序集,JIT,AOT,IL,FCL,CTS,CLS)
公共语言运行时(Comon Languague Runtitle, CLR)
是一个可由多种编程语言使用的“运行时”,CLR的核心功能(例如内存管理、程序集加载、安全性、异常处理和线程同步)可由面向CLR的所有语言使用,事实上在运行时,CLR根本不关心用哪种语言写源代码,只要编译器是面向CLR的,例如:
- C++/CLI
- C#
- Visual Basic
- F#
- IL
需要注意的是,高级语言通常只公开了CLR全部功能中的一个子集(IL汇编语言开发人员访问CLR的全部功能)。所以,如果你选择的编程语言隐藏了所需要的一个CLR功能,可以换用IL汇编语言或者提供所需功能的另一种语言来实现。
CLR使得“混合语言编程”成为开发项目一个指得慎重考虑的选择。下面列一下C++与C#交互的具体例子:
首先,编写一个简单的C++动态链接库(DLL),其中包含一个函数用于演示:
#include <iostream>
// 导出函数
extern "C" __declspec(dllexport) int Add(int a, int b)
{
return a + b;
}
编译这个C++代码生成DLL文件,比如命名为Example.dll。
接下来,在C#中使用P/Invoke来调用这个C++ DLL中的“Add”函数:
using System;
using System.Runtime.InteropServices;
namespace ConsoleApp1
{
class Program
{
// 声明来自DLL的函数
[DllImport("Example.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int Add(int a, int b);
static void Main(string[] args)
{
int result = Add(3, 5);
Console.WriteLine($"3 + 5 = {result}");
}
}
}
详细步骤:
- 编写C++代码:编写并编译C++代码生成DLL文件。确保使用extern "C"和__declspec(dllexport)来导出函数。
- 编写C#代码:在C#代码中使用[DllImport]属性声明从DLL导入的函数,并调用这个函数。
- 运行C#代码:确保C++生成的DLL文件在C#项目的输出目录(如bin/Debug或bin/Release)中,以便C#程序能够找到并加载该DLL。
编译源码

图1-1展示了编译源码文件的过程:
- 可用支持CLR的任何语言创建源代码文件
- 用对应的编译器检查语法和分析源代码
- 最终会生成托管模块,托管模块是标准的32位Microsoft Windows可移植执行体(PE32)文件,或者是标准的64位Windows可移植执行体(PE32+)文件
托管模块可用简单看作是exe文件,例如我新开个C#应用工程

点击重新生成解决方案,会在对应的bin/Debug目录下生成PE文件(ConsoleApp1.exe)

验证方法,可用Microsoft自带的ildasm.exe工具(目录在C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.2 Tools下),该工具专门用来反汇编PE文件,打开ildasm后,将ConsoleApp1.exe文件拖入ildasm中,可以看到看看写的反汇编代码

要注意的点是,如果exe文件不含有CLR头,则ildasm文件会报错,提示“XXX没有有效的CLR头,无法反汇编”,附下托管模块各个部分的截图

托管模块
每个面向CLR的编译器生成的都是IL(中间语言,托管代码)代码,除了生成IL,还会生成完整的元数据(metadata),元数据描述了模块中定义的类型和成员,以及模块引用的类型和成员。
元数据总是与包含IL代码的文件关联(元数据总是嵌入和代码相同的EXE/DLL文件中),这使得二者密不可分。由于编译器同时生成元数据和代码,把它们绑定一起,并嵌入到最终生成的托管模块,所以元数据和它描述的IL代码永远不会失去同步。
Microsoft的C#,Visual Basic,F#和IL汇编器总是生成包含托管代码(IL)和托管数据(可进行垃圾回收的数据类型)的模块。这些模块需要用户安装好CLR(目前作为.NET Framework的一部分提供)
Microsoft的C++编译器则默认生成包含非托管(native)代码的EXE/DLL模块,并在运行时操纵非托管数据(native内存)。这些模块则不需要CLR即可执行。
程序集
CLR实际不和模块工作,它和程序集工作。程序集是一个或多个模块/资源文件的逻辑性分组。其次,程序集是重用、安全性以及版本控制的最小单元。取决于你选择的编译器或工具,既可以生成单文件程序集,也可以生成多文件程序集。在CLR的世界中,程序集相当于“组件”

图中一些托管模块和资源文件准备交由一个工具处理。工具生成,代表文件逻辑分组的一个PE32(+)文件,这个PE32(+)文件包含了一个名为清单(manifest)的数据块。清单也是元数据表的集合。这些表描述了构成程序集的文件、程序集中的文件所实现的公开(public)导出的类型以及与程序集关联的资源或数据文件。
编译器模块将生成的托管模块转换成程序集,即,C#编译器生成的是含有清单的托管模块。
对于只有一个托管模块而且无资源文件的项目,程序集就是托管模块,生成过程中无需执行任何额外的步骤。但是,如果希望将一组文件合并到程序集中,就必须利用更多的工具。
生成的每个程序集既可以是可执行的应用程序,也可以是DLL(其中含有一组可执行程序使用的类型)。
JIT编译
为了执行方法,首先必须把方法的IL转换成本机(native)CPU指令。这是CLR的JIT(just-in-time)编译器的职责。

- 在Main方法执行前,CLR会检测出Main的代码引用的所有类型,这导致CLR分配一个内部数据结构来管理对引用类型的访问。
- 图1-4的Main方法引用了一个Console类型,导致CLR分配了一个内部数据结构。在这个内部数据结构中,Console类型定义的每个方法都有一个对应的记录项,每个记录项都含有一个地址,根据此地址即可找到方法的实现。
- 对这个内部数据结构初始化时,CLR将每个记录项都设置成(指向)包含在CLR内部的一个未编档函数 。该函数被称为JITCompiler。
- Main方法首次调用WriteLine时,JITCompiler函数会被调用。JITCompiler函数负责将方法的IL代码编译成本机CPU指令。由于IL是“即时”(just-in-time)编译,所以通常将CLR这个组件称为JITer或者JIT编译器。
- JITCompiler函数被调用时,它知道要调用的是哪个方法,以及具体是什么类型定义了该方法。然后,JITCompiler会在定义(该类型的)程序集的元数据中查找被调用方法的IL。
- 接着,JITCompiler验证IL代码,并将IL代码编译成本机CPU指令,本机CPU指令保存到动态分配的内存块中。
- 然后,JITCompiler回到CLR为类型创建的内部数据结构,找到与被调用方法对应的那条记录项,修改最初对JITCompiler的引用,使其指向内存块(其中包含了刚才编译好的本机CPU指令)的地址。
- 最后,JITCompiler函数跳转到内存块中的代码,这些代码正是WriteLine方法的具体实现,代码执行完毕并返回时,会回到Main中的方法,并像往常一样继续执行。
(博主的话:7,8应该是编译成本机代码并缓存,这样第二次调用不需要重新JIT编译)

- 现在,Main要第二次调用WriteLine。这一次,由于已对WriteLine的代码进行了验证和编译,所以会直接执行内存块中的代码,完全跳过JITCompiler函数。WriteLine执行完毕后,会再次回到Main。
方法仅在首次调用才会有一些性能损失。以后对该方法的所有调用都以本机代码的形式全速运行,无需重新验证IL并把它编译成本机代码。
JIT编译器将本机CPU指令存储到动态内存中。这意味着一旦应用程序终止,编译好的代码也会被丢弃。还要注意,CLR的JIT编译器会对本机代码进行优化,以C#为例,2个C#编译器开关会影响代码优化:/optimize和/debug。
使用/optimize-,在C#编译器生成的未优化IL代码中,将包含许多NOP(no-operation,空操作)指令,还包含许多跳转到下一行代码的分支指令。Visual Studio利用这些指令在调试期间提供“编辑并继续”功能。另外,利用这些额外的指令,还可在控制流程指令(比如for,while,do,if,else,try,catch和finally语句块)上设置断点,使代码更容易调试。
优化后IL代码,C#编译器会删除多余的NOP和分支指令。代码会难以进行调试,不过优化后的IL代码变得更小,结果EXE/DLL文件也更小,且会更易读。
另外,只有指定/debug(+/full/pdbonly)开关,编译器才会生成Program Database(PDB)文件。PDB文件帮助调试器查找局部变量并将IL指令映射到源代码。/debug:full开关告诉JIT编译器你打算调试程序集,JIT编译器会记录每条IL指令所生成的本机代码。这样一来,就可利用Visual Studio的“即时”调试功能,将调试器连接到正在运行的进程,并方便对源码进行调试。
AOT
.NET Framework SDK配套提供了NGen.exe工具。该工具将程序集的所有IL代码编译成本机代码,并将这些本机代码保存到一个磁盘文件中。在运行加载程序集时,CLR自动判断是否存在该程序集的预编译版本。如果是,CLR就加载预编译代码。这样就避免了运行时进行编译。
不过,NGen.exe对最终执行环境的预设是很保守的,所以,NGen.exe生成的代码不会像JIT编译器生成的代码那样进行高度优化,下面是NGen.exe的使用流程。
安装和准备
- 确保.NET Framework安装:ngen.exe是.NET Framework的一部分,因此需要确保系统上安装了相应版本的.NET Framework
- 找到ngen.exe,通常位于.NET Framework的安装目录中,例如:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\ngen.exe(对于32位)C:\Windows\Microsoft.NET\Framework64\v4.0.30319\ngen.exe(对于64位)
使用ngen.exe预编译程序集
假设你有一个名为MyApp.exe的C#应用程序,以下是如何使用ngen.exe预编译它的步骤:
- 编译并生成C#应用程序:
创建一个简单的C#应用程序,编译生成MyApp.exe:
using System;
class Program
{
static void Main()
{
Console.WriteLine("Hello, World!");
}
}
- 使用ngen.exe生成本机映像:
打开命令提示符,运行以下命令(假设当前目录包含MyApp.exe):
C:\Windows\Microsoft.NET\Framework\v4.0.30319\ngen install MyApp.exe
除了NGen.exe以外,还可以考虑System.Runtime.ProfileOptimization类,该类导致CLR检查程序运行时哪些方法被JIT编译,结果被记录到一个文件。程序再次启动时,如果是在多CPU机器上运行,就用其他线程并发编译这些方法。这使得程序运行得更快,因为多个方法并发编译,而且是在应用程序初始化时编译,而不是用户和程序交互时才“即时”编译。
使用NGen.exe工具,可以在应用程序安装到用户计算机上时,将IL代码编译成本机代码。由于代码已经编译好,所以CLR的JIT编译器不需要在运行时编译IL代码,NGen.exe能在以下两种情况下发挥重要作用。
- 提供应用程序的启动速度
因为代码已经编译成本机代码,运行时不需要再花时间编译。 - 减小应用程序的工作集
如果一个程序集同时加载到多个进程中,对该程序集运行NGen.exe可减小应用文件的工作集。NGen.exe将IL编译成本机代码,并将这些代码保存到单独的文件中。该文件可以通过“内存映射”的方式,同时映射到多个进程地址空间中,使代码得到了共享,避免每个进程都需要一份单独的代码拷贝。
NGen.exe会将生成的新文件放到%SystemRoot%\Assembly\NativeImages_v4.0.#####_64这样一个目录下的一个文件夹中。(v4.0表示CLR版本号,64表示64位Windows编译)
NGen.exe,一方面,获得了托管代码的所有好处(垃圾回收、验证、类型安全等);另一方面,没有托管代码(JIT编译)的所有性能问题。但它并非完美,同样有其他问题。
- 也不具有知识版权
在运行时,CLR要求访问程序集的元数据(用于反射和序列化功能),这就要求发布包含IL和元数据的程序集。此外,如果CLR因为某些原因不能使用NGen生成的文件,CLR会对程序集的IL代码进行JIT编译,所以IL代码必须处于可用状态。 - NGen生成的文件可能失去同步
CLR加载NGen生成的文件时,会将预编译代码的许多特征与当前执行环境进行比较。任何特征不匹配,NGen生成的文件就不能使用。此时要改为使用正常的JIT编译器进程。下面列举了必须匹配的部分特征。
- CLR版本:随补丁或Service Pack改变
- CPU类型:升级处理器发生改变
- Windows操作系统版本:安装新Service Pack后改变
- 程序集的标识模块版本ID(MVID):重新编译后改变
- 引用的程序集的版本ID:重新编译引用的程序集后改变
- 安全性:吊销了之前授予的权限后,安全性就会发生改变。权限包括:声明性继承(declarative inheritance)、声明性链接时(declarative link-time)、SkipVerification或者UnmanagedCode权限
- 较差的执行时性能
编译代码时,NGen无法像JIT编译器那样对执行环境进行许多假定。这会造成NGen.exe生成较差的代码。测试表明,相较于JIT编译的版本,NGen生成的某些应用程序在执行时反而要慢5%左右。
IL和验证
IL基于栈。这意味着它的所有指令都要将操作数压入(push)一个执行栈,并从栈弹出(pop)结果。
IL指令还是“无类型(typeless)”的。例如,IL提供了add指令将压入栈的最后2个操作数加到一起。add指令不分32位和64位版本。add指令执行时,它判断栈中操作数的类型,并执行恰当的操作。
将IL编译成本机CPU指令时,CLR执行一个名为验证(verification)的过程。这个过程会检查高级IL代码,确定代码所做的一切都是安全的。例如,会核实调用的每个方法都有正确数量的参数,传给每个方法的每个参数都有正确的类型,每个方法的返回值都得到了正确的使用,每个方法都有一个返回语句,等等。托管模块的元数据包含验证过程中要用到的所有方法及类型信息。
Microsoft C#编译器默认生成安全(safe)代码,这种代码的安全性可以验证。然后Microsoft C#编译器也允许开发人员写不安全(unsafe)代码,不安全代码允许直接操作内存地址,并可操作这些地址处的字节。
然而,使用不安全的代码存在重大风险:这种代码可能破坏数据结构,危害安全性,甚至造成新的安全漏洞。下面是unsafe代码的示例:
- 包含不安全代码的所有方法都用unsafe关键字标记。
using System;
class Program
{
unsafe static void ModifyArray(int[] numbers)
{
// 使用 fixed 关键字将变量固定在内存中,以确保垃圾回收器(GC)不会在使用指针时移动该变量
// 具体而言,当你需要操作托管堆中的对象,使用fixed关键字可以将这些对象固定在内存中的特定位置,从而获取其地址并进行安全的指针操作
// 直到fiexe语句块结束前,会暂停停止移动这些对象,着确保在使用指针操作对象时,内存地址是有效和安全的
fixed (int *p = numbers)
{
*p = 10;
}
}
static void Main(string[] args)
{
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
ModifyArray(numbers);
foreach(var num in numbers)
{
Console.WriteLine(num);
}
}
}
- 在项目设置中启用不安全代码
在解决方案资源管理器中右键点击你的项目,选择“属性”。
在项目属性窗口中,选择“生成”选项卡。
勾选“允许不安全代码”复选框。
当JIT编译器编译一个unsafe方法时,会检查该方法所在的程序集是否被授予了System.Security.Permissions.SecurityPermission权限,且System.Security.Permissions.SecurityPermissionFlag的SkipVerification标志是否被设置。
Microsoft提供了一个名为PEVerify.exe的实用程序,它检查一个程序集的所有方法,并报告其中含有不安全代码的方法。下面提供PEVerify.exe使用方法:
- PEVerify.exe 通常随 .NET SDK 安装。可以在 Visual Studio 的安装目录中找到它。例如:
C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.2 Tools - 打开cmd,并导航PEVerify.exe的目录,使用以下命令验证程序集:
PEVerify.exe <path-to-your-assembly> - 以我上面Program文件为例,Debug目录输出可执行文件后,调用PEVerify.exe查看

Framework类库(FCL)
.NET Framework包含Framework类库(Framework Class Library,FCL)。FCL是一组DLL程序集的统称,其中含有数千个类型定义,每个类型都公开了些功能。
通用类型系统(CTS)
CLR一切都围绕类型展开。类型向应用程序和其他类型公开了功能。通过类型,用一种编程语言写的代码能与另一种编程语言写的代码沟通。由于类型是CLR的根本,所以Microsift制定了一个正式的规范来描述类型的定义和行为,这就是“通用类型系统”(Common Type System,CTS)
- CTS规范规定,一个类型可以包含零个或者多个成员,例如:
- 字段
- 方法
- 属性
- 事件
- CTS还指定了类型可见性规则以及类型成员的访问规则
- public
- private
- protected
- CTS规定:所有类型都必须从预定义的System.Object类型继承
公共语言规范(CLS)
不同语言创建的对象可以通过COM相互通信。CLR则集成了所有语言,用一种语言创建的对象在另一种语言中,利用后者创建的对象具有相同地位。之所以能实现这样的集成,是因为CLR使用了标准类型集、元数据以及公共执行环境。
要创建很任意从其他编程语言中访问的类型,只能从自己语言中挑选其他所有语言都支持的功能。因此,Microsoft定义了“公共语言规范”(Common Language Specification,CLS),它详细定义了一个最小功能集。任何编译器只有支持这个功能集,生成的类型才能兼容由其他符合CLS、面向CLR的语言生成的组件。



浙公网安备 33010602011771号