CLR简介
CLR(Common Language Runtime)是一个可以由多种语言使用的“运行时",它与具体使用哪种语言无关,只要编译器支持CLR,无论选择什么语言,相应编译器都会将代码编译成一个托管模块,即一个标准的32位PE(Portable Executable)文件或64位PE32+文件,该模块需要CLR才能执行。
CLR的主要功能包括(1)内存管理;(2)程序集加载;(3)安全性;(4)异常处理(5)线程同步。
托管模块以及程序集简介
无论选择哪种编程语言,经过面向CLR的编译器编译后,生成的托管模块是相同的。
托管模块由PE32或PE32+头,CLR头,元数据和中间语言组成。
1. PE32(PE32+)头标识了文件类型(GUI, CUI, DLL),并包含一个文件生成时间的时间标记,对于包含恩弟CPU代码的模块,它还包含与本地CPU代码有关的信息。如果这个头使用了PE32格式,文件能在Windows的32位和64位版本上运行,如果这个头使用了PE32+格式,则文件只能在Windows的64位版本上运行。
2. CLR头包含了使这个模块成为一个托管模块的信息。包括CLR版本,一些flag,托管模块入口方法的MethodDef元数据标记,以及模块的元数据,资源,强名称,一些flag,其他不太重要的数据项的位置和大小。
3. 元数据是一组数据表,主要有两种类型,一种描述了源代码中定义的类型以及成员,另一种描述了源代码中引用的类型和成员。Visual Studio就是使用元数据来为开发人员提供智能感知的,CLR的代码验证过程也是通过使用元数据来确保只执行"类型安全"的操作。元数据还允许将一个对象的字段序列化进内存块,并将其发送给另一台机器,再通过饭序列化重建对象的状态。垃圾回收器也是使用了元数据来跟踪对象的生命周期的。
4. 中间语言(IL)是编译器编译源代码生成的代码,运行时会被CLR编译为本地CPU指令。IL可以提高应用程序的健壮性和安全性,再将IL编译为本地CPU代码时,CLR会执行验证过程,确保代码做的一切都是安全的(参数数量是否正确,参数类型是否正确以及返回值是否被正确使用)。
编译器会同时生成IL和元数据,把它们绑定在一起,并嵌入最终生成的托管模块,所以元数据和它描述的IL永远不会失去同步。
程序集是一个抽象的概念,它是一个或多个模块/资源文件的逻辑分组,也是重用,安全性以及版本控制的最小单元。生成的程序集既可以是一个可执行文件(exe)也可以是一个DLL.
默认情况下,编译器实际会把生成的托管模块转换为程序集,程序集的清单中会指明该程序集仅有一个文件构成。如果项目中只有一个托管模块,没有资源文件或数据文件,那么程序集就是托管模块。如果想将多个文件合并到一个程序集,则需要使用其他工具来实现。
指定应用程序在特定Windows版本运行
我们可以在Visual Studio中设定一个项目目标平台,使生成的程序集只能在32位Windows版本的x86机器上使用或者只能在64位Windows版本的x64机器上使用。
打开项目的属性页面,可以看到目标平台有Any CPU, x86和x64三个选项:
默认为Any CPU, 即生成的程序集可以在任何版本的Windows上运行,在x86 Windows上作为32位应用程序运行,在x64 Windows上作为64位应用程序运行;若将目标平台设置为x86,则在x86 Windows上作为32位应用程序运行,在x64 Windows上作为WoW64应用程序运行;若将目标平台设置为x64, 应用程序只能在x64 Windows上作为64位应用程序运行。
Wow64:Windows 64位版本提供了一个名为Wow64(Windows on Windows64)的技术,允许运行32位Windows应用程序。
程序集代码执行过程
示例代码:
static void Main()
{
Console.WriteLine("Hello");
Console.WriteLine("GoodBye");
}
分析准备阶段:
1. CLR检测Main代码引用的所有类型,此例中为Console类型;
2. CLR分配一个内部数据结构,用于管理对引用的类型(即Console类型)的访问。Console类型中的所有方法在这个内部结构中都有一个入口, 通过该入口可以找到方法的具体实现;对该内部结构初始化时,CLR将每个方法的入口都设置成指向JITCompiler函数(CLR内部未文档化的一个函数);
执行阶段:
1. Main方法中首次调用WriteLine方法时,JITCompiler函数会被调用,这个函数知道调用的是哪个方法(WriteLine),以及什么类型(Console)调用了该方法;
2. JITCompiler在定义了该类型的程序集元数据中查找该方法(WriteLine)的IL,并对IL进行验证,若无误,则将该IL编译为本地CPU指令;
3. 将本地CPU指令保存至一个动态分配的内存块;
4. JITCompiler返回最初为Console类型创建的内部结构,找到WriteLine方法的入口,并将之前设置的指向JITCompiler的引用改为指向保存本地CPU指令的内存块;
5. JITCompiler回到保存本地CPU指令的内存块,并执行该指令。
当Console.WriteLine("Hello")执行结束后,继续执行下一行代码Console.WriteLine("Goodbye"),由于Console.WriteLine方法已经编译为本地CPU指令了,就会跳过JITCompiler函数的繁琐操作,直接执行本地CPU代码。但当应用程序终止后再次启动运行这段代码,JIT编译器必须重新将IL编译为本地CPU指令,因为之前是保存在内存块中的,程序终止后内存块会被清空。
不安全代码
C#编译器默认生成的代码是安全代码,这种代码是否安全是可验证的,然而C#编译器也允许开发人员编写不安全代码来直接操作内存地址,并可操作这些地址处的字节。使用不安全代码风险性比较大,可能破坏数据结构,危害安全性甚至造成新的安全漏洞,鉴于此,C#编译器要求包含不安全代码的所有方法都使用unsafe关键字来标记,同时要求使用unsafe编译器开关来编译源代码。
本地代码生成器:NGen.exe
.NET Framework配套提供的NGen.exe可以将一个应用程序安装到计算机上时将IL代码编译为本地代码,这样CLR的JIT编译器就不需要在运行时编译IL代码,有助于提升应用程序的性能。
运行NGen.exe可以加快应用程序的启动速度,因为运行时不需要再花时间去编译。如果一个程序集会同时加载到多个进程,对程序集运行NGen.exe可以减小应用程序的工作集,NGen.exe将IL编译喂本地代码保存在一个单独文件中,这个文件通过内存映射的方式映射到多个进程地址空间,是代码得到共享,避免每个进程都需要一份单独的代码拷贝。每当CLR加载一个程序集文件时,都会检查是否存在一个对应的由NGen.exe生成的本地文件,若找不到,则像往常一样对IL代码进行JIT编译。
NGen.exe生成的文件存在的问题:
1. 没有知识产权保护;
2. 可能失去同步(CLR加载由NGen生成的文件时,将事先编译好的代码的大量特征与当前执行环境比较,任何一个特征不匹配都无法使用,而诸如对操作系统打补丁等情况都可能改变这些特征);
3. 较差的执行性能(NGen编译代码时无法像JIT编译器一样对最终的执行环境做出许多假设,造成生成较差的代码);
对于服务器端应用程序,NGen几乎无用,因为只有第一个客户端请求才会感受到性能的下降,后续的所有客户端请求都能以全速运行。
CLR, CTS与CLS
CLR是一个可以由多种编程语言使用的“运行时”, 它是围绕类型展开的,类型为应用程序和其他类型公开了功能,通过类型,可以用一种编程语言系的代码和另一种编程语言写的代码进行沟通,而CTS则是一个”通用类型系统”,它描述了类型的定义和行为。而由于CLR允许一种语言使用另一种语言定义的类型,但各种编程语言存在极大的区别,例如有些语言区分大小写而有些语言不区分,CLS是一个 ”公共语言规范”, 它定义了一个最小功能集,用一种语言定义了一种类型时,若想要其他语言可以使用该类型,就不要在该类型的public和protected成员中使用超出CLS的功能。每种编程语言都提供了CLR/CTS的一个子集以及CLS的一个超集。