将源程序编译成托管模块
公共语言运行时(CLR)
概念:CLR是一个可由多种编程语言使用的“运行时”。CLR的核心功能(如内存管理、程序集加载、安全性、异常处理和线程同步)可由面向CLR的所有语言使用。如“运行时”使用异常来报告错误,面向它的语言都能通过异常来报告错误。
特性:CLR不关心开发人员使用的哪种语言,只需要编译器时面向CLR的(符合CLR标准)。可用支持CLR的任何语言创建源代码,然后使用对应的编译器检查语法和分析源代码。
标准:无论使用哪个编辑器,结果都是托管模块,托管模块的标准是32位Microsoft Windows可移植执行体(PE32)文件,或者是标准的64位windows可移植执行体(PE32+)文件,它们都需要CLR才能执行。
IL
本机代码编译器生成的都是面向特定CPU架构的代码。面向CLR的编译器生成都是IL(中间语言)代码。IL代码有时称为托管代码,因为CLR管理它的执行。
元数据(metadata)
概念:元数据是一个数据表集合,一些数据表描述了模块中定义了那些内容(比如类型和成员等),另一些描述了模块引用了什么(比如导入的类型及成员)。
用途:元数据总是嵌在和代码相同的EXE/DLL文件中,编译器同时生成元数据和代码,把它们绑定在一起并生成最终的托管模块。
元数据避免了编译对原生C/C++头和库文件的需求,因为在实现类型/成员的IL代码中,包含有关引用类型/成员全部信息。编译器直接从托管模块读取元数据。
VS使用元数据来实现代码提示
CLR进行验证的过程会使用元数据确保类型安全
元数据运行将对象的字段序列化到内存,并到其他机器反序列化完成对象重建状态。
元数据允许垃圾回收器跟踪对象生存期。垃圾回收器能判断任何对象的类型,并从元数据中知道对于引用了那些其他的对象。
1.2 将托管程序合并成程序集
背景:CLR实际上不和模块工作。它和程序集工作。程序集(assembly)是一个或多个模块/资源文件的逻辑性分组。其次,程序集是重用、安全性以及版本控制的最小单元。在CLR中,程序集相当于“组件”。编译器默认将生成的托管模块转换成程序集。
1.3 加载公共语言运行时
生成的程序集既可以是可执行应用程序,也可以是DLL。最终都是由CLR管理这些代码的执行。所以目标机器必须安装.Net Framework。根据我们程序集编译的platform(32位/64位/ARM)不一样,.Net会加载不同版本MSCorEE.dll,进而进程的主线程调用MSCorEE.dll的方法初始化CLR,加载EXE程序集,在调用其Main方法。应用程序启动并运行。
1.4 执行程序集的代码
JIT 编译
为了执行方法,首先必须把方法的IL转换位本机CPU指令,这是CLR JIT(just-in-time “即时”)编译器的职责。
方法首次调用的时候,JITCompiler函数会被调用。JITCompiler函数负责将方法IL代码编译成CPU指令。JITCompiler会在定义(该类型的)程序集的元数据中查找被调用方法的IL,接着JITCompiler验证IL代码,并将IL代码编译成本地CPU指令。本地CPU指令分配到动态的内存块中。然后JITCompiler回到CLR为类型创建的内部数据结构,找到被调用方法对于的那条记录,修改对JITCompiler的引用,使其执行内存块(刚刚编译好的)的地址。最后JITCompiler函数跳转到内存块的代码,执行。
方法二次调用的时候,完全跳过JITCompiler函数,直接执行本机代码
JIT编译将本地CPU指令存储在动态内存中。程序一旦中止,编译好的代码也会被丢弃。再次运行或应用程序启动多个实例,JIT都必须再次将IL编译为本机代码。对于某些引用程序,可能会显著增加内存。相比之下,本机应用程序的只读代码页可由应用程序运行的所有实例共享。
编译指令
CLR的JIT编译器会对本机代码进行优化,类似于非托管C++编译器后端所做的事情。花费较多时间生成优化代码,提高性能。
/optimize-:在C#编译器生成为优化的IL代码中,包含许多NOP(no-operation,空操作)指令。还包含很多跳转下一行代码的分支指令,主要是用于VS加断点调试。
/debug(+/full/pdbonly):编译器会生成Program Database(PDB)文件,PDB文件帮助调试器查找局部变量并将IL指令映射到源代码。/debug:full告诉JIT编译器你打算调试程序集,JIT编译器会记录每条IL指令所生成的本机代码,这样使用VS的时候就可以对对应进程源代码进行调试。
新建C#项目时,项目Debug配置的时/optimize-和/debug:full,而Release配置的是/optimize+和debug:pdbonly
JIT优化
JIT编译编译器在运行时将IL代码编译成本机代码时,编译器对执行环境的认识比非托管编译器更深刻,下面列举托管代码相较于非托管代码的优势。
JIT编译器能判断应用运行的CPU,并生成对于CPU支持的特殊指令,相反,非托管应用程序通常时针对最小功能集合的CPU编译的,不会使用提升性能的特殊指令
JIT编译器能预判一个方法在它对应机器上总是测试失败。如主机只有一个CPU,JIT编译器不会执行需要多个cpu支持的IL代码生成CPU指令,从而减少代码,执行更快。
应用程序运行时,CLR可以评估代码的执行,并将IL重新编译成本机代码,减少不正确的分支预测。虽然目前的版本不支持。。。
预编译
.Net Framewrok SDK配套的NGen.exe工具将程序集的所有IL代码编译成本机代码,并将这些代码保存到一个磁盘文件中。在运行程序集时,CLR自动判断是否存在该程序集的预编译版本。如果有,CLR加载预编译版本,这样一来就避免了运行时编译。但NGen.exe生成的代码时很保存的,不想JIT编译器生成代码那么高度的优化
1.4.1 IL和验证
IL编译成本地CPU指令时,CLR执行一个名为验证的过程。这个过程会检查高级IL代码,确定代码所作的一切都是安全的。比如,核实调用的每个方法都有正确数量以及类型的参数,每个方法都有一个返回语句等。托管模块的元数据包含验证中所用到的方法及类型信息。
Windows的每个进程都有自己的虚拟地址空间,独立地址空间之所以必要是因为不能简单的信任一个应用程序的代码。应用程序完全可以读写无效的内存地址。所有每个进程都放大独立的地址空间,互不干扰,保证程序健壮性与稳定性。通过验证托管代码,可以保证代码不会不正确的访问内存,不会干扰到另一个应用程序的代码。
健壮性与可靠性:可靠性主要描述系统的正确性,也就是你提供一个参数,他能残生稳定可预测的数据。但是如果你程序员执行完成后,没有正确释放内存,或者说系统没有自动帮它释放占用的资源,就认为这个程序“运行时”不健壮
1.4.2 不安全的代码
定义:C#编译器默认生成安全代码,这种代码安全性可以验证,然而C#编译器也运行写不安全代码,不安全代码允许直接操作内存地址,并可操作这些地址处的字节,通常只有在与非托管代码进行交互或提升对效率要求极高的一个算法性能的时候,才这样做。
风险:这种代码可能破坏数据结构,危害安全性,甚至造成新的安全漏洞,所以C#编译器要求包含不安全代码的所有方法都使用unsafe关键字标记。此外 C#编译器要求使用/unsafe编译器开关来编译源代码。
1.7 通用类型系统
CTS(通用类型系统):Microsoft制定的一个正式的规范来描述类型的定义和行为。
- 字段(Field)
作为对象状态一部分的数据变量,用名称和类型来区分 - 方法(Method)
针对对象执行操作的函数,通常会改变对象状态。方法有一个名称、一个签名、一个或多个修饰符。签名制定参数数量及顺序、类型、方法是否有返回值,如果有返回值还需要返回值类型。 - 属性(Property)
属性运行在访问值之前校验输入参数和对象的状态,以及在必要时在计算某个值(getter、setter) - 事件(Event)
事件在对象以及其他相关对象之间实现通知机制。
访问权限:
- private
成员同类访问 - family
成员可由派生类访问 - family and assembly
成员和由派生类访问且必须在同一程序集 - assembly
成员同一程序集访问 - family or assembly
成员可由任何程序集中派生类访问 - public
成员可任意程序集中任何代码访问