(翻译)《Expert .NET 2.0 IL Assembler》 第五章 元数据表的组织 5.3 RID和符号
RID和符号
记录的索引和符号,是无符号整数值,用于索引元数据表中的记录。RID是简单的索引,只适用于显示指定的表,而符号则携带了识别它们所引用的元数据表的信息。
RID
RID是一个识别符,是一个在包括了记录的表中从1开始的行号。有效的RID的范围从1延展到地址表中的记录的数量,1和这个数量值也包括在内。RID只用于元数据的内部;元数据发布和API重获并不将RID设置为参数。
RID列的类型编码(0-63)是一个从0开始的表的索引值。因此,这个列的类型识别了引用到的表,而这个表的单元格的值则识别了被引用到的记录。这样是工作良好的,只要我们了解一个特定的列总是引用一个特定的表而不是其它的表。到现在为止,我们假设只是将RID与表的鉴定相结合。
符号
实际上,我们是可以这么做的。结合的鉴定实体,被称为“符号”,用于所有的元数据API和IL指令中。符号是一个4字节无符号整数,其中一个最重要的字节携带了从0开始的表的索引(与内部的元数据RID类型相同)。剩下的3个字节则是留给RID的。
然而,在符号类型和内部元数据RID类型之间存在着一个重大的不同,尽管内部RID类型覆盖了所有的元数据表,而符号类型则仅定义在这些标的一个有限的子集中,正如表5-9所记录的。
表5-9 符号类型以及它们引用到的表
符号类型 |
值(RID | (Type<<24)) |
引用表 |
mdtModule |
0x00000000 |
Module |
mdtTypeRef |
0x01000000 |
TypeRef |
mdtTypeDef |
0x02000000 |
TypeDef |
mdtFieldDef |
0x04000000 |
Field |
mdtMethodDef |
0x06000000 |
Method |
mdtParamDef |
0x08000000 |
Param |
mdtInterfaceImpl |
0x09000000 |
InterfaceImpl |
mdtMemberRef |
0x0A000000 |
MemberRef |
mdtCustomAttribute |
0x0C000000 |
CustomAttribute |
mdtPermission |
0x0E000000 |
DeclSecurity |
mdtSignature |
0x11000000 |
StandAloneSig |
mdtEvent |
0x14000000 |
Event |
mdtProperty |
0x17000000 |
Property |
mdtModuleRef |
0x1A000000 |
ModuleRef |
mdtTypeSpec |
0x1B000000 |
TypeSpec |
mdtAssembly |
0x20000000 |
Assembly |
mdtAssemblyRef |
0x23000000 |
AssemblyRef |
mdtFile |
0x26000000 |
File |
mdtExportedType |
0x27000000 |
ExportedType |
mdtManifestResource |
0x28000000 |
ManifestResource |
mdtGenericParam |
0x2A000000 |
GenericParam |
mdtMethodSpec |
0x2B000000 |
MethodSpec |
mdtGenericParamConstraint |
0x2C000000 |
GenericParamConstraint |
这22个没有与符号类型联合的表,并不打算通过元数据API或IL代码被“外部”访问到。这些表具有辅助的或中间的本性并只能被间接访问,通过包括在“暴露的”表中的引用,后者是和符号类型联系在一起的。
这些符号的有效性可以简单地定义为:一个有效的符号具有一个来自表5-9的类型,以及一个有效的RID——就是说,一个范围从1到这个特定类型的记录数量的RID。
一个额外的符号类型,完全不同于在表5-9中列出的类型,这就是mdtString(0x70000000)。这个类型的符号用于涉及到存储在#US流中的用户自定义的Unicode字符串。
用户自定义字符串符号的类型组件和RID组件都不同于那些元数据表的符号。用户自定义字符串符号的类型组件(0x70)不会对列的类型做些什么(最大的列类型是103=0x67),没有什么令人惊讶的,考虑一下没有列类型对应到#US流的偏移量。正如元数据表从来不引用到用户自定义的字符串,为字符串定义一个列的类型并不是必要的。另外,用户自定义字符串符号的RID组件并不表示一个RID,这是因为没有表被引用到。代替的,用户自定义字符串符号的3个低位字节保存了在#US流中的偏移量。这种布局的副作用是,你不能获得大于16MB的#US流,或者更精确地说,所有的用户自定义字符串——最后一个字符串除外,必须适合16MB并小于1B。你可以将最后一个字符串制作成任何你想要的,但是它必须开始于2^24以下的偏移量,或者换句话说,在16MB的边界以下。
用户自定义字符串符号的有效性的定义就更复杂了。RID(或偏移量)组件,只有这个RID大于0并且它定义的字符串开始于一个4字节的边界并且完全包括在#US流中,才是有效的。最后一个条件按照下列方式被检查:由符号的RID组件详细指明的偏移量字节,被认为是这个字符串的压缩长度。(不要忘记#US流失一个blob堆。)如果偏移量的总合和压缩长度的大小产生一个4字节的边界,并且如果这个综合加上计算得到的长度,在#US流的大小范围内,那么一切就是成功的并且符号是有效的。
编码符号
到目前为止,讨论集中在符号的“外部”形式。你有权猜想在元数据内部使用的符号的“内部”形式,是不一样的——而且,确实是这样的。
为什么外部形式不能也像内部形式一样使用呢?因为外部的符号是巨大的。想象一下当我们为每个小得可怜的字节而奋战的时候,每个符号都要有4个字节。(位宽!不要忘记位宽!)压缩么?唉,让这种类型组建占据了最重要的字节,外部的符号代表了非常巨大的无符号整数并因此不能被有效的压缩,即使它们的中间字节都由0组成。我们需要一个新鲜的方法。
符号的内部编码是基于一个简单的想法:一个列必须得到一个符号类型只有当它可能引用到一些表的时候(只引用了一个表的列具有相应的RID类型。)但是任何这样的列肯定不需要引用到所有的表。
因此我们的第一个任务是识别那一个群组中的表的每一个这样的列可能引用并形成一套这样的群组。让我们为每个群组分配一个数字,这将是这个列的一个编码符号类型。编码符号类型占据了64-95的范围,因此我们可以一直定义到32个群组。
现在每个群组都包括了两个或者更多的表的类型。让我们在这个群组中对它们进行枚举,并看一下为了这个枚举我们将需要有多少个位?这个位的数量将会是这个群组的一个特征,并因此也是相应的编码符号类型的特征。这个分配给一个群组中的表的数字被称为标记。
这个标记扮演的角色处略的等同于外部符号的组件类型。但是,不希望在此创建全都是0的巨大的符号。这次我们将不会把这个标记放到这个符号的最重要的位上,而是放在最不重要的位上。接下来让我们将RID左移n位,并添加这个左移的RID到这个标记上,这里n是这个标记的位宽。现在我们已经得到了一个编码符号。例如,一个未编码的TypeSpec符号0x1B000123将会被转换为编码的TypeDefOrRef符号0x0000048E。
编码符号的大小是什么呢?我们知道每个群组有哪些元数据表,并且我们还知道每个表的记录数量,因此我们知道在这个群组中的可能的最大RID。比方说,例如,我们可能需要m位来对最大的RID进行编码。如果我们能够用最大RID(m位)和标记(n位)来填充一个2位的无符号整数(16位),我们就成功了,并且这个群组的编码符号的大小将会是2字节。如果我们不能这么做,那就是运气不好而要为这个群组使用4位的编码符号。不,我们不会考虑3位——这是不合身的。
总结一下,一个编码符号类型具有以下特性:
l 引用表的数量(schema的一部分)
l 引用表的ID组成的数组(schema的一部分)
l 标记的位宽(schema的一部分,派生于引用表的数量)
l 编码符号大小,2或4字节(在元数据打开的时候计算来自标记宽度和引用表的最大记录数量)
表5-10列出了定义在现有的CLR发布版本中的元数据schema中的13种编码符号类型。
表5-10 编码符号类型
编码符号类型 |
标记 |
TypeDefOrRef(64):3个被引用的表,标记大小为2 |
|
TypeDef |
0 |
TypeRef |
1 |
TypeSpec |
2 |
HasConstant(65):3个被引用的表,标记大小为2 |
|
Field |
0 |
Param |
1 |
Property |
2 |
HasCustomAttribute(66):3个被引用的表,标记大小为2 |
|
Method |
0 |
Field |
1 |
TypeRef |
2 |
TypeDef |
3 |
Param |
4 |
InterfaceImpl |
5 |
MemberRef |
6 |
Module |
7 |
DeclSecurity |
8 |
Property |
9 |
Event |
10 |
StandAloneSig |
11 |
ModuleRef |
12 |
TypeSpec |
13 |
Assembly |
14 |
AssemblyRef |
15 |
File |
16 |
ExportedType |
17 |
ManifestResource |
18 |
GenericParam(只适于2.0版本) |
19 |
GenericParamConstraint(只适于2.0版本) |
20 |
MethodSpec(只适于2.0版本) |
21 |
HasFieldMarshal(67):2个被引用的表,标记大小为1 |
|
Field |
0 |
Param |
1 |
HasDeclSecurity(68): 3个被引用的表,标记大小为2 |
|
TypeDef |
0 |
Method |
1 |
Assembly |
2 |
MemberRefParent(69): 5个被引用的表,标记大小为3 |
|
TypeDef |
0 |
TypeRef |
1 |
ModuleRef |
2 |
Method |
3 |
TypeSpec |
4 |
HasSemantics(70): 2个被引用的表,标记大小为1 |
|
Event |
0 |
Property |
1 |
MethodDefOrRef(71): 2个被引用的表,标记大小为1 |
|
Method |
0 |
MemberRef |
1 |
MemberForwarded(72): 2个被引用的表,标记大小为1 |
|
Field |
0 |
Method |
1 |
Implementation(73): 3个被引用的表,标记大小为2 |
|
File |
0 |
AssemblyRef |
1 |
ExportedType |
2 |
CustomAttributeType(74): 5个被引用的表,标记大小为3 |
|
TypeRef(已经废弃,不能再使用) |
0 |
TypeDef(已经废弃,不能再使用) |
1 |
Method |
2 |
MemberRef |
3 |
String(已经废弃,不能再使用) |
4 |
ResolutionScope(75): 4个被引用的表,标记大小为2 |
|
Module |
0 |
ModuleRef |
1 |
AssemblyRef |
2 |
TypeRef |
3 |
TypeOrMethodDef(76)(只适于2.0版本): 2个被引用的表,标记大小为1 |
|
TypeDef |
0 |
Method |
1 |
编码符号类型的范围(64-95)提供了空间用以将来增加另外19种类型,这应该是会变成必须的。
编码类型是元数据内部事件的一部分。IL编译器,就像其它编译器一样,从来不处理编码符号。编译器和其它工具通过元数据的导入和发布的API来读取并发布元数据,直接或者通过由.NET框架类库提供的托管包装器——System.Reflection用于元数据的导入,而System.Reflection.Emit用于元数据的发布。元数据的API自动地将标准的4字节符号转换为编码符号,反过来转换也可以。IL代码也只使用这个标准的4位符号。
虽然如此,预先的定义对我们仍然是有用的,有两个原因。首先,当我们在后面的章节讨论独立的元数据表的时候,我们将需要它们。其次,这些定义提供了一个好的暗示——关于元数据表之间的关系特性。