.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 Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll。

#region 程序集 mscorlib.dll, v4.0.30319
// C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.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 Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.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.NET\Framework\版本号下,视安装的机器不同有两个版本,一个是工作站版本的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.NET\Framework\文件夹下。这里又存在一个“鸡生蛋问题”:根据不同的程序集信息会加载不同版本的CLR,因此加载CLR的组件就应该只有一个,不能再根据CLR的版本去决定加载CLR的组件的版本。

MsCorEE.dll是一个很细的软件层。加载了MsCorEE.dll之后,会调用其中的_CorExeMain()函数,该函数会加载合适版本的CLR。在CLR运行之后,程序的执行权就交给了CLR。CLR会找到程序的入口点,通常是Main()方法,然后执行它。这里又包含了以下过程:

  1. 加载类型。在执行Main()方法之前,首先要找到拥有Main()方法的类型并且加载这个类型。CLR中一个名为Class loader(类加载程序)的组件负责这项工作。它会从GAC、配置文件、程序集元数据中寻找这个类型,然后将它的类型信息加载到内存中的数据结构中。在Class loader找到并加载完这个类型之后,它的类型信息会被缓存起来,这样就无需再次进行相同的过程。在加载这个类以后,还会为它的每个方法插入一个存根(stub)。
  2. 验证。在CLR中,还存在一个验证程序(verifier),该验证程序的工作是在运行时确保代码是类型安全的。它主要校验两个方面,一个是元数据是正确的,一个是CIL代码必须是类型安全的,类型的签名必须正确。
  3. 即时编译。这一步就是将托管的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框架有一个更好的全局性认识。

感谢阅读,希望这篇文章能给你带来帮助。