AssemblyRef元数据表和声明
AssemblyRef(程序集引用)元数据表定义了一个程序集或模块的外部的依赖。主模块和非主要的模块可以——而且通常确实可以——包括这个表。唯一的一个不依赖于其它任何程序集的程序集,并因此而只有一个空的AssemblyRef表,是Mscorlib.dll,.NET Framework类库的根程序集。
AssemblyRef表的列结构如下:
MajorVersion(2字节无符号整数):程序集的主版本。
MinorVersion(2字节无符号整数):程序集的次版本。
BuildNumber(2字节无符号整数):程序集的内部版本号。
RevisionNumber(2字节无符号整数):程序集的修订版本号。
Flags(4字节无符号整数):程序集引用标记,指出这个程序集引用是否保存了一个完整的未经过哈希散列处理的公钥或一个“代理”(公钥符号)。
PublicKey(#Blob流中的偏移量):一个二进制的对象,表示一个强名称程序集的公钥或这个密钥的符号。密钥符号是一个8字节的公钥散列值,而这和元数据符号没有关系。
Name(#String流中的偏移量):被引用的程序集名称,不能为空,不能包括路径和文件名称的扩展名。
Locale(#String流中的偏移量):文化名称。
HashValue(#Blob流中的偏移量):一个二进制的对象,表示一个被引用程序集的主模块中的元数据的哈希散列值。这个值被加载器忽略,所以它可以被安全地省略。
在ILAsm中,这个程序集以下面的方式声明(如例):
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 2:0:0:0
}
程序集声明的ILAsm语法如下:
where <assemblyRefDecl> ::=
| .ver<int32>:<int32>:<int32>:<int32> // Set version numbers
| .publickey = ( <bytes> ) // Set public encryption key
| .publickeytoken = ( <bytes> ) // Set public encryption key token
| .locale <quotedString> // Set assembly locale (culture)
| .hash = ( <bytes> ) // Set hash value
| <customAttrDecl> // Define custom attribute(s)
正如你可能已经注意到的,ILAsm并没有提供在AssemblyRef声明中设置标记的方法。解释是很简单的:唯一与AssemblyRef有关的标记是指出AssemblyRef是否携带了一个完整的未经过哈希散列加密的公钥的标记,并且这个标记只在.publickey指令被使用的时候设置。
当引用一个强名称的程序集时,你需要详细指明.publickeytoken(或.publickey,很少在AssemblyRef中使用)和.ver。在强名称程序集中,对于这个规则唯一的例外是Mscorlib.dll。
如果没有详细指明.locale,引用程序集就被认为是“中性文化”的。
当你需要并排使用同一个程序集的2个或更多版本,一种有趣的情形就会出现。一个程序集由它的名称、版本、公钥(或公钥符号)和文化标志。当你每次引用一个程序集时,列出所有这些标志符是极其麻烦的:“我想要调用来自SomeOtherAssembly程序集的Foo类的Bar方法,而且我想要某某版本号的,文化为nl-BE的,以及……”。当然,如果你不需要并排使用不同的版本,你可以通过名称简单地指定一个程序集。
ILAsm提供了一种AssemblyRef的别名机制来解决这样的情形。AssemblyRef声明可以被扩展为以下方式:
并且无论何时你需要引用这个程序集,你可以使用它的<alias>,正如在这个示例中显示的:
{ .ver 1:1:1:1 }
.assembly extern SomeOtherAssembly as
NewSomeOther
{ .ver 1:3:2:1 }
call int32 [OldSomeOther]Foo::Bar(string)
call int32 [NewSomeOther]Foo::Bar(string)
别名不是元数据的一部分。确切地说,它只是一个简单的语言工具,需要在一些同名的AssemblyRef中标志一个特定的AssemblyRef。IL反编译器为AssemblyRef生成了别名,无论何时它在模块元数据中找到同名的AssemblyRef。
被引用的程序集的自动侦测
IL编译器的2.0版本为你提供了一种引用程序集的方法,而不用详细指定它们的版本、公钥符号以及其它的特性:
.assembly extern <name> as <alias> { auto }
当关键字auto被详细指定时,ILAsm编译器查询GAC并试图查找一个带有特定名称的程序集。如果成功,它就读取程序集特性(版本、公钥、文化)并把这些特性放入生成的AssemblyRef元数据记录中。
注意到这种自动侦测特性只工作于安装在GAC中的被引用程序集。
被引用程序集的特性可能被部分地详细指定并且与自动侦测结合,从而缩小了搜索,例如:
.assembly extern OtherAssembly { .ver 1:3:*:* auto }
前面的指令提示了IL编译器搜索GAC以寻找一个命名为OtherAssembly的程序集,它的主版本为1、次版本为3以及任意的内部版本号和修订版本号。如果这样的一个程序集能够在GAC中找到,接着就获取到它缺少的特性并将其放入到相应的AssemblyRef记录的入口中。
如果多于一个匹配搜索条件的程序集被找到,就会使用其中一个最高的版本。
在这点上,IL编译器区别于其它托管的编译器(VB,C#,VC++),正如那些需要被引用程序集规范的编译器,通过使用文件路径来代替查找GAC。这可能是在开程序员的玩笑,因为CLR加载器总是试图首先加载来自GAC的程序集(正如在下一节所描述的),并且万一有安装在GAC中的被应用程序集和那些被文件路径详细指定的被应用程序集不匹配的情况,应用程序将会通过这些程序集被执行,而不同于那些它被创建时所依赖的程序集。
自动侦测的特性是在IL编译器的2.0版本中引进的。
加载程序搜索程序集
当你在元数据中定义一个AssemblyRef时,你希望加载器准确地找到这个程序集并将其加载到应用程序域中。让我们看一下查找一个外部程序集并将其绑定到引用的程序集的过程。
给出一个AssemblyRef,绑定到那个程序集的过程是受这些因素影响的:
l 应用程序基目录(AppBase),这是一个指向所引用的应用程序位置的URL(这是说,指向你的应用程序所在的目录)。对于执行体而言,这是包括了EXE文件的目录。对于Web应用程序而言,AppBase是由Web服务器定义的应用程序的根目录定义的。
l 版本策略是由应用程序、被引用的共享程序集的发布者或管理者详细指明的。
l 任何额外的搜索路径信息在应用程序配置文件中给出。
l 在配置文件中任何CodeBase位置,由应用程序、发布者或管理者提供。CodeBase是一个指向被引用的外部程序集位置的URL。当存在被引用程序集时就可能有很多CodeBase。
l 无论引用是否指向一个带有强名称的共享程序集或一个私有的程序集。强名称编译集首先会在GAC中被找到。
正如在图6-2所示的,加载器执行以下的步骤来定位一个被引用的程序集:
1.初始化绑定。基本上,这意味着从元数据中取出相关的AssemblyRef记录并看一下其中保存了什么——它外部的程序集名称,它是否为强名称,文化是否被详细指明等等。
2.应用版本策略,这是一些由应用程序、被引用的共享程序集的发布者或管理员生成的语句。这些语句包括在XML配置文件中,并简单地将指向一个程序集特定版本(或一组版本)的引用重定向到一个不同的版本上。
3..NET Framework从一组配置文件中获取它的配置。每个文件表示具有不同左右范围的设置。例如,由CLR的安装包提供的配置文件影响所有使用CLR版本的应用程序。由应用程序提供的配置文件(应用程序配置文件只影响这个应用程序);这个配置文件位于应用程序的目录中。发行者策略文件由共享程序集的发布者提供,它包括了关于程序集兼容性的信息,并重定向程序集的引用到这个共享组件的新版本。当这个共享组件被它的发行者更新时,发行者策略文件通常就会被发布。发行者策略设置优先于应用程序配置文件的设置。管理员策略文件,Machine.config位于CLR安装目录的Configuration子目录中。这个文件包括了由管理员为这台计算机定义的设置,并优先于任何其它的配置文件。在Machine.config文件中指定的覆写会影响所有运行在这台计算机上的应用程序,并且不能被依次覆写。
4.被应用的程序集是强名称的(或者说,AssemblyRef包括了非空的公钥或公钥符号),然后就在GAC中寻找这个程序集。否则,由于弱名称程序集不能安装在GAC中,这一步就会被跳过。如果程序集被找到,这也是最普通的情形,搜索过程就完成了。
5.检查CodeBase。既然CLR知道要寻找哪个版本的程序集,它会开始这个对其进行定位的过程。如果(在配置文件中)提供了CodeBase,它会在执行体加载时直接指向CLR;否则,CLR需要在AppBase中查找(见下一步)。如果由CodeBase详细指明的执行体匹配这个程序集的引用,查找程序集的过程就完成了,并且外部的程序集可以被加载。实际上,即使由CodeBase详细指明的执行体不匹配这个引用,CLR就会停止搜索。在这种情形中,当然,搜索被认为是失败的,并且不会进行程序集加载。
6.探查AppBase。探查涉及到由AppBase定义在目录中的连续的查找,来自同一个XML配置文件的私有的二进制路径(binpath),相关程序集的文化,以及它的名称。AppBase加上在binpath中详细指明的目录形成了一组根目录:{<rootk>, k=1...N}。如果这个AssemblyRef由文化详细指明,搜索就会在目录<rootk>/<culture>中执行,然后是<rootk>/<culture>/<name>;否则,就搜索目录<rootk>,接下来是<rootk>/<name>。当寻找一个私有程序集时,这个过程就会忽略版本数字。如果编译集在探查过程中没有被找到,绑定就失败了。
图6-2 搜索被引用的程序集
当CLR的2.0版本运行在一个64位操作系统上,程序集绑定的问题会由32位或64位的程序集版本的可能存在而加剧。为了解决这个问题,程序集加载器2.0版本的绑定机制使用了下面的程序集分类:
l 平台无关的程序集只能在一个32位或64位的平台上以本地非仿真模式执行;它们不包括任何特定于平台的细节。
l 特定于32位的程序集本身可以在32位平台上执行,在64位平台上这样的程序集需要32位仿真。
l 特定于Itanium的程序集本身可以在Intel Itanium平台上执行,但是不可以在其它平台上执行。
l 特定于64位的程序集本身可以在AMD/Intel X64平台上执行,但是不可以在其它平台上执行。
这样的分类被称为处理器结构,是2.0版本中完整的程序集标志的额外一部分。处理器结构派生于COFF头的Machine入口,Optional NT头的类型,以及CLR头标志的两个最不重要的位(标志ILONLY和32BITREQUIRED)(参见第四章获取更多细节):
l 平台无关的程序集,Machine=I386,32位Optional头,以及CLR头标志的两个最不重要的位被设置为ILONLY(0x1)。
l 特定于32位的程序集,具有相同的Machine和Optional头,以及CLR头标志的两个最不重要的位被设置为32BITREQUIRED| ILONLY(0x3),32BITREQUIRED(0x2)或0。
l 特定于Itanium的程序集,Machine=IA64,64位Optional头,以及CLR头标志不起任何作用。
l 特定于64位的程序集,Machine=AMD64,64位Optional头,以及CLR头标志不起任何作用。
你应该小心声明平台无关的程序集。为了做到真正的平台无关,程序集不应对指针大小作出假设,不应有非托管的输入和输出,不应有内嵌的本地代码,以及不应有线程本地化存储(.tls区段),并且它不能引用特定于平台的程序集或特定于平台的非托管DLL。最后一个条件是其中最糟糕的,因为它是可传递的。很多次程序员写出一个应用程序(EXE)将其声明为平台无关的,没想到它在64位平台上竟会崩溃:应用程序,是平台无关的,创建了一个64位的进程并接下来试图加载一个32位特定的被引用的程序集到这个64位的进程中。Kaboom!或者它试图加载一个平台无关的程序集A,这将依次引用程序集B,而B恰好P/Invoke一个32位的非托管DLL(参见第18章)。Kaboom!好的一面是这样的问题总是立刻就会被发现,而不是在这个应用程序被跳过之后。
CLR的2.0版本认为所有用于1.0和1.1版本的程序集是32位特定的程序集。这是公平的:CLR的1.0和1.1版本不支持64位平台。为1.0和1.1版本生成的程序集由元数据流的头标志(参见第5章);在这个头中详细指明的版本是1.0——对于1.0和1.1版本的程序集;2.0——对于2.0版本的程序集。
Module元数据表和声明
Module元数据表包括了一笔单独的记录,它提供了当前模块的标志。这个表的类结构如下:
Generation(2字节无符号整数):只在运行时的“编辑并继续”模式中使用。
Name(在#String流中的偏移量):模块名称,这与执行体文件的名称是相同的,包括它的扩展名但是不包括路径。以UTF-8编码的长度不应该超过512字节,0休止符也计算在内。
Mvid(在#GUID流中的偏移量):一个全局唯一的标志符,在它生成的时候将其分配到模块。
EncId(在#GUID流中的偏移量):只在运行时的“编辑并继续”模式中使用。
EncBaseId(在#GUID流中的偏移量):只在运行时的“编辑并继续”模式中使用。
由于只有一个模块记录的入口可以被显示地设置(Name入口),在ILAsm中这个模块的声明是非常简单的:
.module <name>
ModuleRef元数据表和声明
ModuleRef元数据表包括了在当前模块中引用到的其它模块的描述符。这组“其它的模块”包括了托管的和非托管的模块。
相关的托管模块是当前程序集的其它模块。在ILAsm中,它们应该被显示地声明,并且它们的声明应该和File的声明(在下面的章节讨论)是成对的。IL编译器并不验证被引用的模块在编译器是否存在。
在ModuleRef表中描述的的非托管模块是简单的非托管DLL,包括了从当前模块调用的方法——使用平台调用机制P/Invoke,在第18章讨论。这些ModuleRef记录通常和File的记录是不配对的。它们不需要在ILAsm中显示地声明,因为DLL名称是P/Invoke规范的一部分,因此IL编译器自动发布相应的ModuleRef记录。
然而,这里有一个原因,对一个引用了非托管模块的ModuleRef记录和File记录进行配对:如果你想要这个非托管的DLL成为你的部属的一部分,你就应该这么做。在这种情形,非托管的DLL将和托管模块放在一起以组成你的程序集,并且也不必在路径上被发现。
一笔ModuleRef记录只包括一个入口,Name入口,这是#String流中的一个偏移量。在ILAsm中,ModuleRef声明并不比Module的声明更加复杂:
.module extern <name>
正如Module的情形,ModuleRef中的<name>是执行体文件的名称,带有它的扩展名而不包括路径,在UTF-8编码中不超过512字节。