本章讨论了程序集和模块的组织、部署和执行。它还对元数据片断提供了逐条的检查,负责程序集和模块的同一性和交互性:清单表(manifest)。正如你可能从第一章回想到的,一个程序集可以包括很多模块(托管的PE文件)。一个多模块程序集的任何模块可以——而且确实可以,一般地说——携带着它自己的清单表,但是每个程序集只有一个模块携带着这个清单表,其中包括了程序集的同一性。因此,每个程序集,无论是多模块还是单模块,只包括一个主模块。
什么是程序集?
程序集是一个部署单元,一个托管的应用程序的构件块。程序集是可重用的,允许不同的应用程序使用同样的程序集。程序集在它们的元数据中携带了一个详尽的自描述,包括版本信息——允许CLR为一个特定的应用程序使用一个明确的版本的程序集。
这种布局消除了所谓的DLL Hell,该情形发生于更新一个应用程序使另一个应用程序实效时,因为它们都恰巧使用了具有相同名称的DLL的不同版本。
私有的和共享的程序集
程序集被分类为私有的和共享的。从结构化和功能性上讲,这两种程序集是相同的,但是它们在命名上和使用上以及由加载器执行的版本检查的级别上是不同的。
私有的程序集被认为是一个特定的应用程序的一部分,并不打算由其它应用程序使用。私有的程序集被使用在与应用程序同样的目录中或者在这个目录的子目录中。这种类型的使用保护了这个私有的程序集不被其它应用程序访问到,而这些应用程序是不该有权访问它的。
作为一个特定的应用程序的一部分,私有程序集通常是由与该应用程序指定的其他组件相同的作者(人、群组或组织)创建的,并因此被认为主要是作者的职责。从而,私有程序集的命名和版本上的要求是放宽的,而CLR并不强迫这些要求。私有的程序集的名称在这个应用程序中必须是唯一的。
共享的程序集并不是一个特定的应用程序的一部分,并被设计为广泛使用在各种各样的应用程序中。共享的程序集通常由群组或组织——而不是那些负责使用这些程序集的应用程序——所创建。一个显著的关于共享程序集的例子就是组成.NET Framework类库的一组程序集。
作为这样定义的结果,共享程序集的命名和版本上的要求比私有程序集要严格的多。共享程序集的名称必须是全局唯一的。额外的程序集同一性由强名称(strong name)提供,它使用了公钥/私钥——密钥对来保证强名称的唯一性并防止假名攻击(name spoofing)。强名称的核心部分是强签名(strong name signature,在第5章提到过)——一个使用发布者的私钥对程序集主模块进行加密的哈希值,用于校验这个强签名。强签名还提供了共享程序集的消费者,带有关于程序集发布者同一性的信息。如果CLR密码校验通过,消费者就可以保证程序集来自所期望的发布者,假设这个发布者的私钥没有被泄露。
共享程序集配置在机器范围内的仓库中——称为GAC(全局程序集缓存)。GAC并排存储了共享程序集的多重版本。加载器就在GAC中寻找共享程序集。
在一些环境中,一个应用程序可能需要在它的目录中使用共享程序集来保证被加载的版本是恰当的。在这种情况下,共享程序集被作为一个私有程序集使用;因此它事实上不是共享的,而无论它是否为强命名的。
作为逻辑执行单元的应用程序域
操作系统和CLR代表性地提供了运行在系统的应用程序间某种形式的隔离。这种隔离有必要保证运行在一个应用程序中的代码不会反过来影响另外的不相干的应用程序。在流行的操作系统中,这种隔离通过使用硬件强制的边界处理来完成,这里的进程,占据了一个唯一的虚地址空间,严格地运行一个应用程序并限定这个进程可以使用的资源的作用范围。
只在这个应用程序的范围内起作用
托管的代码执行对隔离有着相同的需要。这样的隔离可以在一个托管的应用程序中低成本提供,然而,考虑到托管的应用程序运行在CLR的控制之下并被校验为类型安全的。
CLR允许多个应用程序运行在一个单一的操作系统进程中,使用了被称为应用程序域(appliccation domain)的架构来将这些应用程序与另一个隔离开。既然应用程序需要的所有的内存隔离都由CLR完成,那么对于CLR来说,允许一个应用程序只能访问那些由该应用程序分配的对象,以及阻塞一个应用程序试图访问在另一个应用程序域中分配的对象,就是简单的了。在许多方面,应用程序域就是操作系统进程在CLR中的等价物。。
特别的,托管应用程序中的隔离意味如下:
l 不同的安全级别可以被分配到不同的应用程序域,给宿主一个运行应用程序并伴随着在一个进程中改变安全要求的机会
l 运行在一个应用程序中的代码不能直接访问来自另一个应用程序的代码或资源。(这么做可能引进一个安全漏洞。)这个规则的一个例外是,.NET Framework的基类库程序集——Mscorlib——被所有应用程序域在这个进程中所共享。Mscorlib在多个进程间不是共享的。
l 一个应用程序中的错误不能通过停止整个进程影响另外的应用程序。
l 每个应用程序控制代码的加载位置来代替它来自什么地方,以及被加载的代码是什么版本的。此外,配置信息只在这个应用程序的范围内起作用。
下面的示例描述的场景,有益于在同一个进程中运需行多个应用程序:
l ASP.NET在同一个进程中运行多个Web应用程序。在ASP和IIS中,应用程序的隔离由进程边界获取,这是非常昂贵的,以至于不能适当的扩展——在一个进程中运行20个应用程序域比跨越20个单独的进程的成本要低一些。
l Microsoft Internet Explorer在与浏览器代码本身相同的进程中运行来自多个站点的代码。显然,来自一个站点的代码应该不能影响来自另一个站点的代码。
l 数据库引擎需要运行在同一个进程中来自多个用户应用程序的代码。
l 应用程序服务器产品可能需要运行在单一的进程中来自多个应用程序的代码。
环境宿主如ASP.NET或Internet Explorer需要代表用户运行托管代码并利用由应用程序域提供的应用程序隔离的特性。实际上,这是由宿主决定的——应用程序域的边界位于什么地方以及用户代码运行在什么域中,正如这些例子所示:
l ASP.NET创建应用程序域来运行用户代码。正如Web服务器所定义的那样,这些域由每个应用程序创建。
l 默认的Internet Explorer为每个站点创建一个应用程序域(尽管开发者可以自定义这种行为)。
l 在SHELL这个EXE中,每个从命令行启动的应用程序,运行在一个独立的占据一个进程的应用程序域中。
l VBA(Microsoft Visual Basic for Application)使用了进城的默认的应用程序域来运行包含在Microsoft Office文档中的脚本代码。
l WFC(Windows Foundation Class)窗体设计器为每个被创建的的窗体建立了一个单独的应用程序域。当一个窗体被发布或重新编译时,旧有的应用程序域就会被关闭,代码被重新编译,并且新的应用程序域被创建。
由于隔离要求代码或资源不能被运行在另一个应用程序中的代码直接访问,在不同的应用程序域中的对象之间直接的调用是不允许的。跨域通信仅限于复制对象或创建特殊的代理对象,这是该对象在其它域中的“代表”,给在其它域中的代码访问该对象的实例字段或方法的权限。关于跨域通信,这类对象属于下面三种分类的一种:
l 未绑定的对象是跨域按值分组的。这意味着接收域得到这个对象的一份复制来代替原始对象。
l 绑定到AppDomain的对象是跨域按引用分组的,这意味着跨域访问总是通过代理来完成。
l 上下文绑定的对象也是跨域按引用分组的,也可以在同一域中的上下文之间。上下文是一组定义了对象在环境中位置的使用规则。当对象进入或离开上下文时这个规则被强制执行。
CLR依靠代码安全类型验证,来为域之间的错误隔离提供一种比在操作系统中使用的进程隔离所招致更加低廉的消耗。这种隔离是基于静态类型验证的,因此,硬件的信号转换或进程的抢占就不是必须的了。
清单表
描述了程序集及其模块的元数据被称为清单表(Manifest)。清单表携带了下面的信息:
l 同一性(identity),包括了一个简单的文本名称,一个程序集的版本号,一个可选的文化(如果这个程序集包括了本地化的托管资源),以及一个可选的公钥(如果这个程序集是强名称的)。这个信息定义在两个元数据表中:Module和Assembly(只在主模块中)。
l 内容,包括了由这个程序集暴露给外部使用的类型和托管资源以及这些类型和托管资源的位置。包含了这些信息的元数据表是ExportedType(只在主模块中)和ManifestResource。
l 依赖,包括了这个程序集引用的其它(外部的)程序集,以及在多模块程序集的情况中同一个程序集的其它模块。你可以在这些元数据表中找到依赖信息:AssemblyRef,ModuleRef和File。
l 请求权限,总体上特定于这个程序集。更多特定的请求权限可能也用来定义某些类型(类)和方法。这些信息定义在DeclSecurity元数据表中。(第17章描述了请求权限以及声明它们的方法。)
l 自定义特性,特定于清单表组件。自定义特性提供了额外的通畅由编译器和其它工具使用的信息。CLR承认一定数量的自定义特性。自定义特性定义在CustomAttribute元数据表中。(参见第16章获取关于这个主题的更多信息。)
图6-1显示了清单表中的元数据表之间发生的相互引用。
图6-1清单表中的元数据表之间发生的相互引用
程序集元数据表和声明
程序集元数据表至多包括一笔记录,该记录出现在主模块的元数据中。这个表具有以下的列结构:
HashAlgId(4字节无符号整数):使用在这个程序集中对文件进行散列处理的哈希散列算法的ID。这个值必须是定义在头文件Wincrypt.h中若干CLAG_*值的一个。默认的哈希散列算法是CALG_SHA(又名CALG_SHA1)(0x8004)。ECMA International/ISO认为这个算法是标准的——为文件的散列处理提供了最广泛的可用性。
MajorVersion(2字节无符号整数):程序集的主版本。
MinorVersion(2字节无符号整数):程序集的次版本。
BuildNumber(2字节无符号整数):程序集的内部版本号。
RevisionNumber(2字节无符号整数):程序集的修订号。
Flags(4字节无符号整数):程序集标记,指出这个程序集是否为强名称的(如果公钥存在就由元数据发布的API自动设置),JIT跟踪和/或优化是否被支持(在程序集加载时自动设置),以及程序集在运行期是否能被重定向到一个程序集的不同版本。JIT跟踪是一种从IL指令的偏移量到由JIT编译器生成的本地代码的映射;这种映射在托管代码的调试期间使用。
PublicKey(#Blob流中的偏移量):一个二进制的对象,表示一个强名称程序集的公钥。
Name(#String流中的偏移量):程序集名称,不能为空,不能包括路径和文件名称的扩展名(例如,mscorlib,System.Data)。
Locale(#String流中的偏移量):文化(通常被称为本地化)名称,如en-US(美国英语)或fr-CA(加拿大英语),识别了这个程序集的本地化托管资源的文化。文化名称必须匹配贯穿.NET Framework类库的成百个为CLR所知的文化名称的其中一个,但是这个有效性规则是相当无意义的:为了使用一个文化,特定的语言支持必须安装在目标机器上。如果语言支持没有安装,那么这个文化是否为运行时所知就没关系了。
在ILAsm中,这个程序集以下面的方式声明(例如):
{
.publickey = (00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 )
.hash algorithm 0x00008004
.ver 2:0:0:0
}
程序集声明的ILAsm语法如下:
where <flags> ::=
<none> // Assembly cannot be retargeted
| retargetable // Assembly can be retargeted
and <assemblyDecl> ::=
.hash algorithm <int32> // Set hash algorithm ID
| .ver <int32>:<int32>:<int32>:<int32> // Set version numbers
| .publickey = ( <bytes> ) // Set public encryption key
| .locale <quotedString> // Set assembly culture
| <securityDecl> // Set requested permissions
| <customAttrDecl> // Define custom attribute(s)
在这个声明中,<Int32>表示了一个整数,至多4个字节大小。<bytes>符号表示了一个由2位的16进制数字组成的序列,每一个表示1字节;这种字节数字的形式,经常用在ILAsm中表示任意大小的二进制对象。最后,<quotedString>符号,通常地说,一个合成的被引用的字符串——这就是说,一个诸如"ABC"+"DEF"+"GHI"的结构。
in regard to 关于
mutual 相互的
Assembly Identity 程序集标识
BuildNumber 内部版本号
RevisionNumber 修订号