CLR(Common Language Runtime)公共语言进行时是一个可由多种编程语言使用的“进行时”。
- 将源代码编译成托管模块
可用支持CLR的任何语言创建源代码文件,然后用对应的编译器检查语法和分析源代码。无论选择哪个编译器,结果都是托管模块(managed module)。托管模块是标准的32位Microsoft Windows可移植执行体(PE32)文件,或者是标准的64位Windows可移植执行体(PE32+)文件,他们都需要CLR才能执行。(注:PE是Portable Executable(可移植执行体)的简称)
本机代码编译器(native code compilers)生成的是面向特定CPU架构(比如x86,x64或ARM)的代码。相反,每个面向CLR的编译器生成的都是IL(中间语言)的代码。
除了生成IL面向CLR的编译器还要在每个托管模块中生成完整的元数据(metadata)。元数据简单地说就是一个数据表集合。一些数据表描述了模块中定义了什么(比如类型及其成员),另一些描述了模块引用了什么(比如导入的类型及成员)。
Microsoft的C++编译器默认生成包含非托管(native)代码的exe/dll模块,并在运行时操作非托管数据(native内存)CLR即可执行。然而,通过指定/CLR命令行开关,C++编译器就能生成包含托管代码的模块。当然,最终用户必须安装CLR才能执行这种代码。在前面提到的所有Microsoft编译器中,C++编译器是独一无二的,只有它才允许开发人员同时写托管和非托管代码,并生成到同一个模块中。它也是唯一允许开发人员在源代码中同时定义托管和非托管数据类型的Microsoft编译器。
- 将托管模块合并成程序集
CLR实际不和模块工作。它和程序集工作。
首先,程序集是一个或多个模块/资源文件的逻辑分组。其次,程序集是重用、安全性以及版本控制的最小单元。
图中一些托管模块和资源(或数据)文件准备交由一个工具处理。工具生成代表文件逻辑分组的一个PE32(+)文件。实际发生的事情是,这个PE32(+)文件包含一个名为清单(mainfest)的数据块。清单也是元数据表的集合。这些表描述了构成程序集的文件、程序集中的文件所实现的公开导出的类型以及与程序集关联的资源或数据文件。(注:所谓公开导出的类型,就是程序集中定义的public类型,它们在程序集内部外部均可见。)
编译器默认将生成的托管模块转换成程序集。也就是说,C#编译器生成的是含有清单的托管模块。清单指出程序集吸由一个文件构成。对于只有一个托管模块而且无资源(或数据)文件的项目,程序集就是托管模块,生成过程中无需执行任何额外的步骤。但是,如果希望将一组文件合并到程序集中,就必须撑握更多的工具(比如程序集链接器AL.exe)及其命令行选项。
- 加载公共语言运行时CLR
可执行文件(exe)运行时,Windows检查EXE文件头,决定是创建32位还是64位进程之后,会在进程地址空间加载MSCorEE.dll的x86,x64或ARM版本。如果是Windows的x86或ARM版本,MSCorEE.dll的x86版本在%SystemRoot%\System32目录中。如果是Windows的x64版本,MSCorEE.dll的x86版本在%SystemRoot%\SysWow64目录中,64位版本则在%SystemRoot%\System32目录中(为了向后兼容)。然后,进程的主线程序调用MSCorEE.dll中定义的一个方法。这个方法初始化CLR,加载EXE程序集,再调用其入口方法(Main)。随即,托管应用程序启动并运行。(PS:微软在64位系统中将所有处理32位程序的工具都放在SysWow64目录下,Wow就是Windows on Windows的意思。而System32目录是处理64位程序的。还叫32,只是延续了以前的叫法,其实应该是64)
- 执行程序集的代码
开发人员一般用c#,VB等高级语言进行编程。它们的编译器将生成IL。然而,和其他任何机器语言一样,IL也能使用汇编语言编写,Microsoft甚至专门提供了名为ILAsm.exe的IL汇编器和名为ILDasm.exe的IL反汇编器。注意,高级语言通常只公开了CLR全部功能的一个子集。然而IL汇编语言允许开发人员访问CLR的全部功能。要知道CLR具体提供了哪些功能,唯一的办法是阅读CLR文档。
为了执行方法,首先必须把方法的IL转换成本机(navive)CPU指令。这是CLR的JIT(just-in-time或者"即时")编译器的职责。
就在Main方法执行之前,CLR会检测出Main的代码引用的所有类型。这导致CLR分配一个内部数据结构来管理对引用类型的访问。图中Main方法引用了一个Console类型,导致CLR分配一个内部结构。在这个内部数据结构中,Console类型定义的每个方法都有一个对应的记录项。每个记录项都含有一个地址。根据此地址即可找到方法的实现。对这个结构初始化,CLR将每个记录项都设置成(指向)包含在CLR内部的一个未编档函数。我将该函数称为JITCompiler。
Main方法首次调用WriteLine时,JITCompiler函数会被调用。JITCompiler函数负责将方法的IL代码编译成本机CPU指令。由于IL是”即时“(just in time)编译的,所以通常将CLR的这个组件称为JITter或者JIT编译器。
JITCompiler函数被调用时,它知道是要调用的是哪个方法,以及具体是什么类型定义了该方法。然后,JITCompiler会在定义(该类型的)程序集的元数据中查找被调用方法的IL。接着JITCompiler验证IL代码,并将IL代码编译成本机CPU指令。CPU指令保存到动态分配的内在块中。然后 ,JITCompiler回到CLR为类型创建的内部数据结构,找到与被调用方法对应的那条记录,修改最初对JITCompiler的引用,使其指向内在块(其中包含了刚才编译好的本机CPU指令)的地址。最后,JITCompiler函数中跳转到内存块中的代码。这些代码正是WriteLine方法(获取单个String参数的那个版本)的具体实现。代码执行完毕并返回时,会回到Main中的代码,并像往常一样继续执行。
现在,Main要第二次调用WriteLine。这一次,由于已对WriteLine的代码进行了验证和编译,所以会直接执行内存块中的代码,完全跳过JITCompiler函数。WriteLine方法执行完毕后,会再次回到Main。
- IL和验证
将IL编译成本机CPU指令时,CLR执行一个名为验证(verification)的过程。
CLR确实提供了在一个操作系统中执行多个托管应用程序的能力。每个托管应用程序都在一个AppDomain中执行。每个托管EXE文件默认都在它自己的独立地址空间中运行,这个地址空间只有一个AppDomain。然而,CLR的宿主进程(比如IIS或者Microsoft SQL Server)可决定在一个进程中运行多个AppDomain。
- 本机代码生成器:NGen.exe
使用用.NET Framework提供的NGen.exe工具,可以在应用程序安装到用户的计算机上时,将IL代码编译成本机代码。由于代码在安装时已经编译好,所以CLR的JIT编译器不需要在运行时编译IL代码,这有助于提升应用程序的性能。NGen.exe能在以下两种情况下发挥重要作用。
1提高应用程序的启动速度
2减少应用程序的工作集(所谓工作集,是指在进程的所有内存中,已映射的物理内存那一部分(即这些内存全在物理内存中,CPU可以直接访问);进程还有一部分虚拟内存,它们可能在转换列表中(CPU不能通过虚拟地址访问,需要Windows映射之后才能访问);还有一部分内存在磁盘上的分页文件里。)
NGen.exe生成的文件有以下问题
1没有知识产权保护
2NGen生成的文件可能失去同步
3较差的执行时性能
- Framework类库
FCL(Framework Class Library)
- 通用类型系统
Microsoft制定了一个正式规范来描述类型的定义和行为,这就是“通用类型系统”(Common Type System,CTS)。
- 公共语言规范
要创建很容易从其他编程语言中访问的类型,只能从自己 的语言中挑选其他所有语言都支持的功能。为了在这个方面提供帮助,Microsoft定义了“公共语言规范”(Common Language Specification,CLS),它详细定义了一个最小功能集。任何编译器只有支持这个功能集,生成的类型才能兼容由其他符合CLS、面向CLR的语言生成组件。
(个人的理解:CLS是为了不同编程语言之间互相调用而设计的,如果只用一种语言,就不用考虑CLS的规范)
(说明:文中99%内容来自书本原文。把知识要点搬运到这里,只是为了方便本人复习、查阅)