1. 将源代码编译成托管模块
事实上,可将编译器视为语法检查器和“正确代码”分析器,它们检查代码,确定你所写的一起都有意义,并输出对你的意图进行描述的代码,编译器的结果都是托管模块(标准的32位Microsoft Windows可移植执行体(PE32)文件或者标准的64位Windows可移植执行体(PE32+)文件)
<托管模块的各个部分>
组成部分 | 说明 |
---|---|
PE32或PE32+头 | 标准Window PE文件头,类似于“公共对象文件格式”头。如果这个头使用PE32格式,文件能在Windows的32或64位版本上运行,如果这个头使用PE32+格式,文件只能在Windows的64位版本上运行。这个头还标识了文件类型,包括GUI,CUI,或者DLL,并包含一个时间标记来支出文件的生成时间。对于只包含IL代码的模块,PE32+头的大多数信息会被忽视。如果只包含本机(native)CPU的代码的模块,这个头包含CPU代码有关的信息 |
CLR头 | 包含使这个模块成为托管模块的信息(可由CLR和一些实用程序解释)。头中包含要求的CLR版本,一些标志(flag),托管模块入口方法(Main方法)的MethodDef元数据token已经模块的元数据,资源,强名称,一些标志及其他不太重要的数据项的位置大小 |
元数据 | 每个托管模块都包含元数据表。主要有两种表:一种表描述源代码中定义的类型和成员,另一种描述源代码引用的类型和成员 |
IL(中间语言)代码 | 编译器编译源代码时生成的代码。在运行时,CLR将IL编译成本机CPU指令 |
由于编译器同时生成元数据和代码,吧他们绑定在一起,并嵌入最终生成的托管模块,所以元数据和它描述的IL代码永远不会失去同步
元数据有多种用途,下面列举部分
- 元数据避免了编译时对原生C/C++头和库文件的需求,因为在实现类型/成员的IL代码文件中,已包含有关引用类型/成员的全部信息。编译器直接从托管模块读取元数据
- MIcrosoft Visual Studio用元数据帮助你写代码。“智能感知(IntelliSense)”技术会解析元数据,告诉你一个类型提供了哪些方法,属性,事件和字段,对于方法,还能告诉你需要的参数
- CLR的代码验证过程使用元数据确保代码只会执行“类型安全”的操作
- 元数据允许将对象的字段序列化到内存块,将其发送给另一台电脑,然后反序列化,在远程机器上重建对象状态
- 元数据允许垃圾回收器跟踪对象生存周期。垃圾回收器能判断任何对象的类型,并从元数据知道那个对象中的哪些字段引用了其他对象
2. 将托管模块合并成程序集
程序集是抽象概念,是一个或多个模块/资源的逻辑性分组,也是重用,安全性以及版本控制的最小单元。利用程序集,一组文件可作为一个单独的实体对待。可以在不同的地方部署文件,同时仍然将所有文件作为一个整体对待
<将托管模块合并成程序集>
3. 加载公共语言运行时
要知道是否已安装.NET Framework,只需检查C:\Windows\System32目录中的MSCorEE.dll,存在则表明已安装
C#编译器提供了一个/platform命令行开关选项,这个选项允许指定最终生成的程序集在某个平台允许,如果不指定具体平台的话。默认选项就是anycpu
Visual studio用户要想设置目标平台,可以打开项目的属性页,从“生成”选项卡的“目标平台”列表中选择一个选项
</platform开关选项对生成的模块的影响以及在运行时的影响>
/platform开关 | 生成的托管模块 | x86 Windows | x64 Windows | ARM Windows RT |
---|---|---|---|---|
anycpu(默认) | PE32/任意CPU架构 | 作为32位应用程序运行 | 作为64位应用程序运行 | 作为32位应用程序运行 |
anycpu32bitpreferred | PE32/任意CPU架构 | 作为32位应用程序运行 | 作为WoW64应用程序运行 | 作为32位应用程序运行 |
x86 | PE32/x86 | 作为32位应用程序运行 | 作为WoW64应用程序运行 | 不运行 |
x64 | PE32+/x64 | 不运行 | 作为64为应用程序运行 | 不运行 |
ARM | PE32/ARM | 不运行 | 不运行 | 作为32位应用程序运行 |
Windows检查EXE文件头,决定是创建32位还是64位进程后,会在进程地址空间加载MSCorEE.dll的x86,x64或ARM版本
4.执行程序集的代码
高级语言通常只公开CLR全部功能的一个子集,IL汇编语言允许开发人员访问CLR的全部功能
<方法的首次调用>
在Windows不同版本中运行将会得到不同指令(x86,x64,ARM)
<方法的第二次调用>
由于已对WriteLine的代码进行了验证和编译,所以会直接执行代码块中的代码,完全跳过JITCompiler函数,方法仅在首次调用时才会有一些性能损失
CLR的JIT编译器会对本机代码进行优化,两个C#编译器开关会影响代码优化
编译器开关设置 | C# IL代码质量 | JIT本机代码质量 |
---|---|---|
/optimize-/debug-(默认) | 未优化 | 有优化 |
/optimize-/debug(+/full/pdbonly) | 未优化 | 未优化 |
/optimize+/debug(-/+/full/pdbonly) | 有优化 | 有优化 |
使用/optimize-,在C#编译器生成的未优化IL代码中,将包含许多NOP(no-operation,空操作)指令,还包含许多跳转到下一行代码的分支指令,Visual studio利用这些指令在调试期间提供“编辑并继续(edit-and-continue)”功能
托管代码相较于非托管代码的优势
- JIT编译器能判断应用程序是否运行在Intel Pentium 4 CPU上,并生成相应的本机代码来利用Pentium 4支持的任何特殊指令。相反非托管应用程序通常是针对具有最小功能集合的CPU编译的,不会使用能提升性能的特殊指令
- JIT编译器能判断一个特定的测试在它运行的机器上是否总是失败。例如,假定一个方法包含以下代码
if(nunberOfCPU>1){
...........
}
如果主机只有一个CPU,JIT编译器不会为上述代码生成任何CPU指令。在这种情况下本机代码将针对主机进行优化,最终代码会变得更小,执行的更快
IL基于栈。这意味者它的所有指令都要将操作数压入(push)一个执行栈,并从栈弹出(pop)结果。将IL编译成本机CPU指令时,CLR执行一个名为验证(verification)的过程。这个过程会检查高级IL代码,确定代码所做的一切都是安全的。托管模块的元数据包含验证过程要用到的所有方法及类型信息。
Microsoft C#编译器也允许开发人员写不安全的(unsafe)代码。不安全的代码允许直接操作内存地址,并可操作这些地址处的字节。C#编译器要求包含不安全代码的所有方法都用unsafe关键字标记,除此之外,C#编译器要求使用/unsafe编译器开关来编译源代码。Microsoft提供一个名为PEVerify.exe的实用程序,它检查一个程序的所有方法,并报告其中含有不安全代码的方法。
5.本机代码生成器:NGen.exe
使用.Net Framework提供的NGen.exe工具,可以在应用程序安装到用户的计算机上时,将IL代码编译成本机代码。
NGen.exe编译的优缺点
优点
- 提高应用程序的启动速度
- 减小应用程序的工作集
缺点
- 没有知识产权保护
- NGen生成的文件可能失去同步
- 较差的执行时性能
6.Framework类库
.Net Framework包含Framework类库(Framework Class Library,FCL)是组DLL程序集的统称,其中包含数千个类型定义,每个类型都公开一些功能。
部分应用程序
- Web服务(Web service)
- 基于HTML的Web窗体/MVC应用程序(网站)
- “富”Windows GUI应用程序
- Windows控制台应用程序
- Windows服务
- 数据库存储过程
- 组件库
许多类型都允许自定义其行为,你只需从所需的FCL类型派生出自己的类型再进行自定义即可。
7.通用类型系统
通用类型系统(Common Type System,CTS),CTS规范规定,一个类型可以包含零个或多个成员。
- 字段(Field)
作为对象状态一部分的数据变量。字段根据名称和类型来区分 - 方法(Method)
针对对象执行操作的函数,通常会改变对象状态 - 属性(Property)
属性运行在访问值之前校验输入参数和对象状态 - 事件(Event)
事件在对象以及其他相关对象之间实现通知机制
CTS还指定可见性规则和类型成员的访问规则
- private
成员只能由同一个类(Class)类型中的其他成员访问 - family
成员可由派生类访问,不管是否在同一个程序集中。许多语言(比如C++和C#)都用protected修饰符标识family - family and assembly
成员可由派生类访问,但这些派生类必须在同一个程序集中定义。许多语言(比如C++和C#)都没有提供这个访问控制,但IL汇编语言不在此列 - assembly
成员可由同一个程序集中的任何代码访问。许多语言都用internal修饰符来标识assembly - family or assembly
成员可由任何程序集中的派生类访问。成员也可由同一个程序集中的任何类型访问。C#用protected internal修饰符标识family or assembly - public
成员可由任何程序集中的任何代码访问
CTS规定所有类型最终必须从预定义的System.Object类型继承。System.Object类型允许
- 比较两个实例的相等性
- 获取实例的哈希代码
- 查询一个实例的真正类型
- 执行实例到的浅(按位)拷贝
- 获取实例对象当前状态的字符串表示
8.公共语言规范
Microsoft定义了“公共语言规范”(Comman Language Specification,CLS),他详细定义了一个最小功能集。任何编译器只有支持这个功能集,生成的类型才能兼容由其他符合CLS,面向CLR的语言生成的组件。
<CLR,CTS,CLS相对关系>
9.与非托管代码的互操作性
CLR支持三种互操作情形
- 托管代码能调用DLL中的非托管函数
托管代码通过P/Invoke(Platform Invoke)机制调用DLL中的函数 - 托管代码可以使用现有COM组件(服务器)
可参考 .NET Framework SDK提供的TlbImp.exe工具 - 非托管代码可以使用托管类型(服务器)
可参考 .NET Framework SDK提供的TlbExp.exe和RegAsm.exe工具