第四章 其它的元数据表

本章继续描述剩下的没有涉及过的表。为此,创建单独的程序来说明每个不同的表。最后,在本书的最后一章,把它们放入一个单独的程序中,其中每一个表都是交叉引用的。

表4-1

本章的第一个程序使用了Fields表。在b.cs文件中输入下面的代码并编译该文件。

Fields

b.cs

a.cs

Output

实例变量又被称为字段。Field表保存了在valid表中的索引4。由于文件跨越了2个名为zzz和yyy的类,它们包括了3个字段,输出显示了3行。

Field表由以下列组成:

表4-2

Field表中的第1列是FieldAttributes标志。FieldAttributes枚举显示了分配给这个数字的字符串。表中的第2列与字段的名称有关。它是包括在Strings流中的数据的索引。输出清楚地显示了类zzz的字段i和vijay被放在前面,后面是命名空间nnn中的类yyy的字段k。这个顺序是极其重要的,稍后我们将证实这一点。

最后一个字段是Blob堆中的索引。它开始于字节数量2,从而指定了字段签名具有2个字节大小。方法签名中的第1个字节确定了这个调用约定。类似地,签名中的第1个字节总是为0x06,从而指定了这是一个字段签名。这基本上被作为一种充分有力的检查。

这个值之后是签名字节。它与字段的类型有关。为了确定这个值,我们引进了函数GetType,它返回数据类型。因为这个函数,输出会适当地反射出字段的数据类型。节22.1.15描述了每个类型的位表示。我们会解释一些尤其复杂的以备不时之需。

表4-3

让我们回到标志字节上。访问修饰符proptected——只允许被派生类访问,被认为是IL世界中的Family。访问修饰符internal——限定被相同的程序集访问,被认为是IL世界中的Assembly。

如果C#也采用类似于IL中的术语,那么一起就都能轻松得多。

如果我们在这个节骨眼放弃了解释,那么就不能向你暴露这些表之间的交叉联接。因此,我们为这个程序扩充一些必需的代码,而TypeDef细节则是绝对必要的。

在调用xyz函数之后,还要调用aaa函数,就像这样:xyz(); aaa();

把下面的代码放在GetType函数之前。

a.cs

Output

上面程序的输出展示了包括在TypeDef表中的数据,其中,表中的3行会被显示。Field表的每一行都由TypeDef表中的行所拥有。这是因为TypeDef表定义了一个类以及属于类的字段。

没有迹象表明哪一个类型或类拥有Field表中的字段。为了决定这些联接,需要检查TypeDef表。第1行表示全局类或伪类,暂时可以被忽略。第2行表示zzz类。在这一行中的FieldList列指向Field表中的第1行。

由于Field表中的第1行表示变量i,推断出变量i属于类zzz就是合乎逻辑的。

TypeDef表的第3行表示命名空间nnn中的类yyy。FieldList字段的值指向Field表中的第3行。因此,我们可以安全地认为开始2行是由类zzz拥有的,然而,从第3行起,字段都属于类yyy。Field表中的一行只能被TypeDef表中的一行所拥有。

这种前向指针方法可以帮助我们决定Field表的拥有者。使用这个方法,类中的字段可以通过读取行中的FieldList列来确定。Field表中的所有行都属于一个类型,直到我们到达下一行的FieldList列中的给定值。这里需要注意的一点是,在Field表中可以有0个或多个行。该类型包括了所有这些行。

这种行为类似于父-子或一对多的关系,其中,父类型可以有多个子字段,然而,子字段只能拥有一个父类型。

如果存在2个具有相同名称但位于不同类中的实例变量,那么会导致在Field表中创建两个独立的行,每个都拥有一个不同的TypeDef行。同样的规则也适用于方法。

Method

这里省略一些行

由于我们现在主要关注于Field表,我们只是显示了Method表中每一行的方法名称。既然在文件中有2个类,那么就可以看到2个构造函数。从而,.ctor的方法名称也会显示2次。

Row 2和Row 3都表示一个构造函数。因此,我们是如何确定每个构造函数属于哪个类的呢?记住,当看到类型中存在构造函数时,你总是应该以TypeDef表作为开始,而不是Field表或Method表。这个方法是随着我们对MethodRef表的需求而变化的,它有一个TypeRef字段来表示类-命名空间的数据。

类型zzz使用了名为MethodList的字段,它的索引为1。第2个类,即yyy,具有指为3的MethodList。因此,Method表的开始2行属于类zzz,而第3行构成了类yyy的一部分。

Constant

b.cs

a.cs

Output

表4-4

Constant表具有以下的列:

表4-5

Constant表位于valid字段的第11个位置上,并且它的名称表示它存储了在模块中创建的常量。

常量也是字段,因此,相应的项会被附加到Field表中。这些表、字段和常量都被显示——从而允许你涉及到它们。

表4-6

表中的第1个字段与常量的数据类型有关。它是一个单独的字节;因此,下一个字节——包括了0值——是一个填充字节。可信赖的GetType函数用于将这个类型显示为一个可读的字符串。

下一个字段的父亲是HasConst编码索引,其中开始两位对表进行编码——可以是Filed表、Param表或Property表。

表4-7

剩下的6位存储了索引。在这个例子中的2个常量都是Field表中的索引。

表4-8

Field表存储了名称和签名。Field表中的Signature字段提供的信息,与类型提供的信息是相同的。然而,Flags字段显示了一个数字而不是字符串。因此,常量的名称和标志来自于Field表。

最后一个字段存储了分配给该常量的实际值。这个字段中的第1个字节是长度。如果它是一个整数,那么就获取下面的4个字节,然而,如果它是一个字符串,那么就使用这个字符串在Unicode编码下的长度——而不是ASCII编码。编译器使用Blob堆来存储常量的值。

处于这个原因,常量是在编译期间被确定的,而不是在运行期间。

Nested Classes

b.cs

a.cs

Output

表4-10

在b.cs文件中,类zzz外包了类yyy,在C#世界中,这是完全合乎逻辑的。这个概念——在一个类中外包另一个类——在术语上称为“嵌套类”。类yyy依次包括一个名为xxx的嵌套类——这也是允许的。

对于每个嵌套类而言,会在Nested Classes表中添加一笔记录,Nested Classes表位于第41个索引位置上。按照大小,这是目前为止我们所遇到的最小的表。它只有2个索引。这两个索引都指向TypeDef表,后者定义了一个类。

表4-11

TypeDef表包括了4行。因此,在文件中一共驻留了4个类。除了一个伪类,还存在3个类,它们明显是在文件b.cs中创建的。从而,在TypeDef表中,嵌套类,从其自身而言,是一个类。

由于NestedPrivate位被设置为on,所以在TypeDef表中的Flags字段标识了这个类是一个嵌套类。进一步而言,这3个类都被描述为派生自System.Object类。

回到Nested Classes表,表中第一个字段是嵌套类的名称。因此,Row 1涉及到类yyy的TypeDef表的第3个索引,而第2行指向了类xxx的第4行。

表中的第2个字段是Enclosing字段,它标识了嵌套类的外包类。由于类yyy嵌套在类zzz中,所以这个字段显示值2,从而涉及到TypeDef表中的第2行。第2个类xxx则显示为嵌套在类yyy中,或第3行。

因此,Nested Classes表是易于理解的,因为它只保存了指向类的引用以及它的外包类。这些都是TypeDef表的索引。

嵌套类被定义在外包类的词法范围中。然而,当在程序模块中不存在嵌套类时,Nested Classes表的位就会被标注为off,从而消除了所有对表的跟踪以及它曾经存在过的事实。

嵌套类的两个字段必须引用TypeDef表中的有效行,否则,它会被认为是一个错误。此外,EnclosingClass字段不能引用TypeRef表中的一个有效行,后者显示了类型引用。而且,如果嵌套类和外包类共享相同的值,就会流出一个警告,而不是一个错误。

不存在任何两行可以在嵌套类字段上拥有相同的值。因为多个嵌套类可以外包在一个单独的类中,所以这是在外包类型中的唯一情形。一个单独的类型可能具有无数的嵌套类,但是反过来是不允许的。

Param

b.cs

a.cs

Output

文件b.cs现在包括3个函数,即不带参数的abc,带有2个参数的pqr以及带有3个参数xyz。分配到方法的参数被存储在Param表中。Param表在valid字段中的索引位置为8。由于一共有5个参数,Param表就显示了5行。

表4-12

Param表具有以下3个字段:

表4-13

第1个字段FlagAttributes描述了分配给函数参数的特性。

表4-14

为此,提供一个特殊的函数GetParamAttributes,它唯一的任务就是返回一个字符串——依赖于在标志字节中被切换为on的位

第2个字段是一个序号,而第3个字段是参数的名称。

让我们快速浏览一下Method表。Row 2和Row 3分别代表方法abc和pqr。他们都指向Row 1的ParamList。在这一步,它看起来是欺骗性的,因为方法abc不获取任何参数,而方法pqr获取2个参数。Row 1中的构造函数不获取任何参数,可是,它指向参数列表中的第1行。

然而,正如我们在前面的章节中了解到的那样,必须检查Blob堆的第2个字节来决定这个函数要传递的实际参数数量。不管是构造函数还是abc函数都不获取任何参数,因为在第2个字节中指定的值是0。

表4-15

pqr方法获取2个参数。从而,在Param表中,函数的参数出现在第一行之后。序号标识了参数的顺序,这是为什么第1个参数i的值为1,第2个参数z的值为2,以此类推。

Method表的第4行代表了方法xyz。Blob堆中的参数数量显示为3,从而涉及到Param表中的第3行。Param表中的第3行具有名为j的参数,它的序号为1。参数k的下一行的序号为2。

因此,序号提供了参数序列的顺序。它开始于1,并从那以后,都会为每一个新方法重设为1。适宜的方法是,Method表的Blob字段会被首先读取,从而决定了参数的数量和类型。然后,依赖于Params字段的值,会访问Param表中相应的行。Param表提供了参数的名称、特性和在param列表中的顺序。此后,视Blob字段上的参数数量而定,从Param表中获取到下一组行。

参数的数量可以通过检查序号中的值来重新确定。出于未知的原因,特性位没有被精确设置。

IN参数在C#中是默认的。当调用函数被授权修改参数的值时,会使用OUT参数。Ref是在C#中的一个变体。它被认为是IN的变体。因此,它不会显示OUT特性。然而,按照我们的解释,Ref才是OUT的变体。你可能会责备我们这种误解缺乏远见。另一种可能是C#编译器会休息一下。

概念上,Method表中的每一行都拥有Param表中的一个行,唯一的例外就是Row 1。一个行不能在Method表中有两个所有者。因此,如果有2个函数abc和pqr,它们都只有一个参数,即int i,那么在Param表中就有2个唯一的行。这完全不会被认为是错误的,由于重复行是可以接受的。

序号可以是0,表示所有者的方法的返回类型。序号按照增长的序列值来排列。结果,可以忽略序号,它完全是有效的。

.NET世界中的参数不能有默认值。因此,HasDefault标志将总是为0。

Properties

b.cs

a.cs

Output

和往常一样,让我们着手于文件b.cs。在类zzz中,存在2个属性,即aa和bb,而在类yyy中,存在一个单独的属性cc。

在a.cs中,在展示Property表中的值之前,我们最初显示PropertiesMap表的详细内容。这个表位于valid表中的第21个位置。

tableoffset变量的值总是存储在变量old中,而不是变量new1中。

PropertiesMap表拥有下面2行:

表4-17

第1个字段被称为Parent,它是TypeDef表中的一个索引。由于这里有两个包括属性的类,所以就能看到2行。第1行指向TypeDef表中zzz的第2行。第2行的Parent字段指向TypeDef表中yyy的第3行。

PropertyMap表的第2行是Property表中的索引。这个表显示如下:

Property表具有以下的列:

表4-18

表4-19

Property表是valid表中的第23位。表中的第1个字段是一个PropertyAttributes枚举。第2个字段是属性的名称。第3个字段是Blob中的一系列字节。

让我们回到PropertyMap表。类zzz的第1行在Property表中的索引为1,然而,第2行的索引为3。由于不存在2这个值,这表示Property表的开始两行由TypeDef[2]——也就是类zzz拥有。

每个属性在Property表中都包括一行。从而,一个链接是TypeDef-PropertyMap-Property。现在,让我们检查一下Method表中的行。

Method

以下省略一些输出

这个表是由9个函数修饰的。喂,稍等!我们只希望3个函数,即Main函数,以及类zzz和yyy各自的构造函数。现在,我们已经渐渐进行到了关键时刻。对于每个属性aa的出现,都会创建两个方法:一个是作为写访问器的set_aa,用于,另一个是作为读访问器的get_aa。

由于在文件中有3个属性,所以一共创建6个函数;随之而来的是,在Method表中添加6行。

虽然C#语言能够理解属性,但它们在IL世界中是以函数的形式存在的。从而,所有的属性都会被转换为简单的函数调用。随着签名表示这种引进,写函数或写访问器传递一个参数。这个名为value的参数就是Param表中的Row 1。

到现在为止, 一直都还不错,但是会因为缺少Property表和Method表之间的联接而受人注意。这种联系可以在MethodSematic表中找到。MethodSematic表位于valid字段中的第24位上。

MethodSematic表具有下面的列:

表4-20

表4-21

这个表开始于一个2字节的特性标注。我们创建一个名为GetMethodSemantics的函数,它会检查这些为是否为on。创建函数的背后想法是——它们将来也可以被使用。

在这个函数中,编码是使用略不寻常的技术完成的。在大多数场合,这些位的组合可能是on。迄今为止,实现一次检查从而验证一个特定的位是否为on,相应的,会返回一个值。然而,此外还证实了——如果我们渴望确定多个位是否为on,那么这个方法是很低效的。

继续修改我们的代码。相应地,在函数中,只要这个位为on,我们就继续添加或连接到字符串s上。最后的输出是,特性标志提供了关于这是一个getter还是setter的信息。这个标注的可能的值规定如下:

表4-22

Semantics表的第2个字段指向Method表中的一行。从而,MethodSematics表的第一行是一个getter。它与Method表的Row 2有关,后者表示函数get_aa。

最后一个名为association字段,是一个到Property表的联接。它使用了1位的编码索引,从而结果是1。由于Property表的第1行是属性aa,所以它把getter标志和这个属性联系在一起。

表4-23

从而,在MethodSematic表的帮助下,我们确定了在Method表和Property表之间缺少的联接。字段联接被认为是更加复杂的,并且它还处理了事件。稍后我们将对此进行深入研究。

表4-24

总结一下,PropertyMap表讨论了TypeDef表中的类,它拥有Property表中的属性。

此后,Method表仅列举了那些被创建为属性输出的方法。连接Property表和Method表的是MethodSematic表——通过把它们分别指向函数和属性。

FieldLayout

b.cs

a.cs

Output

默认下,按照自然发展的规律,内存定位由运行时分配到存在于类或结构中的字段。在特定的环境下,我们需要手动决定这些内存定位。为了完成这一点,在程序中名为StructLayoutAttribute的特性,必须位于类名称的前面,它会从名为LayoutKind的枚举中得到Explicit值。

FieldOffset特性包括了偏移量和字段,它是最终的授权来决定布局(layout)。在b.cs中,我们指出了第1个字段i,从而在开始的第2个位置进行布局,而不是0。进一步,我们还必须规定这样的事实——第2个字段,通常开始于第一个字段的结尾,应该替代地开始于偏移量20处。FieldOffset特性必须被放置在每个实例程序上。

在对字段进行手动布局的各种场合中,会在FieldLayout表中添加行。它们在valid表中的索引位置为16。FieldLayout表具有以下的列:

表4-25

表4-26

第1个字段是一个整数,它存储了偏移量。第2个字段是Field表中的一个索引。从而,第1行指向Field表中的第1个字段i,而FieldLayout表中的第2行与字段j有关。

表4-27

这是相当直接的!

Events and Delegates

b.cs

上面的例子在命名空间级别上定义了一个名为pqr的委托。委托的引进,从而能以一种类型安全的方式,不按惯例地调用方法。它们与事件关系密切。让我们观察一下创建所用到的各种表。

TypeRef

以下省略一些行

TypeRef表揭示了这样的事实——在程序集中关联到5种类型。第一个和最后一个类型总是存在的。然而,随着委托的创建,所有3种类型,即MulticastDelegate、IAsyncResult和AsyncCallback,都属于System命名空间,都会被扩展。这些引用的产生是由委托类所引进的代码的结果。

TypeDef

以下省略一些行

在前面的章节中,我们检查了TypeDef表中的行。伪类型和zzz类型都会被创建,就像前面那样。随着委托的出现,带有类型名称pqr的一个新行,会被添加到这个表中。这是第一个类的类型,也就是标志sealed被添加到的地方,从而防止访问到所有其它类从中派生。

Extends字段揭示了pqr派生与哪个类型。它指向TypeRef表中的第2行,即MulticastDelegate类。因此,可以完全确定——委托类是派生于MulticastDelegate类的。

现在,我们看一下在第3行之后的Method表,从而显露出委托类所引进的方法。

Methods

以下省略一些行

Method表中的第3行是一个构造函数。由于不能手动进入到这些函数中,所以这4个函数的ImpFlag字段都是Runtime。这是我们遇到这个标志的第一个场合,稍后我们将说明其它标志——Virtual和NewSlot。

构造函数的签名暴露了两个参数。通过检查Param表,我们发现Row 1是一个名为object的参数,而Row 2是一个名为method的参数。相同口气的,第2个函数Invoke有一个名为p的参数。委托中的第3个函数是BeginInvoke,它接受3个参数——p、callback和object。最后,我们会遇到EndInvoke函数,它获取一个名为result的参数。

表4-28

到目前为止,你会发现阅读元数据表变得越来越容易了——这是显然的。

b.cs

在文件b.cs中,字段a被声明为event类型。委托EventHandler存在于System命名空间中。让我们看一下插入到元数据表中的行。

TypeRef

以下省略一些行

这里有2个额外的类型——在Row 2和Row 4中引进;就是该事件使用的委托EventHandler,以及Delegate类型。稍后我们将回到这些类型上。

TypeDef表包括了由伪类和zzz类组成的行。从而,它们不会被显示。事件,不同于委托,基本上被认为是一个字段。因此,会添加一行到Field表中。

Field Table

以下省略一些行

随着在程序中事件的引进,会在Method表中添加2行,就是add_a和remove_a。稍后我们将在一个单独的进程中处理剩余的Flag位。

函数add_a获取一个名为value的参数,正如Param表验证的那样。第2个名为remove_a的方法也获取一个名为value的参数。

因此,事件最终被划分为2个方法,即add_eventname和remove_eventname。

MemberRef

以下省略一些行

MemberRef表揭露了这样的事实——事件与Combine和Remove方法有关。这两个方法索引了TypeRef表中的Row 4,它代表了System.Delegate类。这个签名将在随后的章节中介绍,因为它太费解了,以至于不能就这样被及时处理。

a.cs

Output

每当我们添加一个事件到我们的代码中时,两个表会被添加到我们的元数据中。首先,事件从概念上被认为是属性。第1个表是Event表,id为20。

Event表具有以下列:

表4-29

表4-30

第1个字段总是一个Flag字段,它的2个位最多有一个为on。

Flag的值0x0200是一个特殊的事件,然而,第2个值0x0400要求运行时特殊地对待该事件,从而以一种特殊的方式处理该事件。

表4-31

函数GetEventAttributes以字符串的形式返回事件特性。第2个字段是事件的名称,在我们的例子中是a。第3个字段是TypeDefOrRef编码索引,其中2位决定了这个表是TypeDef 或TypeRef或TypeSpec。在这个例子中,它是TypeRef表中的第2行的索引,它表示System命名空间中的EventHandler类。

每个事件必须是某种委托类型,第2个表是Event表,它的id是18。

EventMap表具有下面的列:

第1个字段是TypeDef表中的索引。它的值为2,从而指向第2行,在表中具有类名zzz。事件a是在类zzz中创建的。EventMap表中的第2个字段是Event表中的索引。它与Event表中的事件索引有关。

表4-34

最后,MethodSematics类把方法和事件连接在一起。你可能还记得MethodSematics表包括Semantics、Method和Association列。

MethodSemanticsAttribu的第1个字段可能是Addon,也可能是Remove。在它之后,是Method表中的一个索引。此后,我们遇到add_a和remove_a,随后,我们会遇到Event表中的事件索引,也就是上面的方法所连接到的。这个表担当了属性和事件以及它们的方法之间的管道。

b.cs

Output

在上面的例子中,我们仅增加了3个事件到类zzz中以及2个事件到类yyy中。Event表存储了5个事件,而EventMap表存储了包括这些事件的类。类xxx不包括任何事件,这就解释了在EventMap表中它是不存在的。

把一个事件和一个类连接在一起确实是一个诡异而棘手的问题,因为事件的数量没有存储在任何位置。必须访问EventMap表来决定EventList中的事件行。第1行和第2行的事件索引之间的不同确定了存在于第1个类中的事件数量。

Pinvoke

b.cs

a.cs

Output

在上面的例子中,有一个名为MessageBox的方法,它获取4个参数,由2个整数和2个字符串组成。这个方法的代码不是被放置在两个大括号之间;而是以一个分号终结。此外,在这个函数中,有一个名为DllImport的函数。

代码通常被放置在DLL(动态链接库)中。运行Windows的代码也被放置在dll中,如User32.dll和Kernerl32.dll。这段代码主要以C语言编写。在往昔岁月中,需要在C语言中编写大量的函数,然后,以Intel汇编语言编译,而不是IL。我们可以从C#程序或任何其它.NET应用程序中访问到这段代码。

为了执行这段代码,需要引入.NET世界中的PInvoke函数。扩充一个DllInport特性导致要添加一行到ModuleRef表中,它的位置是第26位。ModuleRef表只有一列,即Name,它是String堆中的一个索引。

当前,这个表有一个单独的行。字符串表中的索引提供了像user32.dll这样的DLL的名称。

还有一个受互操作服务影响的表是ImplMap表。这个表的位置是第28位。它包括了出现在DLL中的方法的详细信息。

ImplMap表具有以下列:

表4-35

表4-36

第1个字段是Flags或Attribute字段,它是在GetPInvokeAttributes函数的帮助下解决的。

表4-37

Charset值表示所使用的字符集。Unicode标准是国际化标准,它促进了世界上任何语言在计算机上的表示。这个标准被称为I18n,因为在单词InternationalizatioN的字母I和N之间有18个字符。

调用约定观察被放置在栈上的参数,之后,它会挑选出那些被授予清理栈的责任的参数。

调用约定Winapi在开发Windows时会被使用到,其中参数会以相反的顺序被推到栈上。进一步,当函数被调用时,就是这个“被调用的”函数遍历栈来清理和把它存回原来的位置。

ImplMap表中的第2个字段是MemberForwarded。它是一个编码索引。从而,开始2位用于判断它是Field表中的索引还是Method表中的索引。不支持Export字段。

表4-38

MethodDef表中的第一个方法的RVA为0,因为这个函数的代码存在于User32.dll这个dll中,而不在当前文件中。Flags,直接了当地,表示这个方法的PinvokeImpl位设置为on。还有,Signature表示该方法被调用时带有4个参数,而Param表中的偏移量开始于1。

最后一个字段是ModuleRef表中的索引,它提供了方法所驻留的DLL的名称。

接口

b.cs

a.cs

Output

文件b.cs有2个名为yyy和xxx的接口,他们依次都有一个函数并具有相同的名称abc

。然后,类zzz派生于这2个接口,由于函数名称是相同的,所以在类中方法名称abc必须具有接口名称的前缀。

让我们关注被创建的这几个表。第1个有影响的表是InterfaceImpl,它在valid表中的位的索引是9。

InterfaceImpl表具有以下列:

表4-39

表4-40

第1个字段是TypeDef表中的偏移量。这个表中的Row 4涉及到类的名称zzz本身。第2个参数是一个2位的TypeDefOrRef编码索引。在下文中,一个函数被用于对编码索引进行解码,它会检查位并返回表的名称。另一个函数被用于右移并返回索引值。

TypeDefOrRef编码索引,索引到下面3个表中的一个:TypeDef、TypeRef或TypeSpec。

表4-41

在第1行,索引值指向TypeDef表的第2行。第2行有一个yyy接口的项。InterfaceImpl的第2行指向TypeDef表的第4行,即类zzz;但是接口索引现在是TypeDef表的Row 3,也就是接口xxx。

因此,从一个接口派生的每个类,都在InterfaceImpl表中有一个行。由于我们派生于2个接口,那么就存在2行。接口是TypeDef表中的一个类型,并且Abstract和ClassSemanticsMask位都是on。

第2个表被填充的表是MethodImpl表,它位于第25个索引位上。

MethodImpl表具有以下列:

表4-42

表4-43

第1个字段是TypeDef表的索引。由于方法都存在于类zzz中,所以这两行都指向这个类或TypeDef表中的Row 4。接下来2个字段使用了相同的MethodDefOrRef编码索引,它具有一个单独的位——在二者之中进行选择,即MethodDef(它是一个定义)和MethodRef(它是指向一个方法的引用)。

2个函数,与InterfaceImpl表类似,都实现为返回字符串和值。MethodBody索引指向Method表中的第3个方法,即yyy.abc,而MethodDeclaration指向第1个函数,即abc。

表4-44

这个函数的RVA为0,因为它属于接口yyy。这是因为TypeDef表中的MethodList字段具有值1。

MethodImpl表的第2行具有指向xxx.abc方法或第4行的MethodBody索引。MethodDeclaration字段是接口xxx的函数abc。

因此,MethodImpl表提供了关于MethodDeclaration字段的原始函数的信息,它是由类zzz的MethodBody字段中的函数覆写的。

表4-45

如果接口是由100个方法组成的,那么类zzz中的每个方法都会被覆写,从而导致了MethodImpl表中的另外的100行。

StandAloneSig Table :

b.cs

a.cs

Output

在文件b.cs中,Main函数拥有2个局部变量,一个int和一个string。函数abc依次有一个局部变量和bool类型。

每当在一个方法中创建一个局部变量时,就会在StandAloneSig表中添加一行。StandAloneSig表位于valid字段中的第17个位置。这个表具有一个名为Signature的字段,它是Blob堆中的一个索引。

这个索引开始于保留数字7之后的数量。在前面的章节中我们已经遇到这个问题了。接下来是参数和实际数据类型的数量。再一次使用GetType函数来解释数据类型。

存储在Blob堆中的签名可以被来自众多其它的表索引。其中,我们可能面对的是, Blob堆中的签名是没有任何元数据对其进行索引的。当在函数中存在变量时,就存在这样的一种情况。在适当时,我们将表示用来访问这些签名的过程。

Security and Unsafe

b.cs

>csc b.cs /unsafe

a.cs

Output

程序b.cs现在包括2个方法,它们被标记上unsafe参数。无论何时在程序中使用指针,都必须强制使用unsafe修饰符来实现。进一步,当编译上面的程序时,/unsafe选项被添加到编译器中。使用一个单独的不安全方法——添加一行到DeclSecurity表中。DeclSecurity表的位索引为14。

DeclSecurity表具有以下的列:

表4-46

表4-47

除DeclSecurity表之外,TypeRef表还扩展了3行,从而导致了3个引用。所有这3个特性都位于Security命名空间中。

DeclSecurity表的第1个字段是一个短整数,它代表Action。分配给它的值起源于命名空间System.Security.Permissions中的SecurityAction枚举。值的范围从0到0xff,是为未来标准而保留的。由于当前值为8,所以我们暂时不能解释Action表示什么。

第2个字段是一个2位的HasDeclSecurity编码索引,它指向了下面3个实体之一,即TypeDef、MethodDef或Assembly。

表4-48

这个字段被称为Parent。由于分配了Assembly的值,所以安全许可权限适用于整个程序集。最后一个参数是Blob堆的索引。这个值是一个Unicode字符串System.Security.PermissionSet,它是一个有效的序列化的CLI对象图。

资源

以下省略一些行

会创建一个名为a.txt的文本文件来存储字符串资源。资源只是一个名-值对。这里一共有2个名称,即sonal和vijay,它们的值分别是mukhi和ram。这些资源会被放置在我们的exe文件中并稍候使用某个API将其获取到。

现在,运行Resgen程序,即Resgen a.txt,来创建一个名为a.resources的文件。之后,创建另一个名为b.txt的文件并具有行net=yes,随后,还是运行Resgen程序,即Resgen b.txt。

现在,为了添加这两个资源文件到exe文件中,让我们执行下面的命令:

b.cs

Csc b.cs /res:a.resources /res:b.resources

/res命令行选项添加资源到exe文件中。现在,让我们使用这些紧密接触的元数据表。

a.cs

Output

每当使用/res选项将一个资源添加到exe文件时,会添加一行到ManifestResource表中,它在valid字段中的位索引是40。

ManifestResource表具有以下列:

表4-49

表4-50

第1个字段是距离所在位置的偏移量,其中资源存储在可执行文件中。这个偏移量可以从ImageOptional头的资源数据目录中获得。

CLR头包括了一个项,用于资源和它们的偏移量。

第1个资源存储在这个值的偏移量处,我们的文件a.resources的大小是342字节。作为这个头的字节积累结果,第2个资源展现了偏移量值348。第2个资源直接开始于第1个之后。从而,Offset字段包括了从资源开始的偏移量字节。

第2个字段是Flags字段,它生成了关于程序集的资源是否为public或exported的信息,或者它对于程序集是否为private。

表4-51

使用函数GetManifestResource,就会返回相应的字符串。

第3个字段是资源文件的名称。第4个字段是Implementation编码索引,它指向了File或AssemblyRef表。

文档清晰地表明了——如果Implementation是File表中的一个索引,索引值必须是0。表中的索引0表示该索引是无效的。

Exported Type

c.cs

>csc /t:module c.cs

这会为我们生成一个文件c.netmodule。

b.cs

csc /AddModule:c.netmodule b.cs

a.cs

Output

在文件c.cs中创建了2个类。连同编译器的/target选项一起,它们随后会被编译到一个使用了module选项的模块中。这就导致了一个具有.netmodule扩展名的模块文件的创建。之后,使用编译器的.AddModule选项,对文件b.cs进行编译。

这就导致了在ExportedType表中额外的2行。ExportedType表在valid字段中的索引位是39。

ExportedType表具有以下列:

表4-52

表4-53

第1个字段是Flags特性,属于TypeAttributes类型。它显示了每个类拥有的常规标志。第2个字段稍微有点复杂。它被称为TypeDefId。这个值是程序集中另一个模块的TypeDef表的索引。然而,它仅被用作一个引用。

在进行到下一步之前,有必要验证一下其它表也包括相同的名称和命名空间。在这个例子中,所显示的值是相当大的,结果,我们可能完不成值得做的事情。第3个是类的名称。第4个是命名空间的名称。第5个是Implementation编码索引,它指向File表的第1行。

因此,总结一下,ExportedType表只有一行,用于定义在我们的程序集的其它模块中的类型以及它的导出。这些类型显然会被标注为public。

原文地址:https://www.cnblogs.com/Jax/p/1598569.html