转载:http://www.tracefact.net/CLR-and-Framework/DotNet-Framework.aspx
.NET框架
三年前写的《.NET之美》的第六章,现在书名改为了《.NET专题解析》。
本书是一本讲解.NET技术的书籍,目标读者群也是在.NET框架(.NET Framework)下进行开发的程序员,因此我们无法回避的问题就是:什么是.NET框架?它包含了哪些内容?为开发程序提供了哪些支持?很多朋友对这 类个问题的第一反应可能是.NET框架所提供的庞大类库及编写代码所采用的C#语言,实际上远不止这些。
要描述.NET框架,自然会遇到与其相关的一系列专业的技术术语和缩写,相信大家已经见到过许多了,比如:CLI、CIL、CTS、CLS、 CLR、JIT、BCL、FCL、Module、Assembly 等,足以让很多人一头雾水、望而却步。笔者不会像字典一样按首字母排序对术语进行逐一解释,因为这样还是难以理解。我们还是从大家最熟悉的东西开始吧!
6.1 引子
设想一下:编写下面这样一个最简单的显示“Hello, World!”的控制台程序,并将该程序运行起来需要哪几个步骤呢?
using System;
class Program {
static void Main(string[] args) {
string text = "hello, world!";
Console.WriteLine(text);
}
}
这些步骤包括:打开Visual Studio,创建一个C#控制台应用程序项目(在这里将它命名为ConsoleApp),编写代码,编译程序然后运行。虽然这样的程序谁都会写,但是再多进行一下思考就会发现,尽管是一个很小的程序,但已经引入了.NET框架的几个重要方面。
如果创建一个VB.NET类型的项目,实现和上面C#项目完全一样的功能,那么编译后生成的文件有什么区别?
编写控制台应用程序,将字符输出到屏幕,需要调用Console.WriteLine()方法。这个Console类型从何而来呢?
生成的文件在系统中是如何运行起来的?其机制和使用传统VC++生成的可执行文件是否相同?
其实,上面每一个问题的答案都包含.NET框架所提供的支持,这里将它分为三个部分:
- 对于编译后生成的文件格式和内容,.NET中存在着诸多规范。符合这些规范的程序语言,也叫做面向.NET的语言。编译后生成的文件都可以在.NET运行时下执行,这就是大家所熟知的.NET多语言支持。
- 在开发阶段,.NET提供了一个庞大的类库,支持开发者快速开发各种应用程序,也支持程序语言设计者开发其语言编译器。
- 在程序执行阶段,.NET提供了一个程序运行时的环境,这个运行时环境帮助我们管理内存、实时编译程序、进行安全检查、执行垃圾回收等。
接下来就针对上述内容开始为大家详细讲述。
6.2 CIL——公共中间语言
首先要了解的就是C#程序源码在编译之后会得到什么样的一个文件。大家知道,过去使用VC++生成的可执行文件,经过预编译、编译、汇编、链接几个 步骤后,最终生成的可执行文件中就已经包含了处理器的本地代码(Native Code),支持它运行的只是操作系统和本地的机器指令集。那么采用C#编译器生成的文件又是什么呢?现在需要引入程序集这个概念:在.NET框架下,类 似C#这样的高级语言经过编译后生成的结果文件被称做程序集,其后缀名是.dll(类库)或.exe(可执行程序)。在引入这个概念之前,前面(上一节) 提到程序集时,都是用“文件”这个词来描述的。
程序集的定义只是给编译后生成的文件一个稍微正式一点的名称,对于解释“它是由什么构成的”这个问题并没有太大的帮助。为了进一步了解程序集,我们 再来做一个试验,使用VB.NET创建一个控制台应用程序项目(ConsoleAppVB),并生成一个程序集,代码功能和上面用C#创建的项目是一样的 的。
Module Program
Sub Main()
Dim text AsString = "hello, world !"
Console.WriteLine(text)
EndSub
EndModule
现在,需要一个工具来查看这个程序集的内容,并且与C#项目生成的程序集进行对比。还好,微软已经提供了一个利器——IL DASM(IL Disassembler,IL反汇编程序)来帮助开发者查看程序集的信息。如果安装了Visual Studio,IL DASM将会随同Visual Studio一起安装。依次选择开始菜单→ Microsoft Visual Studio 2010 → Microsoft Windows SDK Tools →IL 反汇编程序(IL DASM)可以启动IL DASM。
打开IL DASM后选择VB.NET项目生成的ConsoleAppVB.exe,可以看到如图6-1所示的界面。
图6-1 IL DASM 运行界面
这部分内容很多,会在下一章“程序集”中进行专门讲述,,这里暂且略过。展开图6-1中的ConsoleAppVB.Program类型,在 Main()方法上双击,会弹出另外一个窗口,显示图6-2中的代码,看上去有点像汇编语言。在这里可以看到熟悉的string text变量声明及“hello, world !”。
图6-2 方法体的CIL语言描述(VB.NET)
接下来再打开C#项目生成的ConsoleApp.exe,进行同样的操作,在打开Main()方法后会发现其中的代码与图6-2中几乎完全一样,如图6-3所示
图6-3方法体的CIL语言描述(C#)
至此,可以得到一个初步的推断:不管是VB.NET还是是C#,编译之后的程序集都能够用IL DASM打开,因此它们生成的程序集的格式都是相同的;当程序所实现的功能相同时,程序集所包含的CIL代码也是类似的。
现在对上面程序集中所包含的类似汇编的语言做一下介绍,即是本节标题中的CIL(Common Intermediate Language,公共中间语言)。CIL最初是随着.NET由微软一起发布的,因此之前也叫做MSIL(Microsoft Intermediate Language),后来进行了标准化,之后便被称做CIL。在一些书或文章中,CIL也会简写为IL,其实都是指同样的东西。为了避免混淆,本书统一用 CIL这个缩写。
我们可以将上面的过程用图6-4来表示出来。
图6-4 源程序编译为了程序集
接下来再深入地分析一下,公共中间语言这个术语到底包含了哪几层含义。
- 公共。因为不论是C#语言也好,VB.NET语言也好,C++/CLI语言也好,甚至是重新开发的一套以自己的名字缩写命名的语言,只要它期望运行的目标平台是.NET,在经过相应的编译器编译之后,所生成的程序集就是由CIL语言代码描述的。
- 中间。这个词也是大有深意,为什么不叫公共机器语言(Common Machine Language),或者公共本地语言(Common Native Language)?因为这种语言只是比我们使用的高级语言,比如C#低级一点,并不是CPU可以直接执行的本地机器语言。这种语言还需要.NET运行时 (.Net runtime)环境的支持,在执行之前,进行一个被称为Just-in-time(即时)的二次编译过程,才能转变成计算机可以识别的指令。关 于.NET运行时,以及详细过程后面再介绍,现在只要知道,这个文件所包含的CIL代码并非机器可以直接执行的指令代码。
- 语言。CIL不过是一种程序语言,只不过相对于C#来说,它是一种更低级语言。从图6-2 的代码截图中,已经可以看到,CIL是一种基于堆栈的语言,同时,它提供了class、interface、继承、多态等诸多面向对象的语言特性,因此它 又是完全面向对象的语言。如果愿意,甚至可以直接编写CIL代码,并且使用CIL的编译工具IL ASM(IL Assembler,IL汇编程序)来对它进行编译。只不过,和大多数低级语言一样,这种方式会使开发效率会变得很低。这里注意区别一下IL ASM和IL DASM,它们的拼写是不同的。
为了加深一下印象,我们来做一个试验:编写一段简单的CIL代码,并且使用IL ASM工具对其进行编译,得到和前面一样的ConsoleApp.exe程序。
1)打开记事本程序,输入下面的代码,然后将其保存在D:ConsoleApp.il。
.assembly extern mscorlib{}
.assembly ConsoleApp{}
.module ConsoleApp.exe
.class public auto ansi Program extends System.Object
{
.method public static void Main()
{
.entrypoint
nop
ldstr "Hello, World!"
call void [mscorlib]System.Console::WriteLine(string)
nop
ret
}
}
2)打开Visual Studio 2010命令行工具,输入:
D:>ilasm ConsoleApp.il
3)成功后会看到ConsoleApp.exe程序,它的执行结果和上面用C#编写的完全一样。
由于程序集是由CIL语言所描述的,因此CIL也叫做程序集语言(Assembly Language)。又因为.NET程序集需要由.NET运行时加载才能运行,可以视其为由.NET运行时进行管理的,所以CIL代码也叫做托管代码 (Managed Code)。相对的,不需要.NET运行时就可以执行的代码就叫做非托管代码(Unmanaged Code)。
好了,已经知道了CIL的存在,从现在开始,最好在头脑里建立起两个模型或两种视角:一种是基于C#或其他高级语言的源程序的视角,一种是基于 CIL中间语言的程序集视角。C#源程序在被编译为程序集以后,就独立于C#,因此程序集可以由其他种类的语言所调用;同时,因为程序集并没有包含本地机 器的指令,所以它与具体的机器类型也分隔开了,可以被装有.NET框架的任何机器运行。
6.3 BCL和FCL
6.3.1 BCL——基类库
我们先来看一个有意思的现象:再次打开前面创建的C#控制台项目(ConsoleApp),然后在解决方案面板下打开“引用”文件夹,如果用的是Visual Studio 2010,并且面向的目标框架是.NET 4.0版本,那么将会看到如图6-5所示的这些引用。
图6-5 解决方案中的“引用”文件夹
在创建项目时并没有做任何额外的操作,那么这些引用显然是在创建项目时自动添加的。为了方便初学者,这里稍微解释一下:要使用(实际上笔者觉得 Consume这个词表达的更贴切)其他开发者所设计的类型,就需要在项目中将该类型所在的程序集引用进来。现在看到的这些程序集引用,都是微软认为很常 用的,几乎是每个项目都会使用到的,所以在创建项目时自动添加了进来,免得开发者再手动进行添加。
但是在这里这些引用不利于我们理解一些内容,所以我们把这些引用全部删除掉,如图6-6所示,然后再次编译程序。
图6-6 删除掉所有的项目引用
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApp {
classProgram {
staticvoid Main(string[] args) {
string text = "Hello, world!";
Console.WriteLine(text);
}
}
}
可能有人会认为,在删掉这些引用之后,编译器将会毫不客气地提示编译错误:未能找到类型或命名空间“System”(是否缺少using指令或程序 集引用?)。可实际上,当编译并运行上面的代码时,程序会正确无误地执行。这是因为我们已经删掉了所有引用的程序集,只定义了一个Program类型,并 没有定义Console类型,所以此时要面对的第一个问题就是:Console类型从哪里来?
Visual Studio提供了一个快捷的办法使我们可以快速查看类型:将光标定位在Console上,然后按下键盘上的F12,就可以看到Console的类型定 义。在Console类型定义的最上方,可以看到它所在的程序集地址:C:Program FilesReference AssembliesMicrosoftFramework.NETFrameworkv4.0mscorlib.dll。
#region 程序集 mscorlib.dll, v4.0.30319
// C:Program FilesReference AssembliesMicrosoftFramework.NETFrameworkv4.0mscorlib.dll
#endregion
using System.IO;
using System.Runtime.ConstrainedExecution;
using System.Security;
using System.Text;
namespace System {
public static class Console {
// 中间略
}
}
可以看到Console类型来自于mscorlib.dll这个程序集。从上面的实验可以看出,不管我们是否引用mscorlib.dll程序集, 它总是会自动引用进来。这个程序集中所包含的类库,即是本节标题中的BCL(Base Class Library,基类库)。从名字就可以看出来,这个类库包含的都是些最基本的类型,其本身已经与CIL语言融为一提了,为CIL语言提供基础的编程支 持,以至于该类库已经成为了CLI标准的一部分(后面会介绍,因此也可以说BCL中的类型就是CIL语言的类型,所有面向CIL的语言都能够使用它们。我 们可以使用对象浏览器(Visual Studio菜单→视图→对象浏览器)来查看mscorlib.dll程序集中都包含了哪些命名空间和类型,如图6-7所示。
图6-7 mscorlib.dll中包含的命名空间
可以看到该程序集下包含的主要是System命名空间,稍微细心一点的读者会发现,在新建项目的时候,还包含了System.dll程序集,并且其中所包含的类型与mscorlib中的类型十分相似。
图6-8 System 程序集
图6-9 System.dll中包含的命名空间
这又是怎么回事呢?实际上,只要点开System命名空间就会发现,mscorlib.dll的System命名空间下面定义的类型和System.dll的System命名空间下面定义的类型完全不同,它们之间并没有冲突之处。
现在就明白了:BCL提供了像Console这样的类型来支持开发者编写类似控制台这样的程序。
既然已经思考了这么多,不妨再深入一下,思考这样一个问题:写下的这条语句string text = “hello, world !”,其中的string从哪里来?从直觉来看,string在Visual Studio中以深蓝色呈现,属于C#的关键字,那么它应该是C#提供的内置类型。可是,当我们将光标移动到string上并按下F12时,转到 string的定义时,看到的却是下面这样的内容:
#region 程序集 mscorlib.dll, v4.0.30319
// C:Program FilesReference AssembliesMicrosoftFramework.NETFrameworkv4.0mscorlib.dll
#endregion
using System.Collections;
using System.Collections.Generic;
// 为了节约篇幅,省略了一些using
namespace System {
public sealed class String : IComparable, ICloneable, IConvertible, IComparable<string>, IEnumerable<char>, IEnumerable, IEquatable<string> {
// 省略定义
}
}
注意最上方的程序集地址,再次看到了mscorlib.dll,并且String类型与Console类型一样,同位于System命名空间下。由 此可见,C#的关键字string,不过是BCL中System.String类型的一个别名而已。类似地,VB.NET中的String关键字也是 BCL中的System.String类型的别名。因此,在.NET框架中,语言从本质上来说没有太大的区别,更多的区别是在语法方面。从上面的例子也可 以看出,C#和VB.NET的很多语言能力并不是自己的,而是从CIL“借”过来的这样做也保证了在不同语言中相应类型的行为是一致的。
表6-1列出了几个典型的,不同语言关键字与CIL类型的对应关系。笔者觉得理解重于记忆,所以这里只列出了几个。要了解其他基础类型时,只要将光标移动到类型上,然后再按下F12键就可以了。
表6-1不同语言关键字与CIL类型的对应关系
CIL 类型 | C# 关键字 | VB.NET关键字 |
System.Byte | byte | Byte |
Sytem.Int16 | short | Short |
System.Int64 | int | Integer |
从表6-1可以看出,.NET同时也对语言开发者提供支持.如你需要设计一款语言,那么在开发编译器时将语言的关键字映射为CIL中的类型就可以了,也就是说,对自己语言中的一些特殊符号(关键字)进行映射处理,就好像C#中的关键字int和string一样。
大家可能听说过这样一种特殊的类型——基元类型(Primitive Type)。实际上,讲到这里大家应该已经明白了,那些由编译器直接支持,将语言本身的关键字类型转换为CIL类型的,就叫做基元类型。显然,上面的 byte、int、string都是基元类型。而C#中并没有一个关键字去映射Console,所以我们认为Console只是普通的类类型(Class Type)。
6.3.2 FCL——框架类库
作为一名.NET程序员,每天都要打交道的就是FCL了(Framework Class Library,框架类库)。在上一节中介绍了BCL,它是FCL的一个子集。BCL中包含了与编译器及CIL语言关系紧密的核心类型,以及常见开发任务 中都会使用到的类型。而FCL包含的内容极多,仅服务于一种应用场景的子类库就足够写一本书了,这里仅简单对它进行介绍。
从功能上来看,可以将FCL框架类库划分成以下几层。
- 最内一层,由BCL的大部分组成,主要作用是对.NET框架、.NET运行时及CIL语言本身进行支持,例如基元类型、集合类型、线程处理、应用程序域、运行时、安全性、互操作等。
- 中间一层,包含了对操作系统功能的封装,例如文件系统、网络连接、图形图像、XML操作等。
- 最外一层,包含各种类型的应用程序,例如Windows Forms、Asp.NET、WPF、WCF、WF等。
6.4 CTS——公共类型系统
假设要开发一套新的语言,这种语言和C#或VB.NET一样,在编译后也能够生成CIL代码,也可以在.NET环境下运行,那么首先需要什么呢?
根据6.2节所讲述的内容我们知道,要开发的新语言相当于CIL的高级语言版本,所以实际上要做什么并不是由新语言决定的,而是由CIL来决定的。 因此,需要一套CIL的定义、规则或标准。这套规则定义了我们的语言可以做什么,不可以做什么,具有哪些特性。这套规则就称作CTS(Common Type System,公共类型系统)。任何满足了这套规则的高级语言就可以称为面向.NET框架的语言。C#和VB.NET不过是微软自己开发的一套符合了 CTS的语言,实际上还有很多的组织或团体,也开发出了这样的语言,比如Delphi.Net、FORTRAN等。
那么CTS具体包括哪些内容呢?在回答这个问题之前我们需要弄清楚一个概念。还是通过一段C#代码来说明,先看下面几行代码:
public class Book {
// 省略实现
}
Book item1 = new Book();
Book item2 = new Book();
对于以上代码,通常是这么描述的:定义了一个Book类,并且创建了两个Book类的实例item1、item2。实际上这只包含了两层含义如表6-2所示。
表6-2 类、类的实例
类 | Book |
类的实例 | item1,item2 |
再思考一下就会发现,还有一个更高的层面,那就是Book这个类的类型,我们称之为类类型(Class Type),因此上表可以改成如表6-3所示。
表6-3 类类型、类、类的实例
类类型 | class |
类 | Book |
类的实例 | item1,item2 |
类似的,还有枚举类型(Enum Type)、结构类型((Struct Type)等。现在大家应该明白这里要表达的意思了,CTS规定了可以在语言中定义诸如类、结构、委托等类型,这些规则定义了语言中更高层次的内容。因 此,在C#这个具体的语言实现中,我们才可以去定义类类型(Class Type)或者结构类型(Struct Type)等。
同样,可以在Book类中定义一个字段name并提供一个方法ShowName()。实际上,这些也是CTS定义的,它规范了类型中可以包含字段(filed)、属性(property)、方法(method)、事件(event)等。
除了定义各种类型外,CTS还规定了各种访问性,比如Private、Public、Family(C#中为Protected)、 Assembly(C#中为internal)、Family and assembly(C#中没有提供实现)、Family or assembly(C#中为protected internal)。
CTS还定义了一些约束,例如,所有类型都隐式地继承自System.Object类型,所有类型都只能继承自一个基类。从CTS的名称和公共类型 系统可以看出,不仅C#语言要满足这些约束,所有面向.NET的语言都需要满足这些约束。众所周知,传统C++是可以继承自多个基类的。为了让熟悉C++ 语言的开发者也能在.NET框架上开发应用程序,微软推出了面向.NET的C++/CLI语言(也叫托管C++),它就是符合CTS的C++改版语言,为 了满足CTS规范,它被限制为了只能继承自一个基类。
关于上面内容有两点需要特别说明:
1)C#并没有提供Family and assembly的实现,C#中也没有全局方法(Global Method)。换言之,C#只实现了CTS 的一部分功能。,也就是说,CTS规范了语言能够实现的所有能力,但是符合CTS规范的具体语言实现不一定要实现CTS规范所定义的全部功能。
2)C++/CLI又被约束为只能继承自一个基类,换言之,C++中的部分功能被删除了。,就是说,任何语言要符合CTS,其中与CTS不兼容的部分功能都要被舍弃。
显然,由于CIL是.NET运行时所能理解的语言,因此它实现了CTS的全部功能。虽然它是一种低级语言,但是实际上,它所具有的功能更加完整。C#语言和CIL的关系,可以用图6-10进行表示。
图6-10 C#和CIL的关系
6.5 CLS——公共语言规范
既然已经理解了CTS是一套语言的规则定义,就可以开发一套语言来符合CTS了。假设这个语言叫做N#,它所实现的CTS非常有限,仅实现了其中很少的一部分功能,它与CTS和C#语言的关系可能如图6-11所示。
图6-11 C#、N#和CIL的关系
那么现在就有一个问题:由C#编写的程序集,能够引用由N#编写的程序集吗?答案显然是不能,,虽然C#和N#同属于CTS旗下,但是它们并没有共 通之处。因此,虽然单独的N#或C#程序可以完美地在.NET框架下运行,但是它们之间却无法相互引用。如果使用N#开发项目的开发者本来就不希望其他语 言类型的项目来引用他的项目倒也罢了,但是,如果N#项目期望其他语言类型的项目能够对它进行引用,就需要N#中公开的类型和功能满足C#语言的特性,即 它们需要有共通之处。注意,这句话中有一个词很重要,就是“公开的”(public)。N#中不公开的部分(private、internal、 protected)是不受影响的,可以使用独有的语言特性,因为这些不公开的部分本来就不允许外部进行访问。因此, 如果N#想要被C#所理解和引用,它公开的部分就要满足C#的一些规范,此时,它与CTS和C#语言的关系就会变成如图6-12所示。
图6-12 C#、N#、CIL的关系
如果世界上仅有C#和N#两种语言就好办了,把它们共同的语言特性提取出来,然后要求所有公开的类型都满足这些语言特性,这样C#和N#程序集就可 以相互引用了。可问题是:语言类型有上百种之多,并且.NET的设计目标是实现一个开放的平台,不仅现有的语言经过简单修改就可以运行在.NET框架上, 后续开发的新语言也可以,而新语言此时并不存在,如何提取出它的语言特性?因此又需要一套规范和标准来定义一些常见的、大多数语言都共有的语言特性。对于 未来的新语言,只要它公开的部分能够满足这些规范,就能够被其他语言的程序集所使用。这个规范就叫做CLS (Common Language Specification,公共语言规范)。很明显,CLS是CTS的一个子集。现在引入了CLS,图6-12的关系图就可以改成如图6-13所示。
图6-13 语言、CLS、CIL的关系
如果利用C#开发的一个程序集的公开部分仅采用了CLS中的特性,那么这个程序集就叫做CLS兼容程序集(CLScompliant assembly)。显然,对于上面提到的FCL框架类库,其中的类型都符合CLS,仅有极个别类型的成员不符合CLS,这就保证了所有面向.NET的语 言都可以使用框架类库中的类型。
现在,读者又会有一个疑问:上面几段文字中反复出现了一个词———“语言特性”(language features),满足CLS就是要求语言特性要一致,那么什么叫做语言特性?这里给出几个具体的语言特性:是否区分大小写,标识符的命名规则如何,可 以使用的基本类型有哪些,构造函数的调用方式(是否会调用基类构造函数),支持的访问修饰符等。
那么我们如何检验程序集是否符合CLS呢?.NET为我们提供了一个特性CLSCompliant,便于在编译时检查程序集是否符合CLS。我们来看下面一个例子:
using System;
[assembly:CLSCompliant(true)]
public class CLSTest {
public string name;
// 警告:仅大小写不同的标识符“CLSTest.Name()”不符合 CLS
public string Name() {
return "";
}
// 警告:“CLSTest.GetValue()”的返回类型不符合 CLS
public uint GetValue() {
return 0;
}
// 警告: 参数类型“sbyte”不符合 CLS
public void SetValue(sbyte a) { }
// 警告标识符“CLSTest._aFiled”不符合 CLS
public string _MyProperty { get; set; }
}
可以注意到,在CLSTest类的前面为程序集加上了一个CLSCompliant特性,表明这个程序集是CLS兼容的。但是,有三处并不满足这个要求,因此编译器给出了警告信息。这三处是:
- 不能以大小写来区分成员,因此字段name和方法Name()不符合CLS。
- 方法的返回类型和参数类型必须是CLS兼容的,uint和sbyte类型并非CLS兼容,因此GetValue()和SetValue()方法不符合CLS。
- 标识符的命名不能以下划线“_”开头,因此属性_MyProperty不符合CLS。
还会注意到,编译器给出的只是警告信息,而非错误信息,因此可以无视编译器的警告,不过这个程序集只能由其他C#语言编写的程序集所使用。
6.6 CLR——公共语言运行时
6.6.1 程序集概述
前面提到过:程序集包含了CIL语言代码,而CIL语言代码是无法直接运行的,需要经过.NET运行时进行即时编译才能转换为计算机可以直接执行的机器指令。那么这个过程是如何进行的呢?
接下来我们要了解的就是.NET框架的核心部分:CLR(Common Language Runtime),公共语言运行时),有时也会称做.NET运行时(.NET runtime)。在了解CLR之前,需要先进一步学习一下程序集,因为下一节会对程序集进行专门的讲述,这里仅简单介绍一下程序集中对于理解CLR有帮 助的概念。
从直觉上来看,前面以.exe为后缀的控制台应用程序就是一个直接的可执行文件,因为在双击它后,它确实会运行起来。这里的情况和面向对象中的继承有一点像:一台轿车首先是一部机动车、一只猫首先是一个动物,而一个.NET程序集首先是一个Windows可执行程序。
那么什么样格式的文件才是一个Windows可执行文件?这个格式被称做PE/COFF(Microsoft Windows Portable Executable/Common Object File Format),Windows可移植可执行/通用对象文件格式。Windows操作系统能够加载并运行.dll和.exe是因为它能够理解PE /COFF文件的格式。显然,所有在Windows操作系统上运行的程序都需要符合这个格式,当然也包括.NET程序集在内。在这一级,程序的控制权还属 于操作系统,PE/COFF头包含了供操作系统查看和利用的信息。此时,程序集可以表示成如图6-14所示。
图6-14 程序集结构1
在前面提到过,程序集中包含的CIL语言代码并不是计算机可以直接执行的,还需要进行即时编译,那么在对CIL语言代码进行编译前,需要先将编译的 环境运行起来,因此PE/COFF头之后的就是CLR头了。CLR头最重要的作用之一就是告诉操作系统这个PE/COFF文件是一个.NET程序集,区别 于其他类型的可执行程序。
图6-15 程序集结构2
在CLR头之后就是大家相对熟悉一些的内容了。首先,程序集包含一个清单(manifest),这个清单相当于一个目录,描述了程序集本身的信息,例如程序集标识(名称、版本、文化)、程序集包含的资源(Resources)、组成程序集的文件等。
图6-16 程序集结构3
清单之后就是元数据了。如果说清单描述了程序集自身的信息,那么元数据则描述了程序集所包含的内容。这些内容包括:程序集包含的模块(会在第7章介 绍)、类型、类型的成员、类型和类型成员的可见性等。注意,元数据并不包含类型的实现,有点类似于C++中的.h头文件。在.NET中,查看元数据的过程 就叫做反射(Reflection)。
图6-17 程序集结构4
接下来就是已经转换为CIL的程序代码了,也就是元数据中类型的实现,包括方法体、字段等,类似于C++中的.cpp文件。
图6-18 程序集结构
注意,图6-18中还多添加了一个资源文件,例如.jpg图片。从这幅图可以看出,程序集是自解释型的(Self-Description),不再需要任何额外的东西,例如注册表,就可以完整地知道程序集的一切信息。
至此对程序集的简单介绍就先到这里,接下来看一下程序集是如何被执行的。
6.6.2 运行程序集
现在已经了解过了程序集,并且知道程序集中包含的CIL代码并不能直接运行,还需要CLR的支持。概括来说,CLR是一个软件层或代理,它管理 了.NET程序集的执行,主要包括:管理应用程序域、加载和运行程序集、安全检查、将CIL代码即时编译为机器代码、异常处理、对象析构和垃圾回收等。相 对于编译时(Compile time),这些过程发生在程序运行的过程中,因此,将这个软件层命名为了运行时,实际上它本身与时间是没有太大关系的。有一些朋友在初学.NET的时 候,纠结在了Runtime这个词上,总以为和时间有什么关系,总是不能很好地理解CLR。笔者认为重要的是理解CLR是做什么的,而不用过于关注它的名 称。
实际上,CLR还有一种叫法,即VES(Virtual Execution System,虚拟执行系统)。从上一段的说明来看,这个命名应该更能描述CLR的作用,也不容易引起混淆,但是可能为了和CIL、CTS、CLS等术语 保持一致性,最后将其命名为了CLR。在这里,我们知道CLR不过是一个.NET程序集的运行环境而已,有点类似于Java虚拟机。VES这个术语来自于 CLI,会在6.7节进行讲述。
可以用图6-19来描述CLR的主要作用。
图6-19 CLR的主要作用
前面已经概要地了解了CLR的作用,接下来开始更进一步的学习。首先遇到的问题就是:CLR以什么样的形式位于什么位置?
由于CLR本身用于管理托管代码,因此它是由非托管代码编写的,并不是一个包含了托管代码的程序集,也不能使用IL DASM进行查看。它位于C:\%SystemRoot%Microsoft.NETFramework版本号下,视安装的机器不同有两个版本,一 个是工作站版本的mscorwks.dll,一个是服务器版本的mscorsvr.dll。wks和svr分别代表work station和server。
接下来再看一下CLR是如何运行起来的。虽然从Windows Server 2003开始,.NET框架已经预装在操作系统中,但是它还没有集成为操作系统的一部分。当操作系统尝试打开一个托管程序集(.exe)时,它首先会检查PE头,根据PE头来创建合适的进程。
接下来会进一步检查是否存在CLR头,如果存在,就会立即载入MsCorEE.dll。这个库文件是.NET框架的核心组件之一,注意它也不是一个 程序集。MsCorEE.dll位于C:\%SystemRoot%System32系统文件夹下所有安装了.NET框架的计算机都会有这个文件。大 家可能注意到了,这个库安装在System32系统文件夹下,而没有像其他的核心组件或类库那样按照版本号存放在C:\%SystemRoot% Microsoft.NETFramework文件夹下。这里又存在一个“鸡生蛋问题”:根据不同的程序集信息会加载不同版本的CLR,因此加载 CLR的组件就应该只有一个,不能再根据CLR的版本去决定加载CLR的组件的版本。
MsCorEE.dll是一个很细的软件层。加载了MsCorEE.dll之后,会调用其中的_CorExeMain()函数,该函数会加载合适版 本的CLR。在CLR运行之后,程序的执行权就交给了CLR。CLR会找到程序的入口点,通常是Main()方法,然后执行它。这里又包含了以下过程:
- 加载类型。在执行Main()方法之前,首先要找到拥有Main()方法的类型并且加载这个类型。CLR中一个名为Class loader(类加载程序)的组件负责这项工作。它会从GAC、配置文件、程序集元数据中寻找这个类型,然后将它的类型信息加载到内存中的数据结构中。在 Class loader找到并加载完这个类型之后,它的类型信息会被缓存起来,这样就无需再次进行相同的过程。在加载这个类以后,还会为它的每个方法插入一个存根 (stub)。
- 验证。在CLR中,还存在一个验证程序(verifier),该验证程序的工作是在运行时确保代码是类型安全的。它主要校验两个方面,一个是元数据是正确的,一个是CIL代码必须是类型安全的,类型的签名必须正确。
- 即时编译。这一步就是将托管的CIL代码编译为可以执行的机器代码的过程,由CLR的即时编译器(JIT Complier)完成。即时编译只有在方法的第一次调用时发生。回想一下,类型加载程序会为每个方法插入一个存根。在调用方法时,CLR会检查方法的存 根,如果存根为空,则执行JIT编译过程,并将该方法被编译后的本地机器代码地址写入到方法存根中。当第二次对同一方法进行调用时,会再次检查这个存根, 如果发现其保存了本地机器代码的地址,则直接跳转到本地机器代码进行执行,无需再次进行JIT编译。
可以看出,采用这种架构的一个好处就是,.NET程序集可以运行在任何平台上,不管是Windows、UNIX,还是其他操作系统,只要这个平台拥有针对于该操作系统的.NET框架就可以运行.NET程序集。
6.7 CLI——公共语言基础
CLI是一个国际标准,由ECMA和ISO进行了标准化,全称为Common Language Infrastructure(公共语言基础)。它只是一个概念和汇总,实际上本章的每一小节都是这个标准的一部分。CLI包括:CIL、CTS、 CLS、VES、元数据、基础框架。
看到这里很多人会感觉到有点奇怪,为什么CLI和.NET框架包含的内容如此雷同?它们之间是什么关系?简单来说,CLI是一个标准,而.NET框 架是这个标准的具体实现。在CLI中,并没有CLR的概念,只有VES,而CLR就是.NET框架中VES的具体实现。既然CLI只是一个标准, 而.NET框架是它在Windows平台上的具体实现,那么是不是就只有.NET框架这一个CLI的实现?显然不是,Mono Project就是CLI标准的另一个实现。Mono Project的目标就是将.NET框架多平台化,使其可以运行在各种平台上,包括Mac OS、Linux等。
CLI的详细信息可以在这里查看:http://www.ecma-international.org/publications/standards/Ecma-335.htm,感兴趣的朋友可以将它的PDF标准文档下载下来看一下。
6.8 本章小结
本章系统的学习地介绍了一下.NET框架的底层知识,几乎包含了常见的所有术语,例如程序集、CIL、CTS、CLS、CLR等,同时也介绍了它们之间是如何相互协作共同构建起整个.NET平台的。相信经过本章的学习,大家会对.NET框架有一个更好的全局性认识。
感谢阅读,希望这篇文章能给你带来帮助。