第五章 Blob流

b.il

a.cs

Output

表5-1

上面的例子是解释方法签名的开始步骤。这个程序被直接作为贯穿本章的一个原型使用。新引进的函数将被追加到abc函数的结尾。FillParamsArray和DisplayMethodSignature这2个函数会被添加到程序中。

为了支持函数签名的显示,参数名称的存在是不可避免的,就像我们在输出重看到的那样。目前,FillParamsArray方法进填充了带有每个参数名称的字符串数组。前面给出的代码读取Param表,被重新用来填充实例数组paramnames。

数组row中的第8个成员决定了数组的大小。与数组开始于0相反,行的编号开始于1。因此,数组的大小增加1,从而数组的偏移量与行编号是同步的。

最后,在函数的顶点,有一个称为paramnames的数组,它包括了将要作为函数签名的一部分而被显示的参数的名称。上面程序的精髓不是函数DisplayMethodTable,而是函数DisplayMethodSignature。

在DisplayMethodTable函数中,我们开始于阅读整个Method表。然而,只使用到了Name和Signature字段,它们存储了Blob流中的方法签名偏移量。

此后,会调用DisplayMethodSignature函数,它带有3个参数:Blob堆中的偏移量,Param表中的偏移量(这个函数的第1个参数所在的位置)以及作为一个字符串的函数名称。但是,只有当方法签名识别这些参数时,偏移量param才是意义深远的。

表5-2

和往常一样,第1个字节是字节数量,它的值为6,用于第一个函数pqr。字节数量存储在count变量中。然后,创建一个数组blob1,以及整个方法签名——在大小上受到count值的限制,被编译到一个字节数组中。

这个方法简化了有效地解释方法签名的过程,因为数组blob1专门用来存储签名,不同于适用于其它内容的Blob。

在count字节之后,是1个字节,它包括了2个不同的类型信息。一个涉及到调用约定,另一个则讨论到了this指针。

变量firstfourbits检索了第1个字节的开始4位,这个字节确定了调用约定。

在这4位之中,如果开始3位的值都是0,那么调用约定就是默认的。如果值为0x05,那么调用约定就是vararg。Vararg调用约定简化了可变数量参数的传递。

第2组4个字节,标识了this指针是否被传递,以及是否在函数中使用了修饰符explicit。不会在静态函数中指定this指针。这样的结果是,第6位为off,正如在pqr函数中所看到的那样,它被标记为static。

另一方面,函数abc的第6位为on,并且它也使用了修饰符explicit。

方法签名的第1个字节处理了调用约定。下一个字节提供了方法所使用的参数数量。这个值被存储在paramcount变量中,并且它被用来显示所有的参数。

第3个字节提供了返回类型,假设这是一个简单的类型。GetType函数以字符串格式返回该类型。这里,还假设这些参数保存了简单的值类型。

for循环重复了这个过程,视参数的数量而定,从而显示函数获取的所有参数。GetType函数用于返回类型,而paramarray数组用于提供数组的名称。签名会以可读的形式显示——这就是所依靠的机制

我们创建了太多关于方法签名的假设。既然基本点已经被很好地分析过了,让我们根据上面的基础,创建并编写一个方法,该方法是对最复杂的方法签名的解码。

b.il 

函数FillParamsArray、DisplayMethodTable和DisplayMethodSignature都被修改了。进一步,下面指定的代码,应该位于GetType函数之后,但是在类zzz的封闭的大括号之前。

a.cs

Output

表5-3

让我们首先着手于IL文件。我们尝试着尽可能复杂地渗入其中。pqr函数获取5个参数:第1个参数b,是一个简单类型;第2个,即aa,是一个out参数;第3个参数是zzz类型,第4个是zzz对象。然而,这不是方法中的最后一个词,因为可以将它做得更加复杂。

在FillParamsArray方法中,还会创建2个名为typerefnames和typedefnames的字符串数组,分别用来存储出现在TypeRef表和TypeDef表中的类型的名称我们在方法签名中创建或涉及到的类型,都存储在这些表中。这两个数组的使用方式和paramarray是相同的。

除了FillParamsArray、DisplayMethodTable和DisplayMethodSignature这3个函数之外,剩下的代码都和前面的程序是一样的。我们还消除了显示特性的多余代码及其它。

DisplayMethodSignature方法传递了Blob流中方法签名的起始偏移量。这是最重要的参数,因为没有该签名索引,就无法继续前进。伴随着这个签名,参数的名称也需要被显示。从而,我们让开始的Param表索引作为第2个参数。最后,函数的名称是通过第3个和最后一个参数来显示的。

这个函数的主要目的是返回一个暴露了方法签名的字符串。

为了达到这个目的,我们引进了多个WriteLine语句。我们把代码尽可能地切分成一些方法,从而使它们可以复用。然而,反过来说,当从一个方法到下一个中取得进展时,你就可能困惑不解并受到挫折。虽然如此,好的一面是,它可以使代码易于理解,而无需不可避免的重复。

在我们转移到揭开程序中新引进的代码之前,还有一些刻不容缓的事情。

在Blob流中的任何一个实体的第1个字节,都是字节的数量。出于诸多原因,我们总是假设数量是一个字节,并且在这一点上,我们的理念还没有受到质疑。然而,唯一明显的缺点是,字节是受它的容量约束的,也就是,它只能提供0到255范围内的数字,这一共有256(2的8次幂)个数字。

这里的困惑是,如果签名大小超过了255字节,那么它就不能用一个单字节来表示。它需要2个字节来提供从2到65535范围间的数字,这一共有65535(2的16次幂)个数字。在特定的时间,即使这个范围不幸达不到目标。在这样的情形中,要采用一个整数,它能够存储2的32次幂这样的数字。

然而,在大多数场合,这个数字位于0到127的范围中,并且只在某些特定的场合中,它会被扩展到10000s。从而,保存一个int来存储这个数字——这种试验是无益的,因为这将会承受相当空间的损失。因此,元数据的设计者决定在将其存储在元数据流中之前压缩每个字节。

表5-4

这会导致更小的流大小。随之出现的速度损失,会显著提升额外花费在压缩和反压缩这些字节上的时间。

应用于下面的方法:

表5-5

检查第1个字节的第1位。如果它是0,就表示下面的7个字节代表这个数字。因此,对于0到127(或0x00到0x7f)之间的数字,会使用一个单字节。

表5-6

由于字节数量通常位于这个范围中,所以节省了极大的占据空间。对于更大的整数,第1个位被切换为on。在这样的情形中,会检查后面的位。如果这个位为0,那么它就表示下面的14个存储该数字的位。因此,从128到16383(2^14-1)或从0x80到0x3fff都可以由14个位提供。

表5-7

最后,如果第1位被设置为1,而第2位也被设置为1,第3位会被检查。如果第3位为0,就表示count成员可以使用29位存储直到0x1FFFFFFF或536870911的数字。

让我们详细地检查文档中的一个例子。像0x03或0x7f这样的数字是使用8位存储的。像128或0x80这样的数字——它大于127——将会使用16位,以0x8080的形式存储。

这是因为,第16位会被设置,第15位会被清除,而剩下的14位提供了这个值。0x2E57会以相同的方式存储为0xAE57,而最大的由2个位来提供的数字0x3FFF,被存储为BFFF。数字0x400需要4个字节,因为它大于0x3FFF。它被存储为C0004000,其中第31位被设置而第29位被清除。

最后,最大的数字,以压缩流的方式表示为0x1FFFFFFF。它会被存储为0xDFFFFFFF。另一个小问题是我们正在检查最高位,然而在little endian系统中,首先存储的是最小字节。这会变成一个障碍,因为我们的目的是读取高字节中的高位。

因此,所有值都按照相反的顺序存储,也就是big endian系统,其中首先存储高位。例如数字515,以little endian格式将3存储在2的后面,而以big endian格式将2存储在3的后面。每当读取一个值,就会执行检查来确定这个字节是否被压缩。如果是被压缩的,那么它就需要被解压缩。

存储在这些字节中的值是一个整数,它表示从Blob区域中获取到的字节数量的大小。因此,我们需要一个函数,它接受一个字节数组、字节的开始位置或索引,以及一个存储了未压缩字节的整数。此外,我们还希望存储返回值,以及最后使用的字节数量。

写一个适合于所有这些规范的函数,是一个艰巨的任务。因此,我们观察一个名为MetaInfo的程序,这是由开发者工具代码中的FrameWorkSDK提供的。这个例子还显示了在元数据表中出现的信息。它是用托管的C++编写的。关于这个程序唯一要解决的障碍是,它不给我们访问表中的原始字节。

然而,它提供了如方法签名等方面的丰富信息。在MetaInfo程序中,调用CorSigUncompressData方法来解压缩字节。我们还决定根据相同的名称来调用我们的方法。这个方法获取3个参数,即Blob数组、在数组中签名开始的偏移量,以及存储了最后的未压缩值得输出参数,返回值暴露了解压缩是否使用1或2或4个字节。

现在让我们了解CorSigUncompressData方法的工作。

函数的参数索引有助于检查Blob流中的字节。检查第1位来判断它是否为0,通过和0x80进行AND运算。如果答案为0,就表示最后一位为0,而不是1。这就表示这个值位于0到127的范围内,并且存储在一个单字节中。我们将out参数answer设置为包括在字节数组中的值。然后,我们设置返回值为1,表示在此使用了一个单字节。到目前为止,一切都进行得很好!

通过对数组中得字节和0xc0进行AND位运算,可以确定最后2位是否为on。如果结果为0x80,就表示最后一位是on,就是说设置最后一位,而清除了倒数第2位。这仅表示接下来的2位存储了未压缩的字节,从而,变量cb被设置为2。

然后,在高位,通过将其和3f进行AND位运算,第15位和第14位被设置为0。结果6位被左移8位,从而它们占据了8到15位。然后,它们和字节数组中的下一个字节进行OR运算,从而开始的8位会被填充。

如果这个值超过了有效限制,最后的检查就会用于确定最后2位是否被设置了,以及倒数第3位是否被清除了。从而,字节和0xe0进行AND位运算,它是的位的组合模式——最后的3个位为on。如果answer为0xc0,即最后的两位为on并且第3位为off,它指定了签名的大小被限制为4字节。

由于默认设置为big endian机器,最后3个位被标记为off,并且右移24位来占据这个长整数的高位。此后,我们访问字节数组的下一个字节,并右移16位,从而它占据了倒数第2个字节。第3个字节只是右移8位,而第4个字节根本不会移动。它们都进行OR位运算以获取全部的字节。

变量cb随后被设置为4,因为这里会使用到4字节。这种压缩方法提供了29个可用的位。

表5-8

因此,每当未解压的方法被调用,变量index标识了Blob数组中的偏移量。这个函数的返回值被存储在DisplayMethodSignature的变量cb中。

此后,变量cb的值被添加到index变量中,因为不存在其他方法来决定Blob流中未压缩的字节数量。变量count被初始化为存储在变量uncompressedbyte的值中。在我们的例子中,签名是22字节长。然后,下面的22个字节只是用来以其原始的形式显示方法签名。

我建议你在这里暂停一下,并添加大约120个参数到上面的函数中。这会增加变量count使其超过127,随后,你可以很容易地验证输入。未压缩的方法变戏法般地将cb的值设置为2。

像前面那样,Array.Copy函数提供了blob1数组中的签名字节的一份干净的复制,而变量index会被设置为0,因为这是第1个字节开始的地方。

在变量count之后的字节表示调用约定。为了标识调用约定,要使用函数GetCallingConvention,它会对第一个字节进行解码并返回字符串值。稍后我们将深入研究调用约定的种种复杂。

第3项是param count。它显示值5,表示存在5个参数。这个值存储在变量paramcount中。每当读取一个字节时,首先会对其进行解压缩,然后再使用。

下一个字节是返回值。为了对返回值进行解码,GetReturnType函数会被调用。这个函数执行了一个特定的工作,随后,返回一个字符串值。这个函数的最后一个参数暴露了由返回类型使用的字节数量。除此之外,就像前面一样,这个函数传递了Blob数组和返回字节开始的索引位置。

函数GetReturnType首先尝试确认这个类型是否为一个简单的类型,即这个值是否小于或等于0x0e。如果是这样,那么函数GetType就会被用来解码这个简单的类型并设置变量cb为1。然而,目前,返回的字节数量是18。这个数字表示一个类,而因此,它认定这个类型是一个类。

返回类型之后的字节提供了这个类的细节,它存储为一个符号。类的这些细节可能发出下面3个表中的任何一个:TypeRef、TypeDef和TypeSpec。

表5-9

压缩是元数据世界的至理名言。从而,标记——表示了表的名称和索引,以一种完全压缩的形式被创建。标记的开始2个字节选择了被索引到的表,而剩下的5个位分配了表中行的数量。剩余位的数量为5,而不是6,因为第一个字节检查行编号是否占据了1或2个字节。

这3个表的索引,直到行号32,可以被表示为一个字节。但是,如果行号延展到32之上,那么就需要2个位来存储这个记号。从而,行和表首先会被压缩,随后,这个记号会被进一步压缩。在我们的例子中,记号值为8,而开始2个位被标注为0。因此,它索引了TypeDef表。

表5-10

对于下一个参数,索引值会通过将这个记号右移2位来得到。结果,这个索引是TypeRef表的第2行,也就是类zzz。

相反的编码如下:

我们首先得到一个行索引并将其左移2位。然后,对表的两个位进行OR位运算,最后,这个值会被压缩。cb的值显然是cb1+1。它可以是1、2或4。翻译一个类的类型的代码,可以被放在一个单独的方法中,从而方便它的重用。

现在,我们到达了程序的核心。

在返回类型之后是参数。for循环用来迭代所有的参数。进一步来说,所有的参数都是背对背存放的,中间没有字节。变量cb1存储了参数所需要的字节数量。

在循环中,字节会首先被解压缩,随后,被传递到GetElementType函数中——对其进行解码。该函数以字符串的形式返回参数。

GetElementType方法得到3个项:

l 一个字节数组,它是实际的未压缩的字节。

l 一个名为index的索引,也就是前面的字节驻留的地方。

l out参数。

这个函数的工作就是调用其它的函数,视字节bytes的实际值而定。像从前一样,如果值小于0x0e,那么GetType方法就会被调用。如果是0x12,那么函数GetClassType就会被调用,它会执行实际的解码。

在函数中指定的代码类似于包括在GetReturnType函数中的代码。这里我们检索它,从而使其简化,以达到最大压缩。

变量index指向了字节0x0a,它表示一个长整数。这是第一个参数的类型。下一个字节是16。如果被显示的类型为16,就表示它是一个引用类型,并且在它后面有一个元数据记号。当在C#中使用关键字ref或out时,或者,当在IL中使用关键字out时,就会有一个引用类型。在显示关键字out之后,下一个字节——也就是元数据记号,会被检查以发掘出表和行的索引。

在ref参数之后是类型zzz和yyy的两个参数。从而,GetElementType函数获取一个字节并标识该字节。它可以是一个简单类型,也可以是一个复杂类型。如果值为20,那么该类型就是一个数组。

表5-11

对于数组,GetArrayType函数的实现带有相同的4个函数。

数组签名的开始4个字节是数组的数据类型。通常,GetElementType函数适用于辨别类型。值4表示该类型是一个整型。然后,下一个字节是数组的维数(rank)。维数表示一个数组拥有的维度。在这个例子中,该数组拥有5个维度。

numsizes的下一个字节的任务是测量有几个维度具有大小。在我们的例子中,5个维度中的3个具有大小。最后2个维度是无论如何也没有大小的。这是完全合理的。

由于下面两个字节包括了每个维度的实际大小,所以就会创建一个维度等于numsizes的整数数组。如果该数组的索引范围从6到8,那么大小就为3。这是因为下界和上界都会被包括在内。在b.il中,第3个维度被指定为3到9,因此,大小就为7。

这个字段又被称为数组的大小。如果没有指定维度,那么大小就恰好为0。如果它是一个没有下界和上界的单独的整数,那么字节size就是该数组的维度。在这个例子中,sizearray首先被填充,然后,就会创建包括下界的数组boundsarray。

在sizes之后的字节是数组的下界,在我们的例子中是3。这个数字不同于字节的大小。这些字节是维度的下界,它们的大小已经事先指定了。如果这个值显示为0,就表示没有指定下界。

第3个数组和6一起出现,但是在IL文件中,它会被反射为3。这是因为这个数字是有符号的,因此,他们需要首先被解压缩。从而,会检查第1位。由于它是未设置的,所以这些位只是被右移了1位,从而挤掉第1位。

表5-12

如果第1位是设置的,依赖于压缩的字节数量,一些位会被标注为off。这两个数组会被显示为使用for循环的健康检查(sanity check)。它们也使用bounds变量作为主要的键。

现在,我们需要放置数组的维度。

首先,上界是由计算机确定的,它是由下界+大小-1计算出来的。上界和下界应该随后被放置在返回的字符串中。为了达到这个目的,首先检查bounds数组,以确认它是否为0,并确认size数组是否为非0的。如果这个条件的结果为true,就表示上界和下界都具有一个简单的大小。这个if语句会处理最后一个逗号。

如果这2个数组都包括0,就表示上界和下界都存在。因此,它们会和3个逗号一起被显示。

最后,作为保存特性的空间,所有在结尾指定的维度都没有被放置在这2个数组中。rank和numsizes之间的不同决定了在数组中需要的空逗号的数量。在2个数组的结尾放置0是荒谬而毫无意义的。

局部变量签名

b.cs

>csc b.cs /unsafe

a.cs

Output

表5-13

上面的例子做了2件事情:

l 首先,它重写了读取元数据表的代码。

l 其次,它在较高意义上解释了签名。

每当在函数中创建一个局部变量时,就会在StandAloneSig表中添加一行。由于主函数没有局部变量,所以不会在该表中添加任何行。这个表有一个单独的字段,也就是Signature。没有其它元数据表会引用它,它也不会引用任何其它元数据表。

文件b.cs内嵌了“不安全”的函数。因此,它必须使用unsafe选项。在该函数中还存在一个变量i。在我们进一步解释签名之前,让我们看一下函数abc带来的改变。

在签名的程序中,文件指针被定为在偏移量360处,它具有用于CLL头的数据目录项。这个方法在IL汇编器和C#编译器上都能工作良好,因为这两个产品都开始于PE文件的偏移量128处。

在我们的工作中,大多数编译器都使用了一个标准的程序,无论何时一个程序在DOS下执行,它都会运行。然而,托管C++编译器并不是这样的。因此,我们不能假设PE头总是开始于偏移量128处。

因此,为了避开所有这些假设,PE偏移量被存储在变量ii中,随后,文件指针会被定为在baseofcode开始的地方。这个位置总是位于PE文件开始位置的44字节处。

CLR头还是一个距离开始位置的没有弹性的、固定的偏移量。在定位了CLR头中的文件指针之后,就可以检索到Size和RVA。

另一方面——被认为是理所当然的——是RVA在“文件对齐和节对齐是两件不同的事情”的假设下工作。然而,如果它们恰好是相同的,那么RVA必须被计算为磁盘上的物理偏移量。因此,不会感到进行了任何额外的计算。相同的RVA可能会被用为内存偏移量和磁盘偏移量。

同时,用于流名称的代码也有所改动。第2个while循环会继续执行,直到遇到一个非0值。然而,当任何一个流——它的长度可以被256整除时,我们自身就会发现遇到了极大的麻烦。在这样一个例子中,第1个字节将会是0,因此,流的偏移量、大小和名称就会一个接着一个。从而,循环继续执行,直到遇到一个非0的值或Position属性变成可以被4整除。

FillParamsArray函数没有被修改过。因此,我们不再显示它们。这也适用于用于解码一个数组的代码。

DisplayStandAloneSigTable函数仅调用了具有Blob索引的函数DisplayVariablesSignature。因此,对签名进行解码的是函数DisplayVariablesSignature。

表5-14

表5-15

像往常一样,第一个字节是count字节。基于这个count。Blob区域中的字节被复制到一个名为blob1的数组中。

第2个字节,之前包括了调用约定,现在提供了值7,指定了局部变量签名。如果第2个字节的值不是7,那么就会返回一个错误。第3个字节是本地变量的数量。for循环对其进行迭代。像前面一样,GetElementType函数做了所有艰难的事情。由于第3个字节的值为8,所以GetType函数显示输出为一个整数。

现在,我们添加一个不同的变量类型,来观察字节上的变化和输出。

b.cs

Output

以下省略一些行

在文件b.cs中,有一个指向整数的指针,它导致了签名中的类型15。由于这是一个指针类型,所以我们称之为GetPointerValue方法。这类似于一个类的类型,其中下一个字节是实际的数据类型。在这个例子中,它是一个整数。我们调用GetElementType方法来读取下一个字节并解释这个类型。符号*被添加到字符串上来表示一个指针。

b.cs

yyy a;

Output

Count=4 Bytes 7 1 17 12 yyy

下一个字节是一个对象,它具有值类型yyy。这类似于类的类型,而不是类型编号17。这会调用函数GetValueType,该函数的工作原理类似于函数GetClassType。

b.cs

yyy* a;

Output

Count=5 Bytes 7 1 15 17 12 yyy *

GetElementType函数有效地工作。从输出看来,这是相当明显的,其中该变量被描述为指向类型yyy的指针。类型15是一个指针类型,紧跟在下一个数字17之后,它是一个值类型。这会获取记号12,它代表了类yyy。

b.cs

Output

Count=7 Bytes 7 2 18 16 69 16 8 xxx , Pinned [ByRef] int

举办变量a和i在上面的程序中被描述。变量a的类型是xxx。开始2个字节,即占据了18和16。数字69用于名为pinned的类型,之后是名为byref的类型16,之后是8,它是最后的整数类型。

Pinning是一种媒介——通过运行时来清楚地指示不要对一个托管对象移来移去,因为这里有一个指针引用了它。默认地,运行时是被允许在内存中移动任何对象的。

b.cs

Object a;

Output

Count=3 Bytes 7 1 28 System.Object

类型28是保留的,用于System.Object。类似的,类TypedReference具有数字22、IntPtr 24和UintPtr 25。

b.cs

以下省略一些行

Output

Count=5 Bytes 7 3 22 24 25 System.TypedReference , System.IntPtr , System.UintPtr

Field Signature

b.cpp

以下省略一些行

>cl /clr b.cpp

a.cs

Output

表5-16

在这个程序中使用FillParamsArray 来填充typedefnames和typerefnames数组。因此,注释DisplayStandAloneSigTable并调用其它两个函数。FillParamsArray,如果在程序中省略了它,那么它就可能被输入如下:

以下省略一些行

更进一步,DisplayElementType函数会进行变型。因此,它会被再次引人。此外,ReadStringIndex和GetString函数会被显示,因为它们会被这个程序暂时征用。

文件b.cpp是使用托管C++编写的,所以它的文件扩展名是cpp。唯一必须的函数是main,注意,这里的m是小写的。除了这个main函数之外,还存在一个字段或全局变量p,它是一个指向函数的指针。它获取4个参数,即一个int、一个char、一个float和一个double。最后,它返回一个int。使用C++编译器cl以及选项/clr来编译上面的程序,从而创建一个.NET文件。

在C#程序中,正如先前开始的那样,我们注释了DisplayStandAloneSigTable方法,并调用DisplayFieldsTable方法作为替代。使用DisplayFieldsSignature方法——傲视它传递了Blob流中的索引,字段的名称和签名就会被显示。如果之后的签名恰好字段签名,那么Blob堆中的第1个字节就必须是0x06。GetElementType方法只返回这个值。

表5-17

这一阵子,我们不得不跟踪Blob数组中的位置。这是不合适的,因为每个字段都由它自己在Fields表中的行所表示。实际代码包括在GetElementType函数中,因为指向一个函数的指针的类型编码是27或0x1b。

我们对“指向函数的指针”这个概念的解释相当满意——使用托管的C++,它是我们一直致力于的语言。函数GetPointerToFunctionType会被调用。指向函数类型的指针位于方法签名之后。第1个字节是调用约定字节,因为这是一个方法签名典型的开始位置。

这个字节具有值2,从而表示一个Standard调用约定。我们过去的Windows程序往往要使用到它。GetCallingConvention方法检查调用约定的值是否为以下之一:C、Standard、ThisCall或FastCall。

下一个字节是这个方法获取的参数数量,在这个例子中为4。第3个是方法的返回类型。这个返回类型开始于数字32,在文档中被称为“自定义修饰符”自定义修饰符要么开始于可选的修饰符(例如在我们的例子中),或开始于一个固定的/必须的修饰符31。当修饰符为可选时,编译器就会保存忽略它的选项。然而,如果修饰符是必须的,那么它就会成为编译器需要考虑的一部分。

修饰符之后有一个记号,我们已经在前面详细地介绍过它了。GetElementType函数会被再次调用。并在必须的和可选的修饰符上执行额外的检查。在这个记号的帮助下,被解码的类会被显示。GetElementType方法会被再次调用,对下一个字节进行解码,对于一个简单的字节这可能是必要的。

为大量的参数实现一个循环。它租用GetElementType函数的服务来处理自定义修饰符,因为自定义修饰符还可以位于这些参数之前。指针的开始2个参数是int和char。第1个参数具有一个自定义修饰符,而第2个参数不是这样的。

MemberRef

b.cpp

a.cs

Output

表5-18

MemberRef表会被在这个程序中调用的每个函数的细节所填充。在托管的C++程序中,代码中的3个句点会被用于表示参数的可变数量。然而,有必要指定函数的第1个参数,它会被随后调用,带有任何数量的参数。

在上面的程序b.cpp中,函数abc会在main函数中被调用,带有1个、2个或3个参数。这会添加3笔记里到MemberRef表中。

在调用FillParamsArray函数之后,DisplayMemberRefTable函数会被显式调用。使用这个函数,会显示所有的方法。然而,出于解释的意图,我们将只能考虑方法abc,而不是构造函数。DisplayMethodSignature方法被用于解码Blob堆中的签名。因此,会给它提供index和name。

我们故意避开编码索引并为参数使用了整数类型。因此,值8会一闪而过。数字32是位于记号之后的可选修饰符,从而它也是可以被忽略的。

第1次调用的函数abc具有2个整数,一个是可选的,另一个是必须的。它位于方法引用表的第2行中。该签名的最后3个字节也会被显示。

我们已经做出了假设——签名将以两个8作为结尾。然而,数字65可以在中间看到。这个字节被称为sentinel。它表示后面所有的参数都是可选的。因此,我们会在sentinel字节出现时显示3个句点。

Row 4显示了两个8后面的sentinel字节65。这是因为被调用的abc函数具有两个可选的整数。最后一行带有一个单独的整数。因此,没有可选的参数,这样的结果是, sentinel字节是不存在的。

在编码时,我们发现,可能会在我们的GetElementType方法中为sentinel字节引进代码。这是因为循环依赖于变量paramcount。

这不是全部。在GetElementType函数中给出sentinel字节还会导致参数数量的增长。

Sentinel,处理了我们引入的代码,是绝对直接的。我们逐个减少循环变量。然后我们添加3个句点到字符串s。下一步,我们让变量index增加1,因为下一个字节会被读取。最后,使用continue语句,我们回到for循环的开始位置。

这将是完全健康的选项来改变for循环的索引,从paramcount到签名中字节的数量。

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