第一章 CLR的执行模型
1.1 将源代码编译成托管代码
1.2 将托管模块合并成程序集
1.3 加载公共语言运行时
1.4 执行程序集代码
?托管模块->程序集,区别
?如何通过清单、元数据找到相应文件及方法
?win32、win64应用程序区别
1.1 将源代码编译成托管代码
公共语言运行时(Common Language Runtime):是一个由多种编程语言使用的“运行时”。
CLR核心功能包括:内存管理,程序集加载,安全性,异常处理,以及线程同步。
托管模块(managed module):是标准的PE32/32+文件(微软Windows可移植可执行文件,PE32表示32位,32+表示64位),它需要CLR执行。
图1 将源代码编译成托管代码
表1托管代码的各个组成部分
组词部分 |
说明 |
PE32头或PE32+头 |
标准的PE文件头,类似通用对象文件格式头(Common Object File Format,COFF). |
CLR头 |
包含托管模块的信息:需要CLR的版本,一些标志信息,MethodDef元数据(获取托管模块入口方法(Main方法)),以及模块的元数据、资源、强命名。 |
元数据 |
每一个托管模块包含元数据表。有两种主要的类型:1.描述在源码中定义成员的类型。2.描述在源码中引入的成员类型 |
IL(中间语言)代码 |
编译器编译源码产生的代码,在运行时,CLR将IL编译为本地CPU指令 |
元数据由来:元数据是一些老技术的超级。这些老技术包括COM的“类型库”(Type Library)和“接口定义语言”(Interface Definition Language)文件。
与老技术区别:
(1)CLR元数据比他们完整
(2)元数据总是与IL代码的文件关联
元数据的用途:
(1)编译不需要依赖于原生C/C++头及库文件,因为可以直接从托管模块读取元数据
(2)智能提示
(3)CLR用元数据进行代码审核,确保类型安全
(4)允许序列化一个对象的字段为一个内存块,发送到其他机器,然后被反序列化,重建对象状态。
(5)允许GC跟踪对象的生命周期
1.2 将托管模块合并成程序集
CLR不直接运行模块,而是程序集。程序集(Assmebly)是一个抽象的概念:
(1)程序集是一个或多个模块或资源文件的逻辑分组。
(2)程序集是重用,安全性,版本控制的最小单元
根据我们使用的编译器或工具,可以生成一个或多个文件的程序集(默认情况为一个文件)。在CLR世界里,一个程序集就是我们通常所说的“组件”。
图2将托管模块合并成程序集
一些托管模块和资源文件通过一个工具进行处理,生成一个代表文件逻辑分组的PE32(+)文件。这个PE32(+)文件包含了数据块——称为清单。
清单(manifest)是有元数据表构成的另一种集合,包括(这些信息【1】使得程序集能够自描述):
(1)信息 :说明
(2)程序集名称:指定程序集名称的文本字符串。
(3)版本号:主版本号和次版本号,以及修订号和内部版本号。公共语言运行库使用这些编号来强制实施版本策略。
(4)区域性:有关该程序集支持的区域性或语言的信息。此信息只应用于将一个程序集指定为包含特定区域性或特定语言信息的附属程序集。(具有区域性信息的程序集被自动假定为附属程序集。)
(5)强名称信息:如果已经为程序集提供了一个强名称,则为来自发行者的公钥。
(6)程序集中所有文件的列表: 在程序集中包含的每一文件的散列及文件名。请注意,构成程序集的所有文件所在的目录必须是包含该程序集清单的文件所在的目录。
(7)类型引用信息: 运行库用来将类型引用映射到包含其声明和实现的文件的信息。该信息用于从程序集导出的类型。
(8)有关被引用程序集的信息: 该程序集静态引用的其他程序集的列表。如果依赖的程序集具有强名称,则每一引用均包括该依赖程序集的名称、程序集元数据(版本、区域性、操作系统等)和公钥。
1.3 加载公共语言运行时
CLR管理程序集中的代码的执行。这意味着主机上必须安装.NET Framework。
查看是否安装.Net Framework?
(1) 如果机器上安装了.NET Framework,则在%SystemRoot%\System32目录下将包含MSCorEE.dll文件。
(2) 如果一台机器上可以同时安装多个.NET Framework版本,查看注册表:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP
(3) .NET Framework SDK自带的命令行工具CLRVer.exe可以显示安装的所有CLR的版本。该工具还能列出进程所使用的CLR版本。
在极少数情况下,开发者会编写在特殊版本的Windows下运行的代码。通常是在使用unsafe code或需要与面向特定CPU架构的非托管代码进行互操作时。C#编译器提供了一个/platform命令行选项。该选项允许指定结果程序集是运行在x86机器(32位Windows)还是x64机器(64位Windows),或Intel Itanium机器(64位Windows)。如果没有指定平台,默认为anycpu。Visual Studio用户可以在项目属性页中设置项目所面向的平台。
图3使用Visual Studio设置目标平台
表2 /platform开关堆生成的模块的影响以及在运行时的影响
表3 MSCorEE.dll的存储位置
Windows版本 |
MSCorEE.dll版本 |
路径 |
x86 |
x86 |
C:\Windows\System32 |
x64或IA64 |
x86 |
C:\Windows\SysWow64 |
x64 |
C:\Windows\System32(为了向后兼容) |
加载CLR步骤:
(1)在Windows检查EXE文件头并决定是创建32位、64位还是WoW64位进程
(2)Windows将x86、x64或IA64版本的MSCorEE.dll加载到进程的地址空间中
(3)进程的主线程调用MSCorEE.dll中定义的方法(参考22.1 CLR寄宿)
(4)该方法初始化CLR,加载EXE程序集,然后调用它的入口点方法(Main)
(可以在代码中使用Environment的Is64BitOperatingSystem或Is64BitProcess属性来检查是否运行在64位版本的Windows或运行在64位的地址空间中)
如果一个非托管应用程序调用LoadLibrary来加载托管的程序集,Windows将加载并初始化CLR(如果还未加载的话)。当然在该场景中,进程已经创建并运行,这将限制程序集的可用性。如,使用/platform:x86编译的托管程序集不能被64位的进程加载。
1.4 执行程序集代码
IL(Intermediate Language)是独立于CPU的机器语言。可以将IL看成是一门面向对象的机器语言。
从图1,我们可以得知通过高级语言编译器,可以把高级语言C#,VB等编译成IL。ildasm 可以反编译出中间文件来,再用 ilasm 可以再将中间文件编译回 dll。
另外, C#仅仅暴露CLR提供的功能的一个子集,而IL汇编语言运行开发者访问所有CLR的功能。
要执行一个方法,它的IL必须首先转换为本地CPU指令。这是CLR的JIT编译器的工作。
下图展示了一个方法第一次被调用时的情况:
图4 方法的首次调用
在Main执行之前,CLR会检测出Main的代码所引用的所有类型。这导致CLR为每个类型分配一个内部数据结构,每个类型结构,都有方法表,方法表中每个数据项代表一个方法(详情参见4.4 运行时相互关系)。如上图,CLR为Console类创建了一个内部数据结构,方法表中有个数据项为WriteLine(string),它指向一个地址,该方法未被调用之前,他被初始化指向JITCompiler函数。
Main方法首次调用WriteLine方法时,JITCompiler函数会被调用。JITCompiler函数负责将一个方法的IL代码编译成本地代码,如上图所示。
Main第二次调用WriteLine是,由于已编译成本地代码,直接执行内存中的本地代码。如下图所示。
图5 方法的第二次调用
1.4.1 IL和验证
IL是基于堆栈的,这意味着它所有的指令都是将操作数(operands)压入执行栈(execution stack)中,或从栈中弹出结果。
IL指令还是无类型的。例如,IL提供了add指令,对堆栈中最后两个操作数进行相加。没有32位和64位指令的区别。
IL的最大好处不是对底层CPU的抽象,而是对程序健壮性和安全性的改善。在将IL编译为本地CPU指令时,CLR执行一个叫做验证(verification)的过程。verification检查高级别的IL代码,并确保它做的每件事都是安全的。例如,验证过程检查每个被调用的方法的参数数量是否无误、参数类型是否正确、方法的返回值是否可用、每个方法是否都包含return语句,等等。验证过程所使用的所有方法和类型的信息,都存在于托管模块的元数据中。
在Windows中,每个进程都有虚拟的地址空间。将每个Windows进程放置于分离的内存地址中,一个进程就不可能对另一个进程产生负面影响,从而换来健壮性和稳定性。通过验证的托管代码,可确保代码不会访问不正确的内存,不会干扰到另一个应用程序的代码,这样一来,可放心的将多个托管运用程序方到一个Windows虚拟空间中,即CLR提供了一个在单独的进程中执行多个托管程序的功能(详情看22章 CLR寄宿和AppDomain),节省了系统资源。