每个表都会被分配一个唯一的值,或者更精确的说,valid字段中的一个位。从而,由于valid字段被声明为一个长整数(long),所以最多只能有64个不同的表类型。
在上一章的结尾,我们顺便提及了存在于文件中的表的名称。在本章,我们将详细研究在最小的exe文件中这些表的内容。
Module表
第一个表,由位0所标识,是Module表。由于IL中的.module指令的存在,这一行被作为IL中的.module指令插入到Module表中。在Module表上的限制是——它只适用于包括单独一行。这是因为一个模块只能代表一个单独的exe或dll文件。
表中的字段如下所示:
下面给出的是xyz函数,用来显示每个字段的内容。修改最后一个a.cs程序中xyz函数的内容——通过添加下面的语句:
public void xyz() { int offs = tableoffset; int Generation = BitConverter.ToUInt16(metadata, offs); offs += 2; int Name = BitConverter.ToUInt16(metadata, offs); offs += 2; int Mvid = BitConverter.ToUInt16(metadata, offs); offs += 2; int EncId = BitConverter.ToUInt16(metadata, offs); offs += 2; int EncBaseId = BitConverter.ToUInt16(metadata, offs); Console.WriteLine("Generation: {0}", Generation); Console.WriteLine("Name :{0} {1}", GetString(Name), Name); Console.WriteLine("Mvid :#GUID[{0}]", Mvid); DisplayGuid(Mvid); Console.WriteLine(); Console.WriteLine("EncId :#GUID[{0}]", EncId); Console.WriteLine("EncBaseId :#GUID[{0}]", EncBaseId); }
Output
Generation: 0
Name :b.exe 10
Mvid :#GUID[1]
{A921C043-32F-4C5C-A5D8-C8C3986BD4EA}
EncId :#GUID[0]
EncBaseId :#GUID[0]
每个表都支配了它自身的内部结构。Module表开始于一个带有2个字节的称为Generation的字段,它的值总是被设置为0。我们相信到现在,你已经掌握了使用BitConverter类的方法的机制。
第2个字段是名称字段,它包括了指向字符串表的索引。这里,它的值为10,因此,出现在Strings流中的数据前第10个字节处的字符串,是这个模块所代表的名称。
索引被设置为2个字节,因为#~流头中的heap字段成员包括了值0。它是距离头的开始位置的第7个字节。在8个位之外,目前只有3个位是可用的。
如果设置了第一个位,就表示Strings流中所有的索引都是4字节宽的。由于在这个例子中没有设置它,索引的大小就被设置为2个字节。第2个位是用于GUID流的。第3个流没有使用到。第4个位是用于Blob流的。
因此,一个结构成员每次包括一个索引值,检查heapsize字段来决定设置为2字节还是4字节。基本规则是——如果流的大小大于64k,索引就是4字节;否则,就是2字节。
这就为提高元数据世界的创建效率提供了有力的证据。
使用GetString函数,可以获取到开始于字符串堆区域中第10个位置上的字符串。在这个例子中,被取出的字节表示b.exe的名称。
因此,Name字段是String流中的索引。它不能包括null值。这个名称的格式是由常量MAX_PATH_NAME控制的。格式仅由文件名称和扩展名组成。没有其它信息,如路径名称或驱动器名称,是允许的。
名为Mvid的字段是GUID堆中的一个索引。因此,适用DisplayGuid函数来显示GUID。在我们的输出中显示的值可能完全不同于闪现在屏幕上的值。如果你再次编译b.exe程序,你将会注意到GUID的值也会发生改变。
每当编译器在.NET Framework下创建一个exe文件,它就会生成一个新的GUID。这有助于区别两个不同版本的模块。从而,Mvid列唯一标识了这个列的一个实例。
文档还暴露了这样的事实——所使用的算法是在ISO/IEC 11578:1996(附录A)中指定的。在CORBA(Common Object Request Broker Architecture)和RPC(Remote Procedure Call)的世界中,称为UUID或Universally Unique Identifier,而在COM世界中则使用CLSID、GUID和IID。
Mvid不是由VES(Virtual Execution System)使用的。其它程序(如调试器)可能使用这样的事实——每个exe文件具有一个内嵌其中的唯一数字。最后,虽然Mvid没有被使用,但是它不能是一个值为null的GUID。
下面的两个字段是GUID堆中的索引。它们是保留的,值都为0。
TypeRef表
xyz函数现在显示了TypeRef表的内容。这个表有3行,其中每行的大小都是6字节。还有,这个表在索引的位置上为1,这表示在valid表字段向量中的第2位为on。
a.cs
public bool tablepresent(byte i) { int p = (int)(valid >> i) & 1; byte[] sizes = { 10, 6, 14, 2, 6, 2, 14, 2, 6, 4, 6, 6, 6, 4, 6, 8, 6, 2, 4, 2, 6, 4, 2, 6, 6, 6, 2, 2, 8, 6, 8, 4, 22, 4, 12, 20, 6, 14, 8, 14, 12, 4 }; for (int j = 0; j < i; j++) { int o = sizes[j] * rows[j]; tableoffset = tableoffset + o; } if (p == 1) return true; else return false; }
Output
Row[1]
AssemblyRef[1] token=0x6
Name :Object,0x20
Namespace :System,0x19
Row[2]
AssemblyRef[1] token=0x6
Name :DebuggableAttribute,0x49
Namespace :System.Diagnostics,0x36
Row[3]
AssemblyRef[1] token=0x6
Name :Console,0x5F
Namespace :System,0x19
除了xyz函数之外,还会引进一个新的名为tablepresent的函数。这个函数执行两个任务:
首先,它揭示了在索引位置作为参数提供的表,是否存在。
其次,也是更重要的,它将tableoffset变量设置为这个表在数组中开始的位置。
现在,让我们看一下这个函数的工作。我们传递了一个值1——也就是TypeRef表的索引——到接受它的函数的参数i中。
public bool tablepresent(byte i)
函数中的第一行右移位valid字段i次,然后,它执行和值1的AND位运算。
从而,它只检查第1位是否为on。如果变量p中的合成值是1,那么就会返回布尔值true;否则,就返回布尔值false。
if (p == 1) return true; else return false; int p = (int)(valid >> i) & 1; … … if ( p == 1) return true; else return false;
变量tableoffset的位置以不同的方式处理。开始,变量tableoffset指向元数据数组中tabledata节的开始位置。
出现在valid字段中的表的全部数量可能会改变。因此,我们有一个名为sizes的数组,它指定了表中每行的大小。
byte[] sizes = {
10, 6, 14, 2, 6, 2, 14, 2, 6, 4, 6, 6, 6, 4,
6, 8, 6, 2, 4, 2, 6, 4, 2, 6, 6, 6, 2, 2, 8,
6, 8, 4, 22, 4, 12, 20, 6, 14, 8, 14, 12, 4
};
我们注意到,在前面的程序中,第1个表Module的行的大小是10,第2个表TypeDef的行的大小是6,等等。想到它还没有被明确地文档化,每个表的大小是可以完全被确定的。然而,当表中行的数量超过64k时,大小会发生改变,会导致4字节索引取代2字节。由于我们的程序太小了,所以这样的情形不会发生。
因此,名为sizes的数组会被表类型中的行的大小填充。
一旦达到了这一步,for循环会迭代N次,这里N等于存在的表的数量。在这个例子中,由于变量i的值是1,循环只会重复一次。在循环中,sizes数组中的行的大小乘以rows数组中相应的行的编号。在for循环中,变量j代表这两个数组中的偏移量,也表示表的id的偏移量。
for (int j = 0; j < i; j++) { int o = sizes[j] * rows[j]; tableoffset = tableoffset + o; }
作为上面计算的结果,变量o将描述由每个表占据的空间,它会被添加到变量tableoffset中。
在xyz函数中,函数tablepresent的返回值会被验证,如果值为true,那么这个表就是存在的,这是很显然的。从而,程序会向前执行以展现它的内容。
实现for循环来验证每一行中的值。由于TypeRef表有3行,所以loop会重复3次。
TypeRef表在下面的列中有6个字节的大小:
让我们对此进行向后检查。所有的列都是String堆中的一个索引。从而,每个列都会占据两个字节。最后一列涉及命名空间,第2列指向这个命名空间中的类。输出显示——程序中一共涉及了3个类或类型,即:
System.Object,System.Console和System.Diagnostics.DebuggableAttribute。
为了实例化我们的成果,我们再次提到文件b.cs。
b.cs
public class zzz { public static void Main() { System.Console.WriteLine("hello"); } }
.NET世界中的任何事物都派生于object类。从而,在内部,zzz类是从System.Object派生的。取决于此,object类会在文件中出现。Console类的出现是因为我们显式地调用了它。然而,没有指向DebuggableAttribute类的引用。所以,它是从哪来的呢?
在文件中注释WriteLine函数并再次运行a.exe文件。你将会注意到,在TypeRef表中指向Console类的引用消失了。从而,当你调用不同类的方法时,同样可以把每一个具有唯一类名的行添加到TypeRef表中。
TypeRef表由这个程序中相关的所有类型(type)和类(class)组成。词语“类型”和“类”可以交替使用。这不仅应用于由类调用的静态方法,还应用于任何相关的类,包括使用了new进行实例化的那些类。
在System类和用户自定义类之间是没有区别的。因此,当/R选项应用于其它dll中相关的类时,这个表还会带有一个项。仅仅定义一个变量还会导致额外添加“命名空间-类”的名称到TypeRef表中。从而,简而言之,exe文件中每个外部的相关的类型或类,都可以在TypeRef表中找到一个项。
不幸的是,C#编译器还带来了一些代码,它们与DebuggableAttribute有关。从而,这个类的名称也会被合并到表中。
现在,让我检查行中开始的2个字节。这个短整数被称为“解析作用域”(resolution scope)或“解析作用域的编码索引”(resolution scope coded index)。这个字段具有下列4个值之一:Module、ModuleRef、AssemblyRef或TypeRef
范围解析的值是上面表中的任何一个索引。这个值表示了“命名空间-类”的作用域。开始的2个字节,以短整数的形式,标识了应用于上面所提到的表的作用域。
作用域的定义将实体的使用只约束为固定的几个。例如,本地变量的作用域就是这个方法被创建的地方。
在程序中,要核实开始的2个字节。从而,我们使用和3的AND位运算。这就证实了所有这3个类都具有Assembly的作用域,Assembly是.NET世界中最大的实体。剩下的6个位提供了表中实际的索引值,它们是由开始的2个位标识的。
那么,这些位右移2位以提供指向AssemblyRef表的索引。我们将在后期相当详细地阐述这个表。
当程序中涉及的类型源于另一个程序集时,就会显示AssemblyRef标记。
name字段是String堆中的一个索引。这个字符串不能是一个null字符串。这个字符串的长度是由MAX_CLASS_NAME限定的。相反,namespace字段允许为null。每个名称应该是一个有效的CLS标识符。
最后,你可能会观察到,具有相同解析作用域、名称和命名空间的两行,显然是不能同时存在的。
涉及所有这三个类的AssemblyRef表,稍后将会说明,因为它是一个独立的表。
TypeDef表
a.cs
using System; using System.Reflection; ... ... public void xyz() { bool b = tablepresent(2); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[2]; k++) { TypeAttributes flags = (TypeAttributes)BitConverter.ToInt32(metadata, offs); offs += 4; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int nspace = BitConverter.ToInt16(metadata, offs); offs += 2; int cindex = BitConverter.ToInt16(metadata, offs); offs += 2; int findex = BitConverter.ToInt16(metadata, offs); offs += 2; int mindex = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row:{0}", k); Console.WriteLine("Flags : {0}", flags); Console.WriteLine("Name : {0}", GetString(name)); Console.WriteLine("NameSpace : {0}", GetString(nspace)); Console.Write("Extends:"); int u = cindex & 3; if (u == 0) Console.Write("TypeDef"); if (u == 1) Console.Write("TypeRef"); if (u == 2) Console.Write("TypeSpec"); Console.Write("[{0}]", cindex >> 2); Console.WriteLine(); Console.WriteLine("FieldList Field[{0}]", findex); Console.WriteLine("MethodList Method[{0}]", mindex); } } }
Output
Row:1
Flags : Class
Name : <Module>
NameSpace :
Extends:TypeDef[0]
FieldList Field[1]
MethodList Method[1]
Row:2
Flags : AutoLayout, AnsiClass, NotPublic, Public, BeforeFieldInit
Name : zzz
NameSpace :
Extends:TypeRef[1]
FieldList Field[1]
MethodList Method[1]
为了避免编译器错误,必须在程序的开始添加using System.Reflection;语句。
按照顺序,紧跟在TypeRef表之后的是TypeDef表。因此,它分配到的值是2。Tablepresent函数保留而不做修改。xyz函数稍作修改以适应TypeDef表中的字段。
简而言之,TypeDef表存储了在我们的程序集中创建的每个类型和类。类型可以是类、接口、结构、枚举等等。
你可以在文件b.cs中添加接口、结构或枚举。然后,你就能确定会为每个附加的新类型添加一个新的行。
我们已经揭示——类型和类是相同的。然而,我们现在放大类型的定义以包括上面提到的实体。TypeDef结构是下面字段的产物:
开始的4个字节是Flags字段,我们对其进行简短说明。它们在类型的名称之后。第1行具有类型名称<Module>以及标志Class。第2行具有类型名称zzz,它是在这个程序中类的名称。从而,TypeDef表中的行或类型依赖于在这类中创建的实体。然而,我们从不创建第1个类型,也就是<Module>。
第1行象征着一个名为<Module>的伪类。它包括了全局的或在模块级别创建所有的函数和变量。它适合成为所有这些实体的父亲。在C#语言中,不允许我们在类的外面创建任何事物,然而,在C++中,允许我们创建全局函数和全局变量,它们都是被外包在名为Class的类中的。
Name字段紧跟在Namespace的索引之后。名为<Module>的类不属于任何一个命名空间,因为这样的一个概念在C#语言中是不存在的。类zzz的命名空间也是不存在的。因此,String堆中的索引值为0。
下一个字段称为Extends字段。这个字段是一个代码索引,它可以是下面三个值的其中一个:TypeDef、TypeRef 或TypeSpec。它涉及了这个值是哪个表的索引。在名为<Module>的类中,它是TypeDef表中的一个索引。然而,由于这个索引的值是0,所以它是一个无效的索引。在它背后的基本原理是,这个名为<Module>的类不是从任何类中派生出来的。
这个字段在第2行中是很容易理解的。.NET世界中所有的类都是从System.Object中派生的。因此,可以认为zzz类也是从中派生的。从而,代码索引指向TypeRef表中的第一个索引。
在前面的例子中,TypeDef表的这3行中的第1行,揭示了不同的类型引用。这里,其中一个表示System.Object类。从而,代码索引使这个表,还有表中特定的行都为人所知。最后我们将写一个程序来交叉引用所有这些完全不同的表。
下面两个字段分别是字段表和方法表中的索引。我们将在适当的内容中提供这些表的说明。
在我们结束TypeDef表的介绍之前,有必要理解TypeAttributes这个整数。Reflection命名空间定义了一个称为TypeAttributes的枚举。TypeDef表的第一个字段只是一个整数,其中每个位都与应用到这个类声明的特性有关。我们只使用到了ToString函数来显示这些特性。
为了满足你对知识的渴望,你可以看一下名为corhdr.h的文件,它位于文件夹Program files-Microsoft Visual Studio.Net-FrameWorkSDK-Include中。这个头文件具有名为CorTypeAttr的枚举,它们拥有表示这个类的特性的位。如果第1位是on,那么就表示它是一个公共访问类。如果第6为是on,那么就表示这个类是一个接口。
最清晰的解决方案是使用枚举的ToString函数,正如我们这里所统一的那样。输出表示了这个名为<Module>的特殊类,只是由一个单独的名为Class的类标记的。
然而,类zzz设置了很多标志。让我们理解一下这些不同的位实际上表示什么。
我们已经设置了标志AutoLayout。它指定了CLR或由微软提供的代码,将负责展开类的字段。AnsiClass并不适用于C#代码,由于它基本上处理将C++指针转为一个字符串的互操作,或者,按照ANSI或Unicode术语,一个LPTSTR。
最后,BeforeFieldInit标志要求CLR在第一个静态字段被访问之前就初始化类的成员。这是关于这个标志的最简洁的解释。稍后,当我们遇到其它类型设置的标志(如结构、接口或枚举)时,我们将回到这个标志字段上来。
Method表
a.cs
public void xyz() { bool b = tablepresent(6); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[6]; k++) { int rva = BitConverter.ToInt32(metadata, offs); offs += 4; MethodImplAttributes impflags = (MethodImplAttributes)BitConverter.ToInt16(metadata, offs); offs += 2; MethodAttributes flags = (MethodAttributes)BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int signature = BitConverter.ToInt16(metadata, offs); offs += 2; int param = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("RVA :{0}", rva.ToString("X")); Console.WriteLine("Name : {0}", GetString(name)); Console.WriteLine("ImpFlags :{0}", impflags); Console.WriteLine("Flags :{0}", flags.ToString("X")); Console.WriteLine("Signature: #Blob[{0}]", signature); Console.WriteLine("ParamList: Param[{0}]", param); Console.WriteLine(); } } }
Output
Row 1
RVA :2050
Name : Main
ImpFlags :Managed
Flags :00000096
Signature: #Blob[10]
ParamList: Param[1]
Row 2
RVA :2068
Name : .ctor
ImpFlags :Managed
Flags :00001886
Signature: #Blob[14]
ParamList: Param[1]
我们将要处理的下一个元数据表,是valid表中的第7个位置上的那一个。它就是Method表,从而具有索引6。在模块中创建的每个方法都在这个表中有一行。输出证实了两个方法的存在。
在讨论RVA一切都是怎么回事之前,让我们首先研究一下Name字段,它是name表中的索引。第1行有一个名为Main的方法,第2行有一个名为.ctor的方法。你肯定想知道关于这个方法是起源自哪里的。
名称开始于一个句点的所有方法,它们的创建都是不需要人为干涉的。从编译器的观点出发,这样的方法都是有特殊意义的。
一个没有构造函数的类,总是有一个默认的无参构造函数。这个默认的构造函数被命名为.ctor。
从而,你当然会明白并称赞——C#语言中的大部分可以通过解析元数据来了解到。
RVA字节就是相对虚地址,这是一个数字,指向了方法的可执行代码的开始位置。输出显示了第一个函数Main开始于内存位置2050。
为了到达代码在磁盘上开始的位置,我们首先计算出0x2050 和0x2000(节的对齐)之间的差。结果是50,然后将它添加到512(文件对齐)。最后的输出是592,这是Main方法在磁盘上开始的位置。
这段代码,和其它代码具有很相似的风格,开始于一个头,在IL中后面紧跟着一些字节。这些字节依次与元数据表有关。下一本元数据系列丛书将说明在ILDasm中反汇编的机制。
第2个字节由MethodImplAttributes标志组成。这些标志决定了应用于方法上的特性。
这里有两种基本类型的方法,即托管方法和非托管方法。当在C#中使用指针时,就要使用非托管方法,从而不需要进行代码验证。
文档包括了关于每个位和及其表示的细节。
如果第1位是off,那么方法实现就是CIL和托管的。如果是on,那么它就表示一个本地方法。
第2位,又被称为OPTIL,是保留的,并总是分配到一个0值。这就指定了代码只能由.NET架构使用,而不能由像我们这样的开发者来使用。
值3表示这个方法是一个运行时方法,指定了在文件中不存在代码,因为它不是由运行时提供的。事件会以相同的方式被处理。
值0x20通知我们——这个方法是单线程的或同步的。C#的锁语句就是为此而开发的。
值0x08表示——这个方法是不能被内嵌的。CodTypeMask,它的值为3,指定了这些标记。这些标记表示了代码的类型。值IL或0暗示着个方法的代码是在IL中。值0x1000代表一个内部的调用,它是保留的,仅仅用于内部使用。
范围检查的值是0xffff。PreserveSig通知我们方法签名按照所告知的那样导出,并且不能破坏HRESULT转换。HRESULT是COM世界中的返回类型。
让我们看看下面这个程序,来解释方法特性的第2个标记字段。这个字段完全不同于我们前面介绍的方法实现特性。
a.cs
public void xyz() { bool b = tablepresent(6); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[6]; k++) { int rva = BitConverter.ToInt32(metadata, offs); offs += 4; MethodImplAttributes impflags = (MethodImplAttributes)BitConverter.ToInt16(metadata, offs); offs += 2; short flags = BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int signature = BitConverter.ToInt16(metadata, offs); offs += 2; int param = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Name : {0}", GetString(name)); Console.WriteLine("Flags :{0}", flags.ToString("X")); Type t = typeof(System.Reflection.MethodAttributes); FieldInfo[] f = t.GetFields(BindingFlags.Public | BindingFlags.Static); for (int i = 0; i < f.Length; i++) { int fv = (int)f[i].GetValue(null); if ((fv & flags) == fv) Console.Write(" {0} {1} ", f[i].Name, fv.ToString("X")); } Console.WriteLine(); } } }
Output
Row 1
Name : Main
Flags :96
PrivateScope 0 FamANDAssem 2 Family 4 Public 6 Static 10 HideBySig 80 ReuseSlot 0
Row 2
Name : .ctor
Flags :1886
PrivateScope 0 FamANDAssem 2 Family 4 Public 6 HideBySig 80 ReuseSlot 0 SpecialName 800 RTSpecialName 1000
关于方法特性的一个小问题是,不同于实现特性,MethodAttributes 枚举的ToString方法只显示了16进制值,每个标志变量都贯彻相同的概念。每个位都表示被设置的某个属性或特性,如static、private等等。
文档没有给我们留下关于这些位的启迪。因此,我们放弃通过写一个大的程序来比较每一位。代替地,推荐使用Reflection API实现。这个API只是一个面向元数据的窗口,从而使我们更加轻松。
在上面的程序中,第3个字段是从一个名为flags的整数变量中读取到的。然后,使用关键字typeof,就可以存储来自System.Reflection命名空间的类MethodAttributes的Type对象。每个类都有相应的类型对象与之相关联。
GetFields函数返回一个FieldInfo结构的数组,在我们的例子中,是一个由类型中公共字段变量组成的数组。Binding标志枚举一共有18个成员。因此,枚举直接担当一个过滤器。
通过分析字段信息对象中的位集合,我们可以熟悉元数据的本质。变量fv保存了所有可以设置为on的位,然而,flags字段存储了用于这个指定方法的所有设置为on的位。
因此,我们只需要在两个变量上使用AND位运算。如果这个操作的输出保持不变,那么就表示相应的位是on。在这样的情形中,使用FieldInfo对象,就分配这个位的一个英文描述。因此,如果有20个可能的位组合,for循环就会运行20次。
下一个程序使用了Reflection API来代替位运算。
a.cs
public void xyz() { bool b = tablepresent(6); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[6]; k++) { int rva = BitConverter.ToInt32(metadata, offs); offs += 4; short impflags = BitConverter.ToInt16(metadata, offs); offs += 2; short flags = BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int signature = BitConverter.ToInt16(metadata, offs); offs += 2; int param = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("Name : {0}", GetString(name)); Type t = typeof(System.Reflection.MethodAttributes); FieldInfo[] f = t.GetFields(BindingFlags.Public | BindingFlags.Static); Console.Write("Flags "); for (int i = 0; i < f.Length; i++) { int fv = (int)f[i].GetValue(null); if ((fv & flags) == fv) Console.Write("{0} ", f[i].Name); } Console.WriteLine(); t = typeof(System.Reflection.MethodImplAttributes); f = t.GetFields(BindingFlags.Public | BindingFlags.Static); Console.Write("Impl Flags "); for (int i = 0; i < f.Length; i++) { int fv = (int)f[i].GetValue(null); if ((fv & impflags) == fv) Console.Write("{0} ", f[i].Name); } Console.WriteLine(); } } }
Output
Row 1
Name : Main
Flags PrivateScope FamANDAssem Family Public Static HideBySig ReuseSlot
Impl Flags IL Managed
Row 2
Name : .ctor
Flags PrivateScope FamANDAssem Family Public HideBySig ReuseSlot SpecialName RTSpecialName
Impl Flags IL Managed
MethodAttributes被枚举MethodImplAttributes代替,由于这里的目标是检查来自这个枚举的位集合。开发Reflection API中的函数,是用来生成MethodImplAttributes的输出。
最后一个字段是Params表中的一个项。在我们对Params表进行到进一步解释之前,我劝你修改b.cs文件以包括下面代码:
b.cs
public class zzz { public static void Main() { System.Console.WriteLine("hell"); } public int abc(float k) { return 0; } public long pqr(int i, char j) { return 0; } public void xyz() { } }
a.cs
public void xyz() { bool b = tablepresent(6); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[6]; k++) { offs += 8; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int signature = BitConverter.ToInt16(metadata, offs); offs += 4; Console.WriteLine("Row {0}", k); Console.WriteLine("Name :{0}", GetString(name)); byte count = blob[signature]; Console.WriteLine("Blob:{0} Count:{1} ", signature, count); for (int l = 1; l <= count; l++) { Console.Write("{0} ", blob[signature + l].ToString("X")); } Console.WriteLine(); } } }
Output
Row 1
Name :Main
Blob:10 Count:3
0 0 1
Row 2
Name :abc
Blob:14 Count:4
20 1 8 C
Row 3
Name :pqr
Blob:19 Count:5
20 2 A 8 3
Row 4
Name :xyz
Blob:25 Count:3
20 0 1
Row 5
Name :.ctor
Blob:25 Count:3
20 0 1
上面的例子检查了函数签名,它存储在Blob堆中。到目前为止,还没有对Blob堆进行寻址。
签名暴露了与函数相关的所有信息,这些细节包括,例如:调用约定;被放到栈上的值;this指针是否被传递到函数中;以及最重要的——参数和返回值。
既然这个程序详细地检查了方法签名,我们会以额外3个程序来扩充我们的程序b.cs,即abc、pqr和xyz。在第一个Main方法中,函数的签名被存储在Blob堆中的第10个位置上。
函数签名开始于字节数量的总数,例如3,紧跟在实际的签名之后。函数Main使用了3个字节来存储它的签名,而abc函数要使用到4个字节。如果我们在这里停止我们的探险,在签名存储的背后将会仍然保持神秘。
事实真理是,元数据世界热衷于压缩它需要存储的每一个字节。因此,Blob堆中的任何内容都会被压缩。然而,为了达到这一点,需要遵循一种特定的模式。
如果左边的第1位(例如,第7位或最高位)是0,下面7个字节就会以未压缩的形式存储这个值。因此,从0到127的数字将不会以压缩的形式存储。
如果左手边的第1个字节是1,第2个字节是0,例如,第15位和第14位,那么在这种情形中,下面的14位会被用于存储这个值。这就赋予了它一个范围:从0x80到0x3fff,或2^8到2^14-1。
最后,如果第1位是1,第2位也是1,第3位是0,例如,第31位、第30位和第29位,那么下面的29位会被用于存储这个值
幸运的是,我们暂时不需要为压缩而苦恼,由于在我们的程序中字节数量不会超过127。因此,从左边开始的2个位总是为0。然而,我们可能随后到达将要迫使我们首先解压缩字节然后读取它们的地方。这些字节以相反的顺序或以big endian格式存储。在Intel机器上默认是little endian格式,其中,较小的字节会被首先存储。
在确定了Blob流中的位置之后,字节数量被存储在count变量中,并且它的值会被现实。然后,使用for循环——直到count,将显示下一组Blob堆中的字节。
Row 1
Name :Main
Blob:10 Count:3
0 0 1
Row 2
Name :abc
Blob:14 Count:4
20 1 8 C
Name :xyz
Blob:25 Count:3
20 0 1
让我们开始于研究最简单的名为xyz的函数。
第1个字节透漏了两种类型的信息:this指针和调用约定。初始的计算按照顺序从0到第5位。如果这个位是on,那么它表示this指针已经被传递到函数了。
因此,xyz是一个实例变量,由于它的第5位被标记为on。如果你使得函数xyz为静态的,那么你将注意到值0x20改变为0x00。值0x0表示调用约定DEFAULT。
函数Main没有传递this指针。这个事实易于验证,由于第5个字节是off。
第2个字节给出了被传递的参数的数量。
函数Main、.ctor和xyz没有参数,方法abc有1个参数需要传递,而方法pqr有2个参数需要传递。下一个字节包括了返回类型。
每个函数——例如Main、.ctor和xyz都有一个值1,它表示元素void。值8是一个整数,从而暗示了方法abc返回一个整数。函数pqr返回一个长整数,这是为什么被分配的值是0xA的原因。
文档没有提及int或long。取而代之,它指定了I4和I8,表示字节的实际数量。每个类型都被分配了一个独立的数字,这是在ECMA标准的22.1.15节中被证明的。
在这个字节后是实际参数的信息。因为其中的3个函数都没有参数,所以不存在更多的字节。因此,签名最小为3个字节大小。
函数abc接受一个单独的浮点型作为参数,因而,它的签名大小是4字节。第4个字节包括传递到这个函数的参数类型。由于函数pqr有2个参数,大小为5个字节,这里最后两个字节表示提供给函数的参数类型。
稍后,我们将深入研究方法签名的概念
a.cs
public void xyz() { bool b = tablepresent(6); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[6]; k++) { int rva = BitConverter.ToInt32(metadata, offs); offs += 4; MethodImplAttributes impflags = (MethodImplAttributes)BitConverter.ToInt16(metadata, offs); offs += 2; int flags = (int)BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int signature = BitConverter.ToInt16(metadata, offs); offs += 2; int param = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.WriteLine("RVA :{0}", rva.ToString("X")); Console.WriteLine("Name : {0}", GetString(name)); Console.WriteLine("ImpFlags :{0}", impflags); Console.WriteLine("Flags :{0}", flags.ToString("X")); Type t = typeof(System.Reflection.MethodAttributes); FieldInfo[] f = t.GetFields(BindingFlags.Public | BindingFlags.Static); for (int i = 0; i < f.Length; i++) { int fv = (int)f[i].GetValue(null); if ((fv & flags) == fv) Console.Write("{0} ", f[i].Name); } Console.WriteLine(); Console.WriteLine("Signature: #Blob[{0}]", signature); byte count = blob[signature]; Console.Write("Blob:{0} Count:{1} Bytes ", signature, count); for (int l = 1; l <= count; l++) Console.Write("{0} ", blob[signature + l].ToString("X")); } Console.WriteLine(); Console.WriteLine("ParamList: Param[{0}]", param); Console.WriteLine(); } }
Output
Position of Blob 1240
tableoffset 64
Row 1
RVA :2050
Name : Main
ImpFlags :Managed
Flags :96
PrivateScope FamANDAssem Family Public Static HideBySig ReuseSlot
Signature: #Blob[10]
Blob:10 Count:3 Bytes 0 0 1
ParamList: Param[1]
Row 2
RVA :2068
Name : abc
ImpFlags :Managed
Flags :86
PrivateScope FamANDAssem Family Public HideBySig ReuseSlot
Signature: #Blob[14]
Blob:14 Count:4 Bytes 20 1 8 C
ParamList: Param[1]
Row 3
RVA :207C
Name : pqr
ImpFlags :Managed
Flags :86
PrivateScope FamANDAssem Family Public HideBySig ReuseSlot
Signature: #Blob[19]
Blob:19 Count:5 Bytes 20 2 A 8 3
ParamList: Param[2]
Row 4
RVA :2090
Name : xyz
ImpFlags :Managed
Flags :86
PrivateScope FamANDAssem Family Public HideBySig ReuseSlot
Signature: #Blob[25]
Blob:25 Count:3 Bytes 20 0 1
ParamList: Param[4]
Row 5
RVA :20A0
Name : .ctor
ImpFlags :Managed
Flags :1886
PrivateScope FamANDAssem Family Public HideBySig ReuseSlot SpecialName RTSpecialName
Signature: #Blob[25]
Blob:25 Count:3 Bytes 20 0 1
ParamList: Param[4]
上面的例子包括了我们到目前为止已经处理的所有程序。因此,我们将不再浪费时间进行解释。取而代之,让我们进下一个程序,它显示了MemberRef表。
修改文件b.cs以包括前面的代码。
MemberRef表
b.cs
public class zzz { public static void Main() { System.Console.WriteLine("hello"); } }
a.cs
public void xyz() { bool b = tablepresent(10); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[10]; k++) { int clas = BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int sig = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("Row {0}", k); Console.Write("Class:"); int tag = clas & 0x07; int rid = (int)((uint)clas >> 3); if (tag == 0) Console.Write("TypeDef"); if (tag == 1) Console.Write("TypeRef"); if (tag == 2) Console.Write("ModuleRef"); if (tag == 3) Console.Write("MethodDef"); if (tag == 4) Console.Write("TypeSpec"); Console.WriteLine("[{0}]", rid); Console.WriteLine("Name:{0}", GetString(name)); int count = blob[sig]; Console.Write("Signature #BLOB[{0}] Count {1} ", sig, count.ToString("X")); for (int l = 1; l <= count; l++) { Console.Write("{0} ", blob[sig + l].ToString("X")); } Console.WriteLine(); } } }
Output
Row 1
Class:TypeRef[2]
Name:.ctor
Signature #BLOB[18] Count 5 20 2 1 2 2
Row 2
Class:TypeRef[3]
Name:WriteLine
Signature #BLOB[24] Count 4 0 1 1 E
Row 3
Class:TypeRef[1]
Name:.ctor
Signature #BLOB[14] Count 3 20 0 1
TypeRef Table Output
Row[1]
AssemblyRef[1] token=0x6
Name :Object,0x20
Namespace :System,0x19
Row[2]
AssemblyRef[1] token=0x6
Name : DebuggableAttribute,0x49
Namespace : System.Diagnostics,0x36
Row[3]
AssemblyRef[1] token=0x6
Name :Console,0x5F
Namespace :System,0x19
MemberRef表,或常常称之为MethodRef表,位于valid字段中的第10个位置上。这个表的大小只有6字节。
这个表中的第2个成员是方法名称,它与所在的模块有关。
第3个方法由2个构造函数和WriteLine函数组成。WriteLine函数是被显式调用的,但是直到关系到这些构造函数,我们甚至还没有创建一个单独的对象。
为了确立方法所属的类和命名空间,需要检查名为Class第1个字段
这个字段是这5个表中的一个表的索引,即TypeRef、ModuleRef、Method、TypeSpec 或TypeDef。实际上,字节中的第3位是由MemberRefParent编码索引得到的。
由于开始3位的最终值是1,与之相关的表就是TypeRef表,为了获得表中指定的行,我们首先将值右移3位,因为它是编码索引的一部分。
因此,MemberRef表中的第1行指向TypeRef表的第2行。为了确保综合性以及交叉引用实体,我们传递了包括在TypeRef表中的这些行。
Row[2]
AssemblyRef[1] token=0x6
Name : DebuggableAttribute,0x49
Namespace : System.Diagnostics,0x36
因此,假设来自命名空间System.Diagnostics的类DebuggableAttribute的构造函数是表中的第一个成员行。我们并没有添加这个特性。出于内在原因,C#编译器会自动实现这个操作。
第2个方法,也就是WriteLine函数,将进一步说明这个概念。
MemberRefParent表中的值1,像前面那样,指向TypeRef表。在将25右移3位之后,得到的值是3,正如下面所示。
因此,TypeRef表中行的索引是3。TypeRef表中第3行表示命名空间System的类Console。
Row[3]
AssemblyRef[1] token=0x6
Name :Console,0x5F
Namespace :System,0x19
从而,现在更容易计算出方法属于哪个命名空间-类的联合体。
在MemberRef表中最后一个名为sig的字段是被调用函数的签名。这个签名是非常重要的,因为它是唯一验证参数是否以适当的顺序传递方式。
开始的字节是4,它表示第2笔记录的数量。
因为WriteLine函数是一个静态函数,所以不能传递this指针。从而,下一个字节是00。传递到函数的参数数量是1,而返回类型是1——表示void。最后一个字节将参数定义为ELEMENT_TYPE_STRING。我们将在后面进行更详细的介绍。
第3行表示一个构造函数,它与TypeRef表的第1行有关。这一行表示System表的Object类。签名Blob进一步增强了我们的认识——通过揭示它是一个非静态的函数,不传递参数,返回值为void。
需要注意的一点是,每个被创建的对象,必须调用基类的构造函数。出于这个原因,当创建zzz时,基类Object的构造函数也会被调用。在本书适当的时候,我们将会揭露为这个模块所写的IL代码。此外,当没有手动创建构造函数时,就会默认为类zzz分配一个无参的构造函数。
DebuggableAttribute的这个构造函数获取2个参数,因此显示值5。
从而,最后一句话,MemberRef表中的每一行都使我们认识到代码中一个特别的方法的存在。Class字段指向具有这个成员类型的表;Name字段提供了名称,最后,Signature字段描述了方法调用的实际签名。
现在,我们在文件b.cs中插入下面的代码:
b.cs
public class zzz { public static void Main() { System.Console.WriteLine("hell"); System.Console.WriteLine(10); System.Console.WriteLine(true); } }
Output
Row 1
Class:TypeRef[2]
Name:.ctor
Signature #BLOB[18] Count 5 20 2 1 2 2
Row 2
Class:TypeRef[3]
Name:WriteLine
Signature #BLOB[24] Count 4 0 1 1 E
Row 3
Class:TypeRef[3]
Name:WriteLine
Signature #BLOB[29] Count 4 0 1 1 8
Row 4
Class:TypeRef[3]
Name:WriteLine
Signature #BLOB[34] Count 4 0 1 1 2
Row 5
Class:TypeRef[1]
Name:.ctor
Signature #BLOB[14] Count 3 20 0 1
修改文件b.cs以调用WriteLine两次。因此,在MemberRef表中,存在3个用于WriteLine的项。TypeRef表的索引保留为3。唯一修改的地方是Signature,因为参数的数据类型是不同的。
参数类型更加复杂,比仅表示数据类型具有更重要的角色。元素类型表E是一个字符串,8是一个整数而2是一个布尔值。
在下面的程序中会展示Custom Attribute表,这里需要修改文件b.cs,只是为了包括一个WriteLine函数,就像前面那样。
Custom Attribute表
a.cs
public void xyz() { bool b = tablepresent(12); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[12]; k++) { int parent = BitConverter.ToInt16(metadata, offs); offs += 2; int type = BitConverter.ToInt16(metadata, offs); offs += 2; int value = BitConverter.ToInt16(metadata, offs); offs += 2; int tag = parent & 0x1F; int rid = (int)((uint)parent >> 5); Console.Write("Parent:"); if (tag == 0) Console.Write("MethodRef"); if (tag == 1) Console.Write("FieldRef"); if (tag == 2) Console.Write("TypeRef"); if (tag == 3) Console.Write("TypeDef"); if (tag == 4) Console.Write("ParamDef"); if (tag == 5) Console.Write("InterfaceImpl"); if (tag == 6) Console.Write("MemberRef"); if (tag == 7) Console.Write("Module"); if (tag == 8) Console.Write("Permission"); if (tag == 9) Console.Write("Property"); if (tag == 10) Console.Write("Event"); if (tag == 11) Console.Write("Signature"); if (tag == 12) Console.Write("ModuleRef"); if (tag == 13) Console.Write("TypeSpec"); if (tag == 14) Console.Write("Assembly"); if (tag == 15) Console.Write("AssemblyRef"); if (tag == 16) Console.Write("File"); if (tag == 17) Console.Write("ExportedType"); if (tag == 16) Console.Write("ManifestResource"); Console.WriteLine("[{0}]", rid); tag = type & 0x07; rid = (int)((uint)type >> 3); Console.Write("Type:"); if (tag == 0) Console.Write("TypeRef"); if (tag == 1) Console.Write("TypeDef"); if (tag == 2) Console.Write("MethodDef"); if (tag == 3) Console.Write("MemberRef"); if (tag == 4) Console.Write("String"); Console.WriteLine("[{0}]", rid); int count = blob[value]; Console.WriteLine("Value Blob[{0}] Count {1}", value, count); for (int l = 1; l <= count; l++) { Console.Write("{0} ", blob[value + l].ToString("X")); } } } }
Output
Parent:Assembly[1]
Type:MemberRef[1]
Value Blob[29] Count 6
1 0 0 1 0 0
MemberRef Table
Row 1
Class:TypeRef[2]
Name:.ctor
Signature #BLOB[18] Count 5 20 2 1 2 2
TypeRef Table
Row[2]
AssemblyRef[1] token=0x6
Name : DebuggableAttribute,0x49
Namespace : System.Diagnostics,0x36
Custom Attribute表被分配到valid字段的第12个位置,用于处理特性(Attribute)。你可能还记得不久之前,我们向你透漏了通过C#编译器添加一个特性。
Custom Attribute表具有下面的列:
第1个字段称为父亲,它具有一个HasCustomAttribute编码索引。
这个索引使用了开始的5个位来对表进行编码。可能的值范围从0到18,并且具有下列重要性
Assembly表是父亲。表的类型是在和开始的5个字节执行AND位运算之后检索得到的。然后,为了确定表中的索引,要将这个字节右移5位来检查最后3位。它可以是任何一个表的索引——Custom Attribute表除外。
结果是1,表示它是Assembly表中的第1个索引。第2个字段名为type。它是5个可能的表中任何一个表的CustomAttributeType编码索引。
最后3个位提供了值3,从而表示MemberRef表。在字节右移3位之后,表中的索引将会是1。因此,这个特性适用于MemberRef表的第1个索引,例如,在属于命名空间System.Diagnostics的DebuggableAttribute类中的.ctor。
最后一位是一个Blob堆中的2字节索引。CustomAttribute表包括了Blob堆中的数据。它被用来实例化一个对象,这是在运行期间Custom Attribute的一个实例。
在表之间存在交叉引用。因此,我们决定首先阐述独立的表。Type字段是Custom Attribute中的构造函数的索引。这里不存在规则来保证自定义特性的存在是必须的。
文档中没有明确条款规定——类型索引必须索引Method和MethodRef表中的一个有效表。不过,.NET上的代码清晰论证了它可以是前面提到过的5个表中的任何一个。最后一列的值可以为null。
一如既往,Blob堆开始于字节的数量。我们的自定义特性有6个字节。
节22.3定义了Blob堆用于自定义特性的语法。它结束于一个prolog,这是一个具有值0x0001的短整数。如果你还记得我们曾经反转过这些字节,那么你是完全正确的,因为这些Blob中的字节是以big endian格式存储的,它与small endian正好相反。
让我们考虑一个值:258。
这个十进制的数字的十六进制表示在little endian格式下是0x0102,可是在big endian格式下是0x0201。这时因为这些字节在big endian格式下被反转了。
Assembly表
a.cs
using System.Configuration.Assemblies; ... ... public void xyz() { bool b = tablepresent(32); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[32]; k++) { AssemblyHashAlgorithm HashAlgId = (AssemblyHashAlgorithm)BitConverter.ToInt32(metadata, offs); offs += 4; int major = BitConverter.ToInt16(metadata, offs); offs += 2; int minor = BitConverter.ToInt16(metadata, offs); offs += 2; int build = BitConverter.ToInt16(metadata, offs); offs += 2; int revision = BitConverter.ToInt16(metadata, offs); offs += 2; AssemblyFlags flags = (AssemblyFlags)BitConverter.ToInt32(metadata, offs); offs += 4; int publickey = BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int culture = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("HashAlgId {0}", HashAlgId); Console.WriteLine("MajorVersion {0}", major); Console.WriteLine("MinorVersion {0}", minor); Console.WriteLine("BuildNumber {0}", build); Console.WriteLine("RevisionNumber {0}", revision); Console.WriteLine("Flags {0}", flags.ToString()); Console.WriteLine("Public Key #BLOB[{0}] {1}", publickey, blob[publickey]); Console.WriteLine("Name:{0}", GetString(name)); Console.WriteLine("Culture:{0}", GetString(culture)); } } } public enum AssemblyFlags { PublicKey = 0x0001, SideBySideCompatible = 0x0000, NonSideBySideAppDomain = 0x0010, NonSideBySideProcess = 0x0020, NonSideBySideMachine = 0x0030, EnableJITcompileTracking = 0x8000, DisableJITcompileOptimizer = 0x4000, }
Output
HashAlgId SHA1
MajorVersion 0
MinorVersion 0
BuildNumber 0
RevisionNumber 0
Flags SideBySideCompatible
Public Key #BLOB[0] 0
Name:b
Culture:
Assembly表定位在valid表的第32位上。它的任务是存储程序集的细节。一个程序集是由多个模块依次组成的,依次表示一个dll或exe文件。Assembly表只可以包括0或1行。
第1个字段是命名空间System.Configuration.Assemblies中的枚举AssemblyHashAlgorithm的4字节常量。因此,我们添加了这个命名空间到我们的程序中。散列技术有多个包括加密算法的应用程序。这个散列值可以假设为三个可能值中的一个。
接近于AssemblyHashAlgorithm尾部的4个字节,存在着4组2字节常量,分别表示Major Version、Minor Version、Build Number和Revision Number。每个的值都恰好为0。
之后是一个4字节标志,使用枚举标注了它所表示的内容。在枚举中,每个成员都被分配一个默认值。因此,当显示这个字段而不是枚举值时,就会显示成员的名称。这个方法胜过将不同的值进行AND位运算的过程。然而,这个方法只能用于当只有一个成员与枚举相匹配时。如果多于1个成员与枚举匹配,那么就会显示这个成员而不是成员名称。
如果设置了PublicKey标记,就表示程序集引用保存了完整的公钥。Side by Side Compatible的值正是名称所指出的。最后两个值是保留的。PublicKey字段是Blob堆的索引。由于它有一个值为0的索引,所以它是无效的,从而完全没有索引。
这还证实了PublicKey可以是0。
接下来出现的是程序集的名称,不包含文件扩展名。之后是Culture字段,当前是一个null字符串。
AssemblyRef Table
a.cs
public void xyz() { bool b = tablepresent(35); int offs = tableoffset; if (b) { for (int k = 1; k <= rows[35]; k++) { int major = BitConverter.ToInt16(metadata, offs); offs += 2; int minor = BitConverter.ToInt16(metadata, offs); offs += 2; int build = BitConverter.ToInt16(metadata, offs); offs += 2; int revision = BitConverter.ToInt16(metadata, offs); offs += 2; AssemblyFlags flags = (AssemblyFlags)BitConverter.ToInt32(metadata, offs); offs += 4; int publickey = BitConverter.ToInt16(metadata, offs); offs += 2; int name = BitConverter.ToInt16(metadata, offs); offs += 2; int culture = BitConverter.ToInt16(metadata, offs); offs += 2; int hashvalue = BitConverter.ToInt16(metadata, offs); offs += 2; Console.WriteLine("MajorVersion {0}", major); Console.WriteLine("MinorVersion {0}", minor); Console.WriteLine("BuildNumber {0}", build); Console.WriteLine("RevisionNumber {0}", revision); Console.WriteLine("Flags {0}", flags.ToString()); int count = blob[publickey]; Console.WriteLine("Public Key or Token #BLOB[{0}] {1}", publickey, count); for (int l = 1; l <= count; l++) { Console.Write("{0} ", blob[publickey + l].ToString("X")); } Console.WriteLine(); Console.WriteLine("Name:{0}", GetString(name)); Console.WriteLine("Culture:{0}", GetString(culture)); Console.WriteLine("Hash Value #BLOB[{0}]", hashvalue); } } }
Output
MajorVersion 1
MinorVersion 0
BuildNumber 3300
RevisionNumber 0
Flags SideBySideCompatible
Public Key or Token #BLOB[1] 8
B7 7A 5C 56 19 34 E0 89
Name:mscorlib
Culture:
Hash Value #BLOB[0]
AssemblyRef表存储了在文件中引用的所有程序集。
System命名空间的代码位于名为mscorlib.dll的文件中。由于在程序中只引用了一个恶程序集,所以在AssemblyRef表中只存在一行。AssemblyRef表可以在valid字段的第35个位置上得到。
Major Version、Minor Version、Build Number、Revision Number、Flags、Public Key和Culture都可以从mscorlib.dll的Assembly表中得到。编译器读取被引用的每个程序集的元数据,从而计算它们包括的命名空间和类。
我们的exe文件没有公钥,但是mscorlib有一个8位的公钥会被显示。
我们就要结束本章了,我们已经仔细查看了出现在最小可能的exe文件中的8个元数据表。下一章将会研究到目前为止剩下的还没有介绍的表。