ExportedType元数据和声明
ExportedType元数据表包括了关于声明在程序集的非主要模块的公有类(在程序集外部可见)的信息。只有主模块的清单可以携带这个表。
之所以需要这个表是因为加载器希望一个程序集的主模块保存关于所有由程序集导出的类的信息。这些类——定义在主模块和ExportedType表中——的联合,给出了加载器一个完整的画面。
另一方面,定义在主模块中的类和那些定义在ExportedType表中的类之间的交叠必须为零。结果,只在多模块程序集的主模块中,ExportedType表可以是非空的。如果没有非主模块,那么由这个程序集定义的所有的类就位于主模块自身中。
在2.0版本中,ExportedType表提供一个额外的功能:它包括了所谓的类的邮差,这从概念上接近于在非托管世界中的重复导出或现实生活中的邮差。邮差指出了哪个程序集的某某类(曾经位于这个程序集中)被移动了。邮差机制,显然,允许你重置你的多程序集产品而不需要所有你的客户重新生成它们的应用程序。
ExportedType表有以下的列结构:
Flags(4字节宽位域):二进制标记,指出了导出类型是不是一个邮差(forwarder)以及导出类型的可访问性。我们感兴趣的可访问性标记是public和nested public;其它可访问性标记——与在第7章讨论的类的可访问性标记一样——在语句机构上是可接受的,但是并不用于定义真实的导出类型。其它标记只可以出现在冒充的ExportedType中,加载器可以用来解决多模块程序集中的未区分范围的类型引用。
一种合适的解释是这样的。无论何时一个类型(类)在一个模块中被引用,应该提供解析范围来指出被引用的类定义在什么地方(在当前模块中,在这个程序集的另一个模块中,或在另一个程序集中)。如果没有提供解析范围,被引用的类型就应该声明在当前的模块中。然而,如果这个类型在引用它的模块中被找到,并且如果主模块的清单携带一个同一命名的假冒记录来指出这个类型被定义的确切位置,加载器仍然能够解决这个类型引用。
除IL编译器之外,当前没有任何Microsoft托管的编译器,使用这种相当奇异的技术。IL编译器必须如此,出于一些明显的原因。
TypeDefId(4字节无符号整数):一个未编码的符号,指向这个模块——导出类被定义的位置——的TypeDef表的一笔记录。这是在整个的元数据模型中唯一的场合:在这里一个模块的元数据包括来自另一个模块的元数据符号的确切值。这个符号用于给加载器一些提示,并且可以被省略而没有任何不良效果。如果提供了这个符号,加载器就会从相应的模块元数据中获取这个特定的TypeDef记录,并对照TypeDef的完整名称来检查ExportedType的完整名称。如果名称匹配,加载器就会找到它要寻找的类;如果名称不匹配或者这个符号并没有一开始被提供,加载器就会通过它的全名称来找到所需的TypeDef。我的建议是:当用ILAsm编程时,不要显示地指定TypeDefId符号。这个快捷键只在某些场合下并只为自动化工具工作如程序集连接器(AL)。
TypeName(#Strings流中的偏移量):导出类型的名称;必须是非空的。
TypeNamespace(#Strings流中的偏移量):导出类型的命名空间;可以是空的。类名称和命名空间在第7章中讨论。
Implementation(Implementation类型的编码符号):File记录的符号,指出了导出类被定义所在的程序集的文件,或者另一个ExportedType的符号,如果当前符号内嵌在另一个之中。代运人把AssemblyRef符号作为Implementation,依我初步看来,这使得forwarder标记是多余的:一个导出类型的转移本性可以从它的Implementation作为一个AssemblyRef推导出来。
导出类型在ILAsm中声明如下:
指令.assembly extern .file和.class extern定义了一个Implementation入口,而他们是互不相容的。当在.mresource声明的情形中,相应的AssemblyRef、File或ExportedType必须被声明在被Implementation入口引用之前。
相当明显,如果Implementation被指定为.class extern,我们就要处理一个内嵌的导出类型,并且Flags标记必须被设置为nested public。相反,如果Implementation被指定为.file,我们就要处理一个顶级的非内嵌类,并且Flags标记必须被设置为public。
ILAsm中清单声明的次序
在ILAsm中(并且不仅在ILAsm中)的通常规则是“声明,然后引用”。换句话说,这总是安全的,并且,在某些完全需要的情形中,从而声明一个元数据项在对它进行引用之前。有时候你会引用一个还未声明的项——例如,调用一个定义在源代码后面的方法。但是在清单表声明中你不能这么做。
如果我们重复检查图6-1,该图演示了清单元数据表之间的相互引用,我们能够看到下面的从属关系清单。
l 导出类型引用了外部的程序集、文件和封闭的导出类型。
l 清单资源引用了文件和外部的程序集。
l 每个清单项可以聚合自定义特性,而自定义特性引用外部的程序集和(很少的)外部的模块。(参见第16章获取更多细节。)
为了遵守“声明,然后引用”的规则,推荐为ILAsm程序使用下面的声明顺序,同时清单声明在源代码中的所有其它声明之前。
1.AssemblyRef声明(.assembly extern),因为自定义特性。对Mscorlib程序集的引用应该最先进行,因为大多数自定义特性引用这个程序集。
2.ModuleRef声明(.module extern),再一次因为自定义特性。
3.Assembly声明(.assembly)。ILAsm编译器在编译Mscorlib.dll和其它程序集时,得到一些不同的路径,因此最好让它尽快知道获取哪个路径。在2.0版本中,你还可以使用特殊的关键字mscorlib来指出你正在编译Mscorlib.dll。这个关键字最好被放置在程序的开始位置。然而,如果你不编译Mscorlib.dll的话,这就不太重要了;默认下编译器假设它在编译一个“常规的”模块。
4.File声明(.File),因为ExportedType和ManifestResource声明可能引用到它们。
5.ExportedType声明(.class extern),并且密闭的ExportedType声明位于内嵌的ExportedType声明之前。
6.ManifestResource声明(.mresource)。
记住,只有主模块的清单携带着Assembly声明和ExportedType声明。
单模块和多模块程序集
单模块程序集由一个唯一的主模块组成。单模块程序集的清单通常既不携带File表也不携带ExportedType表:这里不存在需要声明的其它文件,以及所有定义在主模块中的类型。然而,你可能想为一个托管的DLL声明一笔File记录,作为部署的一部分,或者你的单独的程序集可能通过ExportedType表使用类型转递(Type Forwarding)
包包译注:Type Forwarding指的是:.NET运行时将对某一个程序集(Assembly)之中定义类型的引用转递为对另外一个或者几个(更新的)Assembly之中同样类型的引用。通过Type forwarding,所有引用(Reference)了Original程序集的程序会去引用新的程序集。
单模块程序集的优点包括了低开销、易部署,并且更加安全。低开销是因为只有一组头和元数据必须被读取、传输和分析。简单的程序集部署是因为只有一个PE文件必须被部署。而更高的安全等级是因为程序集的主模块受到一个强签名的保护,这将使伪造变得极其困难并实际上保护了主模块的真实性。非主模块只通过他们的哈希值进行验证(被主模块的File记录引用到)而理论上更容易进行欺骗。
多模块程序集的模块清单携带了File表,以及这样一个程序集的主模块的清单,可能或不可能携带ExportedType表,依赖于每个公有类型是否都定义在非主模块中。
多模块程序集的优点包括了易部署和低开销。(不,我并没有在和你开玩笑)。这两个优点都阻止了多模块程序集显而易见的模块性
多模块程序集易于部署是因为你在这些模块中很好的分布这些功能,你可以开发一个独立的模块并接下来增长地添加到程序集中。(我并不是说一个多模块程序集易于设计。)
一个众所周知的用于从一组模块创建一个多模块程序集的技术,是基于一种“代言人”的方法:这个模块被分析,并且一个额外的主模块被创建,除了清单表和(可能)一个强名称签名外不携带任何东西。这样一个主模块不携带其自身的任何功能性的或实际的定义,无论什么——它只是功能性模块的开头,“代言人”处理了代表功能性模块的加载器。Assembly Linker工具,分布式的.NET Framework,使用这种技术来创建来自多组非主模块的多模块程序集。
元数据验证规则小结
在本节,我将总结一下包含在清单表中的元数据验证规则。既然其中一些规则和加载器如何工作有着直接的关系,这些相应的检查是在运行期执行的。其它“格式良好”的元数据;违反了其中一条规则就可能在程序执行期间导致相当奇特的效果,但是这并不代表一个崩溃或违背安全性的风险,因此加载器并不执行这些检查。你可以在ECMA/ISO标准第2卷中找到这套完整的验证规则;本节在这里回顾其中一些最重要的。
ILAsm不运行你生成无效的元数据。因此,在编译后小心地检查你的模块是极其重要的。
为了查明一个模块中的任何元数据是否都是有效的,你可以运行PEVerify这个工具,它包括在.NET Framework SDK中,使用到了/MD选项(元数据验证)。可选择的,你可以调用IL反编译器。选择View,MetaInfo,以及Validate,接下来按住Ctrl+M。这两个工具都使用到了内嵌在CLR中的元数据验证(MD Validator)。
Assembly表的验证规则
表中记录的数量必须大于1。这是不能在运行期检查的,因为加载器忽略所有的Assembly记录——第一笔记录除外。(我将为由加载器检查的所有元数据验证规则标记一个“[run time]”标签)
Flags入口必须拥有仅定义在CorHdr.h的CorAssemblyFlags枚举中的位的集合。对于CLR的2.0版本,这个有效的标记是0xC101,并且只有一位(0x0100,可重定向的)可以被详细指明。
Locale入口必须被设置为0或者指向字符串堆中一个非空的字符串,它匹配了一个已知的文化名城。你可以通过调用来自.NET Framework类库德CultureInfo.GetCultures方法来获得一个已知文化名城的清单。
[run time] 如果Locale没有被设置为0,被引用的字符串加上0休止符,必须不能长于1023个字符。
[runtime] Name入口必须指向字符串堆中一个非空的字符串。这个名称必须是该模块的文件名,扩展名、路径和驱动器号排除在外
[runtime] PublicKey入口必须被设置为0或必须在#Blob流中包括一个有效的偏移量。
AssemblyRef表的验证规则
Flags入口可以只拥有最不重要的位集合(相应于afPublicKey值,参见CorHdr.h中的CorAssemblyFlags标记)。
[runtime] PublicKeyOrToken入口必须被设置为0或必须在#Blob流中包括一个有效的偏移量。
Locale入口必须遵守和Assembly表的Locale入口同样的规则(在本章前面讨论过)。
这个表不能有重复的记录——同时地匹配Name、Locale、PublicKeyOrToken以及所有的Version入口。
[runtime] Name入口必须指向字符串堆中一个非空的字符串。这个名称必须是主模块的文件名,扩展名、路径和驱动器号排除在外
Module表的验证规则
[runtime]表中记录的数量必须至少是1。
表中记录的数量必须严格为1。这在运行期是不检查的,因为加载器使用第一个Module记录而忽略其他的。
[runtime] Name入口必须指向字符串堆中一个非空的字符串,加上0休止符长度不能超过511个字符。这个名称必须是该模块的文件名,扩展名包括在内而路径和驱动器号排除在外。
Mvid入口必须指定#GUID流中的一个非0的GUID。这个Mvid入口的值是自动生成的并且不能在ILAsm中显示地指定。
ModuleRef表的验证规则
[runtime] Name入口必须指向字符串堆中一个非空的字符串,加上0休止符长度不能超过511个字符。这个名称必须是该模块的文件名,扩展名包括在内而路径和驱动器号排除在
外。
File表的验证规则
Flags入口可以只拥有最不重要的位集合(相应于afPublicKey值,参见CorHdr.h中的CorAssemblyFlags标记)。
[runtime] Name入口必须指向字符串堆中一个非空的字符串,加上0休止符长度不能超过511个字符。这个名称必须是该模块的文件名,扩展名包括在内而路径和驱动器号排除在外。
[runtime]被Name入口引用的字符串不可以匹配S[N][[C]*],这里
S ::= con | aux | lpt | prn | nul | com
N ::= 0..9
C ::= $ | :
[runtime]HashValue入口必须在#Blob流中保存一个有效的偏移量。
这个表不可以包括重复的数据,它们的Name入口指向了匹配字符串。
这个表不可以包括重复的数据,它们的Name入口指向了匹配这个模块名称的字符串。
ManifestResource表的验证规则
[runtime]Implementation入口必须被设置为0,或者保存一个有效的AssemblyRef或File符号。
[runtime]如果Implementation入口没有保存一个AssemblyRef符号,Offset入口必须在由目标文件的CLR头的Resource数据目录界限内保存一个有效的偏移量(如果目标文件不是一个不带元数据的纯资源文件)。
[runtime]Flags入口必须保存1或2——相应的为mrPublic或mrPrivate。
[runtime]Name入口必须指向字符串堆中一个非空的字符串。
这个表不可以包括重复的数据,它们的Name入口指向了匹配字符串。
ExportType表的验证规则
TypeName和TypeNamespace不可以有任何行匹配Name和Namespace,相应地,属于TypeDef表的任意一行。
Flags入口必须保存CorTypeAttr枚举(参见CorHdr.h)的一个可见性标记(0x0-0x7),或者一个forwarder标记(0x00200000)。
[runtime] Implementation入口必须保存一个有效的ExportedType或File或AssemblyRef符号。在最后一种情形中,forwarder标记必须被设置。
[runtime] Implementation入口不可以保存一个指向这条记录的ExportedType符号。
如果Implementation入口保存了一个ExportedType符号,Flags入口必须保存一个内嵌的范围为2-7的可见性的值。
如果Implementation入口保存了一个File符号,Flags入口必须保存tdNonPublic或tdPublic的可见性的值(0或1)。
[runtime]TypeName入口必须指向字符串堆中一个非空的字符串。
[runtime]TypeNamespace入口必须被设置为0或必须指向字符串堆中一个非空的字符串。
[runtime]由TypeName和TypeNamespace引用的字符串的联合长度。
这个表不可以包括重复的数据,它们的Implementation入口保存了File或AssemblyRef符号而它们的TypeName和TypeNamespace入口指向了匹配字符串。
这个表不可以包括重复的数据,它们的Implementation入口保存了同样的ExportedType符号而它们的TypeName入口指向了匹配字符串。
in regard to 关于
mutual 相互的
Assembly Identity 程序集标识
BuildNumber 内部版本号
RevisionNumber 修订号
publisher policy file 发行者策略文件
hub-and-spoke 集散
there are times 有时候会
pull one’s leg 开某人的玩笑