Shared Source CLI Essentials第一章第二部分

** 共享类型系统和中间语言 ** ** **

CLI 中的类型在最低限度上是由字段和方法构成的,但是这些字段和方法自身又是如何定义的呢? CLI 标准定义了一个与处理器无关的中间语言,用于描述程序,也定义了一个通用类型系统来为这种中间语言提供基本数据类型。这两个事物一起组成了一个抽象计算模型。标准使用一些规则来修饰这个抽象模型,这些规则描述了抽象模型如何才能转化成机器指令流和内存引用;这些转化过程的设计十分高效,能够识别和准确的描述许多不同编程语言的语义。中间语言,中间语言类型,以及转换的规则,组成了一个具有普遍意义的,用于描述程序的语言无关的方法。

CLI 规范中定义的中间语言叫做通用中间语言( CIL )。它包含一个与任何现存的计算机硬件结构均无关的丰富的操作码集合,用于驱动一个易于理解的抽象堆栈机。同样的,通用类型系统( CTS )定义了一个包含标准的跨语言互操作性的类型的基本集合。为了充分实现这种语言无关的世界的好处,高级编译器需要理解 CIL 指令集和它所匹配的数据类型的集合。如果没有这个协议,那么不同的语言就必须选择不同的映射方式;例如, C# 中的 int 类型的长度有多大?它和 Visual Basic 中的 Integer 类型有什么关系?它和 C++ 中的 long 类型完全相同吗?通过将指令集和这些类型进行匹配,这些选择将变得相当简单,当然,关于具体应该使用哪个指令和类型的选择是由编译器决定的,但是,一个良好的规范的出现意味着使得这些选择变得相当地直接和简单。通过使用这种方法,结果代码可以和其它语言编写的代码和框架进行互操作,从而导致了更加高效的重用。第 3 章详细描述了 CLI 类型系统,而第 5 章描述了 CIL ,以及它是如何转换成本地指令的。

** 基于类型的可移植打包单元:装配件 ** ** **

CLI 利用它的类型系统和抽象计算模型,实现了这样的理想:人们可以利用由不同人员在不同时间编写的软件组件,通过校验,装载这些组件,来使用它们一起创建应用程序。在 CLI 中,单独的组件可以打包到称为装配件的单元中,装配件可以在执行引擎中按需动态装载,既可以从本地磁盘中装载,也可以从网络上装载,甚至还可以在程序的控制下动态的创建。

装配件为 CLI 定义了组件模型的语义。类型不能存在于装配件的外部。反过来说,装配件是将类型装载到 CLI 中的唯一的机制。装配件又是由一个或多个模块(模块是驻留信息的打包子单元),以及一大块名为装配件清单的描述装配件的元数据组成的。虽然装配件也能由多个模块组成,不过一般一个装配件都只包含一个模块。

为了保证装配件不会在被编译和被装载的时候被篡改,每一个装配件通过一个秘钥对和整个装配件的一个哈希表来进行签名,这个签名可以被放在装配件清单中。这个签名被执行引擎所信任,并且可以保证装配件不会被篡改,以及有危险的装配件不会被装载。如果在运行时从装配件生成的哈希表和装配件清单中包含的哈希表不匹配,运行时会拒绝装载装配件,并且在潜在的危险代码有机会做任何事情之前抛出一个异常。

在许多方面,装配件对于 CLI 的意义,就像共享库或 DLL 对于操作系统的意义一样:它们都是绑定和识别属于同一个部分的代码的方法。感谢 CLI 中建立的可靠的对元数据和符号的绑定方法,这使得每一个组件都可以在独立于它的邻居的情况下被装载,翻译以及执行,即使它们之间相互依赖,也不会互相干扰。这是至关紧要的,因为平台,应用程序,库,以及硬件都会随着时间的变化而改变。基于组件建立的解决方案应该在这些组件变化时继续正常工作。我们将在第 3 章和第 4 章讨论装配件。

** 组件隔离:应用程序域和远程调用 ** ** **

以使组件能够一起工作,并保护组件不受其它组件中的恶意代码或 bug 危害的方式装载各组件的能力,与在组件内部将代码组织在一起的能力一样重要。操作系统常常通过建立保护地址空间,以及提供连接保护地址空间和其它地址空间的通讯机制的方法来获得独立性;地址空间提供保护边界,而通讯机制为协作提供通道。在 CLI 中有相似的隔离执行代码的概念,它由应用程序域和对远程调用的支持组成。

程序集总是在一个应用程序域的上下文中被装载的,因此类型就被它们的应用程序域限制了范围,例如,程序集中定义的静态变量在应用程序域中分配空间和存储。如果同一个程序集在三个不同的域中被加载,会为这个程序集中类型的数据分配三个不同的拷贝。在本质上,应用程序域是 ” 轻量级的地址空间 ” ,对于在各个应用程序域之间传递数据, CLI 执行和操作系统在不同的地址空间之间传递数据所执行的相同的限制。希望跨越域边界进行通讯的类型,必须使用特殊的通讯通道,并按照特定的规则来进行操作。

被称为远程调用的技术,可以用来在不同的物理计算机(计算机上可能运行不同的操作系统,具有不同的处理器)上运行的应用程序域之间进行通讯。就像经常一样,远程调用机制常常用于分隔位于同一个机器中同一个进程的域中的各组件。希望参与到远程调用中的组件要么必须是可序列化的,这样它们就能在域之间传递,要么必须继承自 System.MarshalByRefObject 类型,这样它们可以使用负担传递工作的代理对象进行通讯。在第 4 章中会讲述应用程序域,远程调用,以及装载的详细内容。

** 为灵活的版本装载服务的命名规则 ** ** **

因为所有的类型和类型的代码都存在于程序集中,因此必须要有一组定义明确的描述当执行引擎需要程序集中的类型时如何查找和使用程序集的规则。程序集的名称由一个元素的标准集合组成,包括程序集的基础名称,一个版本号,一个地区文化(为全球化服务),以及一个代表发布这个程序集的发布者的公钥的哈希表。组合名称保证了由各程序集创建的软件会优雅的适应版本的变化。编译时,每个程序集都会包含在编译时它所依赖的其它程序集的组合名称的引用,并记住每个这些程序集的版本信息。这样,当装载时,程序集会非常明确的要求它所依赖的程序集的某个特定(或语义一致)的版本。用来满足这些需求的绑定策略可以通过对配置的设置来改变,但是绑定策略是不可能被忽略的。

通常可以在下列两个地方之一找到程序集:在一个被成为全局程序集缓存 (GAC) ,作用于整个机器的缓存,或者一个基于 URL 的查找路径。 GAC 是每个机器上的程序集的有效的数据库,每一个程序集都由它的四部分名称来唯一的识别。 GAC 可以是一个文件系统的目录,但也可以不是, CLI 的运行必须能够在 GAC 中存放同一个程序集的不同版本,并且能够跟踪这些不同版本。查找路径基本上是一个 URL (通常是文件系统的目录)的集合,当需要装载一个程序集时,会搜索这些路径。第 4 章会详细讲述装载的过程,以及装载是如何实现的。

** JIT ** ** 编译和类型安全 ** ** **

CLI 描述的执行模型意味着编译高级类型的工作应该和将这些类型描述转换为基于特定处理器的代码和内存结构的工作分离开来。这种分离为计算模型带来了很多重要的优势,例如能够在出现了新的操作系统和处理器后,很容易的调整代码来适应,以及能够独立的定义来自各个不同来源的组件的版本。这种分离也带来了新的挑战。例如,因为所有的类型都由 CIL 和 CTS 来描述,所以,所有的类型都必须在它们能够被使用前翻译成机器码和内存结构;本质上,整个应用程序总是必须在能够运行前被重新编译,这可能会是一个非常昂贵的方法。

为了分次消耗将 CIL 转化成机器指令的代价(即包括装载所花费的时间,也包括所需要的内存),基于 CLI 的应用程序的类型的装载方式很独特,直到软件的运行需要它们时才会被装载,一个类型一旦被装载,它的各个方法会直到软件的执行需要它们时才被翻译。这种延期的装载和代码生成被称作即时 (JIT) 编译。 CLI 并不是一定要求最后时刻的 JIT 编译,但是延迟装载和编译总是会发生在一个应用程序的生命周期的某些点上,来将 CIL 转化为机器码。可以想象的一种情况就是,软件的安装程序就可以执行编译。第 5 章将讲述为了符合 CLI 规范 JIT 编译需要实现的方法。

之所以要在 CLI 执行模型中要建立 JIT 编译的最重要的原因并不明显。在执行引擎自己的装载器和编译器的控制下进行从抽象组件到可运行的机器码的转换,是使得执行引擎在运行时保持控制,以及高效的运行代码的原因,即使是在 c++ 编写的代码和一个托管语言编写的代码之间进行来回调用,执行引擎也能很好的工作。传统的编译,链接,和装载的过程,在 CLI 中依然存在,但是,就像我们看到的一样,每个工具链上的元素必须大量使用复杂的技术(例如缓存),因为延迟的使用导致了较高的运行时代价。这些较高的代价是值得忍受的,因为延迟也使得能够对运行的组件的行为进行全面控制。因为 CLI 的执行是基于对类型的逐渐加载,以及所有的类型都是使用平台无关的中间语言定义的,所以 CLI 执行引擎在它运行的过程中是在不断的编译和添加新的行为的。因为 CIL 被设计成可校验的,和类型安全的,编译成机器码的过程是在具有特权的执行引擎的控制下执行的,所以可以在允许一个新类型运行之前校验类型安全性。安全策略也能够在 CIL 被转换成机器码时检查和应用,这就意味着安全检查可以直接插入到代码中,在方法被执行时以系统的名义来执行。简而言之,通过使用延迟加载,校验和直到运行时才对组件进行编译这些技术, CLI 可以加强可靠的托管执行。

** 托管执行 ** ** **

类型装载是导致 CLI 的工具链在运行时十分忙碌的关键所在。看看装载进程的部分工作, CLI 需要编译,装配,链接,验证可执行文件的格式和程序的元数据,校验类型安全,最终甚至是管理运行时的资源,例如内存和处理器周期,在它的控制下代表组件运行。将所有这些阶段联系在一起的任务导致 CLI 包括了名称绑定,内存分布,编译和打补丁,分隔,同步化,以及符号解析的基础结构。因为总是希望这些元素的执行延迟到尽可能最后的时刻,所以执行引擎可以享受对装载和执行策略,内存组织,生成代码,以及代码和底层平台、操作系统的交互方式的高度可靠的控制。

延迟编译,链接,和装载为跨越目标平台和跨越版本变化提供了更好的可移植性。通过延迟排序和排列的决定,延迟地址和偏移量的计算,延迟处理器指令的选择,延迟调用的转化,当然,还有延迟链接到平台的自身服务上,程序集可以变得更加的向前兼容。由定义良好的元数据和策略驱动的延迟过程,是非常有活力的。

翻译元数据的执行引擎是可信赖的系统代码,因此,通过后期装载,安全性和稳定性也得到了增强。每个程序集都包含一个与它相关的许可权限的集合,定义了允许这个程序集执行什么操作。当这个程序集中的代码企图执行一个敏感的操作时(例如企图读或写一个文件,或者企图使用网络), CLI 会查看调用堆栈,检查它来判断是否当前范围内的所有代码都有合适的权限――如果堆栈上的代码没有合适的权限,操作就会被拒绝,一个异常会被抛出(异常是另一个能够在组件之间进行简单的交互的机制; CLI 的设计不仅支持在执行引擎内广泛范围内的异常语义,也紧密的集成了来自底层平台的异常信号)。第 6 章和第 7 章将详细描述托管执行。

** ** ** 使用元数据来实现数据驱动的可扩展性 ** ** **

CLI 组件是自描述的。一个 CLI 组件包含了它内部的每个成员的定义,而这些定义信息在运行时中受保证的有效性是帮助虚拟执行具有高度可适应性的一个因素。每个类型,每个方法,每个字段,每个单一方法调用上的每个单一的参数必须是被充分描述的,而这个描述信息必须存储在程序集内部。因为 CLI 将各种链接操作延迟到必需执行的最后时刻,那些希望通过使用元数据来操作组件和创建新组件的工具和程序获得了极大的灵活性。建立在 CLI 之上的代码可以使用与 CLI 所使用的相同类型的技巧,对于工具软件和运行时服务来说,这简直就是天降横财。

为了获得类型的信息, CLI 程序员可以使用执行引擎提供的反射服务。反射提供了在运行时检查编译时信息的能力。例如,对于一个托管组件,开发者可以获得类型的结构信息,包括它的构造器,字段,方法,属性,事件,接口,以及继承关系。可能更重要的是,开发者可以使用名为定制属性的功能将自己的元数据添加到组件的描述信息中。

通过反射不仅可以获得编译时信息,而且可以操作运行中的实例。开发者可以使用反射来进入类型内部,获得它们的结构信息,并基于这个结构信息操作类型的内部信息。对于方法来说,是一样的;开发者可以在运行时动态的调用方法。这种元数据驱动风格的编程能力,以及如何才能实现它的问题,我们会在第 3 章接触到,并且在第 8 章会详细的进行讲述。

** CLI ** ** 的共享源码实现: ** ** Roter **

2001 年夏天,雷德蒙公司的一小组开发人员宣布了一个微软公司少有的开发计划:一个免费使用的软件产品,包括可修改,可重新发布的源代码。这个叫做 SharedSource CLI(SSCLI ,人们也亲切的称呼它的代码名称,“ Rotor ” ) 的产品,包括一个实现了全部功能的 CLI 执行引擎,一个 C# 编译器,基础的编程库,和许多相关的开发工具。这个产品在商业的 .Net 框架周边静悄悄的发展,代表了微软的开发者工具策略的一个重要方面,实际上, SSCLI 要实现三个目的:检验 CLI 规范的可移植性,帮助人们学习和理解微软的商业 CLR 产品,以及长期促进学院对于 CLI 的兴趣。最重要的是, SSCLI 要符合 ECMA 标准,以便任何希望了解和实现这个标准的人可以将 SSCLI 作为一个指南。

虽然名义上 SSCLI 是这本书的主题,但实际上 CLI 标准才是本书的核心。 SSCLI 帮助我们阐明 CLI 是怎样一件如此令人感兴趣的工作,以及为什么。这个产品本身包含了巨大数量的代码,因此,它可以为在开发工具或系统设计领域工作的研究者和试验者提供重要的帮助,对于那些教授计算机科学的老师也是一样。本书尝试通过提供远多于 CLI 理论的知识,来辅助分析和解释基本代码的规则,以便为这些人提供一个针对这些代码的高级指南。 CLI 标准会变得越来越重要,而完全理解它的最好方法,就是浏览,创建,观察和分析一个运行中的实例。

而 Rotor 向我们演示了创建一个可移植,编程语言无关的 CLI 规范的实际版本的一种方法,当然,它不是唯一的方法。在写作本书的时候,就存在着其它的 CLI 规范的实现方式,包括微软公司的两种(商业 .Net 框架和名为“精简框架”的运行在微型设备上的版本),以及第三方厂商的两种开源的实现,一种来自 Ximian 公司(名为 Mono ),另一种来自 DotGNU 项目(名为 Portable.NET ――可移植 .NET )。 Rotor 自身提供了大大超过标准的各种附加开发工具和功能。为了说明在这个产品中包含了那些内容,图 1 - 3 使用一张图来说明微软商业产品( .NET CLR ), CLI 和 C# 标准,以及 Roter 之间的区别。

如图所示, SSCLI 是 CLI 标准的一个超集,而微软的商业产品又是 SSCLI 的一个超集。

Rotor 是一个由许多人历经多年开发的巨大数量的代码的集合,因此,它是一个复杂和

文体上的变量( stylistically variable??? ),就软件的大小来说,它可以与那些最大的著名开源产品相比,例如 Xfree86 , Mozilla ,以及 OpenOffice 。和这些产品一样,如果希望开始了解这些巨大数量的代码,可能会令人望而却步。本书将帮助您较为容易的完成这个任务,让我们从这个产品自身的简短历程开始吧。

SSCLI 是使用 C++ 和 C# ,以及少数处理特定处理器细节的汇编组合编码开发的,这个产品通过一个三阶段的过程开发完成。首先,一个基于特定平台的 C++ 编译器被用来开发一个平台适配层 (PAL) ,这是一个用于将操作系统的各 API 的差别隐藏在一个单一抽象编程集合后面的库。之后,一系列创建 SSCLI 所必需的开发工具(包括 C# 编译器)在 PAL 库上被创建和链接。最后,使用这些工具和 PAL 库来开发产品的其它部分。

表 1 - 1 列出了一些在浏览 SSCLI 源码时有用的子目录, SSCLI 源码位于本书的随书光盘中(也可以从 http://msdn.microsoft.com/net/sscli 处下载 )

表 1 - 1 。产品中重要的子目录,以及各目录的内容。

** 子目录 ** ** 内容 **

/build 包括创建好的可执行文件和库文件

/clr/src 许多包含核心内容子目录的主目录

/bcl 基础类库,用 C# 编写

/csharp 一个 C# 编译器,用 C# 编写

/classlibnative C++ 编写的开发库

/debug 实现托管调试

/dlls/mscorsn 强名称加密代码

/fjit SSCLI 的 JIT 编译器

/fusion 定位版本文件的代码

/ilasm 一个 CIL 汇编器

/ildasm 一个 CIL 反汇编器

/inc 共享的包含文件

/md 元数据工具

/toolbox/caspol caspol 安全工具的源码

/tools 许多工具程序的主目录

<P class=MsoNormal style="MARGIN: 0

Published At
Categories with Web编程
Tagged with
comments powered by Disqus