65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 文件格式 - 签名的幕后,第 2 部分(共 2 部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (31投票s)

2009 年 9 月 28 日

CPOL

34分钟阅读

viewsIcon

52915

downloadIcon

756

.NET 文件格式中包含的签名的完整描述

目录

  1. 签名(续)
    1. LocalVarSig
    2. CustomAttrib
    3. MethodSpec
    4. TypeSpec
    5. MarshalSpec
  2. 元素
    1. CustomMod
    2. TypeDefOrRefEncoded
    3. Param
    4. RetType
    5. 类型
    6. ArrayShape
  3. 结论
  4. 参考文献
  5. 修订历史

1. 签名(续)

这是第一部分的续篇。

1.1 LocalVarSig

LocalVarSig 签名也由 StandAloneSig.Signature 列索引,它存储方法运行时分配的所有局部变量的类型。LOCAL_SIG 元素是签名的前导符,其常量值为 0x07Count 元素是一个无符号整数(当然是压缩过的!),它存储与该方法关联的局部变量数量。BYREF 元素是 ELEMENT_TYPE_BYREF 常量的缩写(参见第一部分中的常量),它指示 Type 元素指向实际变量。还有一个值得一提的元素是 Constraint 元素,它表示目标类型在执行内存回收时不会被垃圾回收器移动,因为局部变量位于堆栈上(GC 不会在那里执行任何操作),变量的 Type 应该是引用类型(如 System.Object - 分配在堆上)或值类型(如 System.Decimal - 分配在堆栈上)。但当目标类型(固定)是值类型时,其定义应包含 BYREF 元素,在这种情况下,变量的引用存储在堆栈上,但变量本身分配在堆上。您可以在此处了解更多关于固定(pinning)的信息。在下面的图 1 中,您可以看到此签名的完整语法图。

我想特别提请您注意下面图中的 TYPEDBYREF 元素,这是类型化引用(typed reference),它不仅包含一个指向某个位置的托管指针(像普通引用一样),还包含数据在运行时表示。我从规范中引用它的描述:

"类型化引用局部变量签名表明,局部变量将同时包含指向某个位置的托管指针以及可以存储在该位置的类型的运行时表示。类型化引用签名类似于 byref 约束,但 byref 在类型描述中将类型作为 byref 约束的一部分(因此是静态的),而类型化引用则动态地提供类型信息。类型化引用本身就是一个完整的签名,不能与其他约束结合。特别是,无法指定一个类型化引用为 byref。"

当将未装箱(unboxed)的数据(即存储在堆栈上的数据,这些数据始终是值类型)通过 byref 传递给那些不严格限制其接受类型并除了传递指向某个位置的托管指针外还需要该位置的静态类型的方法时,类型化引用非常有用。类型化引用满足这些需求。还要注意,类型化引用参数可以指向堆栈上的一个位置,并且该位置的生命周期仅限于方法运行期间(在类型化引用分配期间),因此 CIL 编译器会对 byref 和类型化引用参数的生命周期应用适当的检查,请参阅ECMA-355 规范 §12.4.1.5.2。

The LocalVarSig signature syntax diagram

图 1:LocalVarSig 签名语法图

示例 1

此示例代表在堆栈上(仅)声明 byref 值类型,示例代码以 CIL 语言编写,如下所示:

// Full source: LocalVarSig\1.il
// Binary: LocalVarSig\1.dll
// (...)

.method public static void TestMethod()
{ 
    .locals init(int32 &IntVarByRef)
    ret
}

下面的表格详细分析了此示例代码的 LocalVarSig 签名:

偏移量 含义
0x05 0x04 签名大小
0x06 0x07 签名前导符 (LOCAL_SIG 常量)
0x07 0x01 此方法中声明的总变量数为一个
0x08 0x10 因为实际变量位于运行时堆上,所以存在值为 0x10BYREF 元素
0x09 0x08 变量的类型 (int32),参见第一部分中的常量

示例 2

下面的示例演示了如果我们使用类型化引用,签名会发生什么变化。开始时,我们声明 IntVar 变量,下一行,我们使用 __makeref 关键字(此关键字未公开且不符合 CLS 标准)获取一个类型化引用,并将其保存在 TypedByRefVar 变量中。

// Full source: LocalVarSig\2.cs
// Binary: LocalVarSig\2.dll
// (...)

[CLSCompliant(false)]
public void TestMethod()
{
    int IntVar = 0;
    TypedReference TypedByRefVar = __makeref(IntVar);
}

此示例的 LocalVarSig 如下所示:

偏移量 含义
0x1E 0x04 签名大小
0x1F 0x07 签名前导符 (LOCAL_SIG 常量)
0x20 0x02 此方法中声明的总变量数为两个
0x21 0x08 第一个变量的类型 (int32),参见第一部分中的常量
0x22 0x16 第二个变量的类型 (TYPEDBYREF),参见第一部分中的常量

示例 3

现在来看一个稍微复杂一点的示例。在此示例代码中,我们创建了 TestDataClass 类,该类只有一个名为 StringVarToBePinned 的成员,类型为 string。在 TestMethod 方法(标记为unsafe)中,我们实例化 TestDataClass 类。在下面一行,我们尝试“固定”(pin)StringVarToBePinned 成员,并将它们的引用分配给 FixedVar 指针,使用fixed 关键字。此处理确保在 {} 大括号之间,dataClass.StringVarToBePinned 成员不会被垃圾回收器移动,因此 FixedVar 指向该成员在 fixed 关键字大括号内始终有效。请注意,我们不能直接在方法中声明要固定的变量,因为这样的值已经固定(放在堆栈上),因此变量必须包装在 TestDataClass 类中(该类放在堆上)。

// Full source: LocalVarSig\3.cs
// Binary: LocalVarSig\3.dll
// compile with "/unsafe" switch
// (...)

public class TestDataClass
{
    public string StringVarToBePinned;
}

public class TestClass
{
    public unsafe void TestMethod()
    {
        TestDataClass dataClass = new TestDataClass();
        fixed (char* FixedVar = dataClass.StringVarToBePinned) { }
    }
}

此示例还因另一个原因而复杂:它在某个点使用了尚未描述的元素,即 TypeDefOrRefEncoded。此元素定义了在哪个元数据表(TypeDefTypeRefTypeSpec)的哪一行描述了指定的类型。我们在此不深入探讨此元素的细节,如果您愿意,可以直接跳转到该元素2.2 TypeDefOrRefEncoded 小节的描述。

偏移量 含义
0x20 0x08 签名大小
0x21 0x07 签名前导符 (LOCAL_SIG 常量)
0x22 0x03 此方法中声明的总变量数为三个
0x23 0x12 第一个变量的类型(CLASS - 后跟 TypeDefOrRefEncoded 元素),参见第一部分中的常量
0x24 0x08 第一个变量的类型在 TypeDef 元数据表的第 2 行中描述,即 TestDataClass 类。这是当前章节未解释的 TypeDefOrRefEncoded 元素。
0x25 0x0F 第二个变量的类型(PTR - 后跟 Type 元素),参见第一部分中的常量
0x26 0x03 前一个字节指针的类型(char - 最终是 char*),参见第一部分中的常量
0x27 0x45 第三个变量被固定,参见常量
0x28 0x0E 第三个(被固定的)变量的类型(string),参见常量

1.2 CustomAttrib

正如您所料,此签名存储自定义特性的实例,但与之前讨论的签名略有不同。关键区别在于,与 MethodRefSig 签名相比,CustomAttrib 存储提供给自定义特性的参数的,而不存储参数的类型。换句话说,CustomAttrib 签名仅存储在实例化自定义特性时提供的参数(固定命名)的,关于它们的类型和数量的信息在签名中重复。该签名由 CustomAttribute.Value 列索引。Parent 列指示在哪个表(TypeDef - 用于类型,MethodDef - 用于方法,依此类推)和在哪一行描述了被标注的元素(方法、类型等)。与其他签名相比,还有第二个显著区别:在 CustomAttrib 签名中,所有二进制值均以未压缩的 little-endian 字节顺序存储,PackedLen 项(如下文所述)和签名大小除外。我再说一遍,请勿混淆自定义特性(custom attribute)和自定义修饰符(custom modifier)!完整的语法图由四个部分组成,我们来看第一个。

The CustomAttrib signature syntax diagram

图 2a:CustomAttrib 签名语法图

到目前为止,这很简单。它以 Prolog 开始,其常量值为 0x0001,占用两个字节(unsigned int16 - 未压缩且为 little-endian)。接下来是固定参数(FixedArg图 2b 中说明),它们的数量和类型可以通过检查 MethodDefMemberRef(当特性的类位于另一个程序集中时)元数据表中的关联构造函数行来获取。请注意,vararg 方法不能用作特性的构造函数。接着是命名参数的数量(NumNamed 是一个两字节的 unsigned int16 - 也未压缩且为 little-endian),最后是命名参数本身,重复 NumNamed 次。

The CustomAttrib signature syntax diagram

图 2b:CustomAttrib 签名语法图

这部分比前一部分稍微难一些,但也很简单。图中的上方路径表示参数不是单维、零基的数组(SZARRAY,参见第一部分中的常量)。下方路径表示 SZARRAY 参数,即参数是数组。SZARRAY 数组中的元素数量存储在 int32 类型的 NumElem 元素中(未压缩且为 little-endian),占用四个字节。如果 SZARRAY 参数为 null,则 NumNamed 设置为 0xFFFFFFFF 值。CLI完全不允许使用除一维数组(下界为零)之外的数组(SZARRAY)。int32 类型的一维零基数组是 int32[],而不是 int32[,,],也不是 int32[3...8]。如果您想了解更多关于 .NET 中数组的信息,请阅读 MSDN 杂志上的 .NET 中的数组类型文章。

The CustomAttrib signature syntax diagram

图 2c:CustomAttrib 签名语法图

这部分可能是四个部分中最奇怪的。Elem 的格式根据以下条件而变化(引用自规范):

如果参数类型是简单的(上面图中的第一行)(boolcharfloat32float64int8int16int32int64unsigned int8unsigned int16unsigned int32unsigned int64),那么 'blob' 包含其二进制值(Val)。(bool 是一个值为 0false)或 1true)的单字节;char 是一个两字节的 Unicode 字符;其他类型则具有其显而易见的含义。)如果参数类型是enum,也使用此模式——只需存储 enum 底层整数类型的值。

如果参数类型是字符串(上面图中的中间行),那么 blob 包含一个 SerString——一个 PackedLen 计数的字节数压缩且为 big-endian - 由作者添加),后跟 UTF8 字符。如果字符串为 null,则其 PackedLen 值为 0xFF(没有后续字符)。如果字符串为空(""),则 PackedLen 值为 0x00(没有后续字符)。

如果参数类型是 System.Type(参见typeof 关键字 - 由文章作者添加)(同样,上面图中的中间行),则其值存储为 SerString(如前一段定义),表示其规范名称。规范名称由定义它的程序集、其版本、区域性和公钥令牌组成。如果省略程序集名称,CLI 会首先在当前程序集中查找,然后是系统库(mscorlib);在这两种特殊情况下,允许省略程序集名称、版本、区域性和公钥令牌。

如果参数类型是 System.Object(上面图中的第三行),则存储的值表示该值类型的“装箱”(boxed)实例。在这种情况下,blob 包含实际类型的 FieldOrPropType见下文),后跟参数的未装箱值。[注意:在这种情况下,无法传递 null 值。结束注释]

The CustomAttrib signature syntax diagram

图 2d:CustomAttrib 签名语法图

最后一部分说明了 NamedArg 元素(代表命名参数,可以是字段或属性)的格式。因为字段和属性可以同名,第一个元素是 FIELD(当命名参数引用字段时,常量值为单字节 0x53)或 PROPERTY(当命名参数引用属性时,常量值为单字节 0x54)。接下来是 FieldOrPropType 元素,它描述命名属性或字段的类型,占一到两个字节。如果命名参数的类型是未装箱的简单值类型(上面定义),则 FieldOrPropType 应包含一个关联类型的常量值(BOOLEANCHARI1U1I2U2I4U4I8U8R4R8STRING - 参见第一部分中的常量表);但如果命名参数的类型是装箱的简单值类型,则 FieldOrPropType 元素前面有一个字节,值为 0x51,在这种情况下,FieldOrPropType 占两个字节。FieldOrPropName 元素是 SerString上面解释),包含属性或字段的名称。最后是一个单独的 FixedArg 元素(前面显示)。所以,正如您所见,NamedArg 元素是正常的 FixedArg,前面有一些附加信息,用于标识它代表哪个字段或属性。我希望我没有吓到您,正如您很快就会看到的,签名并不像看起来那么复杂。

示例 1

本例主要展示了 SerString 元素以及 CustomAttrib 如何区分用作命名参数的字段和属性。在下面的示例中,我们有一个 TestAttribute 属性,它需要提供一个类型为 int32 的固定参数 Fixed1。此外,我们还可以(并且确实)提供两个额外的、类型分别为 int16string 的命名参数,如下面的代码列表所示。

// Full source: CustomAttrib\1.cs
// Binary: CustomAttrib\1.dll
// (...)

[AttributeUsage(AttributeTargets.Class)]
public class TestAttribute : Attribute
{
    public TestAttribute(int Fixed1) { }

    public short Named1 { get; set; }

    public string Named2;
}

[Test(1, Named1 = 1, Named2 = "Abcd")]
public class TestClass { }

这种情况下的完整 CustomAttrib 签名是 33 字节长,因此在某些点,我们将几个字节合并到一行中,并带有单个描述。

偏移量 含义
0x3E 0x21 签名大小,存储为压缩整数,big-endian 字节顺序
0x3F

0x40

0x01

0x00

Prolog,存储为未压缩且为 little-endian 的 unsigned int16,值为 0x0001
0x41

0x42

0x43

0x44

0x01

0x00

0x00

0x00

属性的第一个固定参数(Fixed1)的值,该值为 0x00000001,并存储为未压缩的 little-endian int32。这由图 2b 的上方行和图 2c 的第一条路径表示。
0x45

0x46

0x02

0x00

提供给属性的命名参数数量,由图 2a 上的 NumNamed 元素表示,并存储为 unsigned int16,little-endian。我们提供了正好两个可选参数,该两字节元素的值自然是 0x0002
0x47 0x54 此字节的值表示目标命名参数由属性(property)表示(参见第一部分中的常量),这是图 2d 上的 PROPOERTY 元素。
0x48 0x06 目标属性的类型(int16,参见第一部分中的常量)。此字节由图 2d 上的 FieldOrPropType 元素表示。
0x49

0x4A

0x4B

0x4C

0x4D

0x4E

0x4F

0x06

0x4E

0x61

0x6D

0x65

0x64

0x31

这是 SerString 字符串,它指定目标属性的名称(由图 2d 上的 FieldOrPropName 元素表示)。SerString 是一个普通的 Unicode 字符串,前面是其字节大小,大小以压缩整数存储,使用 big-endian 字节顺序。因此,我们有一个 6 字节长的字符串(偏移量 0x49),因为字符串名称不包含ASCII 表之外的任何字符,每个字符占用一个字节,我们可以轻松读取字符串文本,它是 Named1
0x50

0x51

0x01

0x00

属性的第一个命名参数(Named1)的值,该值为 0x0001,并存储为未压缩的 little-endian int16。这由图 2b 的上方行和图 2c 的第一条路径表示。
0x52 0x53 此字节的值表示目标命名参数由字段(field)表示(参见第一部分中的常量),这是图 2d 上的 FIELD 元素。
0x53 0x0E 目标字段的类型(string,参见第一部分中的常量)。此字节由图 2d 上的 FieldOrPropType 元素表示。
0x54

0x55

0x56

0x57

0x58

0x59

0x5A

0x06

0x4E

0x61

0x6D

0x65

0x64

0x32

这是再次的 SerString 字符串,它指定目标属性的名称(由图 2d 上的 FieldOrPropName 元素表示)。此字符串的长度为 6 字节(查看偏移量 0x54),其余字节与前一个字符串非常相似,仅最后一个字节不同,字符串文本是 Named2,参见ASCII 表
0x5B

0x5C

0x5D

0x5E

0x5F

0x04

0x41

0x62

0x63

0x64

属性的第二个命名参数(Named2)的值,该值为 Abcd(参见ASCII 表)并存储为 SerString。这由图 2b 的上方行和图 2c 的中间路径表示。因为 0x5F - 0x3E = 0x21,即最后一个偏移量 - 第一个偏移量 = 签名大小,所以签名在此结束。

示例 2

在本例中,我们将演示使用 System.TypeSZARRAY 和装箱值类型作为下面定义的 TestAttribute 属性的参数时的签名格式。

// Full source: CustomAttrib\2.cs
// Binary: CustomAttrib\2.dll
// (...)

[AttributeUsage(AttributeTargets.Class)]
public class TestAttribute : Attribute
{
    public TestAttribute(object Param1, int[] Param2, Type Param3) { }
}

[Test(1, new int[] {1, 2, 3}, typeof(string))]
public class TestClass { }

与前一个示例一样,签名非常长(有 116 字节!),我将其分成几个部分。

偏移量 含义
0x2B 0x74 签名大小,存储为压缩整数,big-endian 字节顺序
0x2C

0x2D

0x01

0x00

Prolog,存储为未压缩且为 little-endian 的 unsigned int16,值为 0x0001
0x2E 0x08 第一个固定参数的类型(int32 - 装箱在 System.Object 中),这种情况由图 2c 的第三条路径表示,其中值前面紧跟着值的类型。
0x2F

0x30

0x31

0x32

0x01

0x00

0x00

0x00

前面字节中指定的类型的值。因为该值类型是 int32,所以它正好占用 4 个字节。它以 little-endian 字节顺序存储,因此值为 0x00000001
0x33

0x34

0x35

0x36

0x03

0x00

0x00

0x00

接下来是第二个参数的定义。因为第二个参数是单维零基数组(SZARRAY),这四个字节指定了提供给第二个参数数组的元素数量。此值以 little-endian 字节顺序存储为 unsigned int32
0x37

0x38

0x39

0x3A

0x01

0x00

0x00

0x00

第二个参数中数组的第一个元素的值。由于数组类型是 int32,因此它是四字节长,值为 0x00000001
0x3B

0x3C

0x3D

0x3E

0x02

0x00

0x00

0x00

第二个参数中数组的第二个元素的值。由于数组类型是 int32,因此它是四字节长,值为 0x00000002
0x3F

0x40

0x41

0x42

0x03

0x00

0x00

0x00

第二个参数中数组的第三个元素的值。由于数组类型是 int32,因此它是四字节长,值为 0x00000003
0x43

0x44

0x45

0x46

0x47

0x48

0x49

0x4A

0x4B

0x4C

0x4D

0x4E

0x4F

0x50

0x51

0x52

0x53

0x54

0x55

0x56

0x57

0x58

0x59

0x5A

0x5B

0x5C

0x5D

0x5E

0x5F

0x60

0x61

0x62

0x63

0x64

0x65

0x66

0x67

0x68

0x69

0x6A

0x6B

0x6C

0x6D

0x6E

0x6F

0x70

0x71

0x72

0x73

0x74

0x75

0x76

0x77

0x78

0x79

0x7A

0x7B

0x7C

0x7D

0x7E

0x7F

0x80

0x81

0x82

0x83

0x84

0x85

0x86

0x87

0x88

0x89

0x8A

0x8B

0x8C

0x8D

0x8E

0x8F

0x90

0x91

0x92

0x93

0x94

0x95

0x96

0x97

0x98

0x99

0x9A

0x9B

0x9C

0x9D

0x5A

0x53

0x79

0x73

0x74

0x65

0x6D

0x2E

0x53

0x74

0x72

0x69

0x6E

0x67

0x2C

0x20

0x6D

0x73

0x63

0x6F

0x72

0x6C

0x69

0x62

0x2C

0x20

0x56

0x65

0x72

0x73

0x69

0x6F

0x6E

0x3D

0x32

0x2E

0x30

0x2E

0x30

0x2E

0x30

0x2C

0x20

0x43

0x75

0x6C

0x74

0x75

0x72

0x65

0x3D

0x6E

0x65

0x75

0x74

0x72

0x61

0x6C

0x2C

0x20

0x50

0x75

0x62

0x6C

0x69

0x63

0x4B

0x65

0x79

0x54

0x6F

0x6B

0x65

0x6E

0x3D

0x62

0x37

0x37

0x61

0x35

0x63

0x35

0x36

0x31

0x39

0x33

0x34

0x65

0x30

0x38

0x39

这个 90 字节长的 SerString 描述了提供给第三个参数的类型的规范名称,其值为 System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089。这由图 2c 的中间路径表示。
0x9E

0x9F

0x00

0x00

两个结束字节,是前一个 SerString 的一部分(0x9F - 0x44 != 0x5A),但它们构成了整个 CustomAttrib 的一部分(0x9F - 0x2C = 0x74),并且不包含任何数据。我认为规范名称存在某些对齐,因此这些零是存在的,不幸的是规范对此没有任何说明。

1.3 MethodSpec

MethodSpec 签名很简单。它描述了一个泛型方法的每个实例化,由 MethodSpec.Signature 列索引,其语法如下:它以 GENRICINST(您看到丢失的“E”了吗?)前导符开始,其单字节值为 0x0A此常量与第一部分中的常量表定义的 ELEMENT_TYPE_GENERICINST 值不同),其中 Type 重复 GenArgCount 次。

MethodSpecBlob ::=
   GENRICINST GenArgCount Type Type*

示例 1

在下面的示例中,我们实例化了 TestMethod 泛型方法,提供了三个泛型参数。

// Full source: MethodSpec\1.cs
// Binary: MethodSpec\1.dll
// (...)

public class TestClass
{
    public void TestMethod<GenArg1, GenArg2, GenArg3>() { }
}

public class TestRunClass
{
    public void TestRunMethod()
    {
        new TestClass().TestMethod<short, int, string>();
    }
}

此情况下的 MethodSpec 如下所示:

偏移量 含义
0x18 0x05 签名大小
0x19 0x0A 前导符
0x1A 0x03 提供给泛型方法的泛型参数的数量
0x1B 0x06 第一个参数的类型(int16),参见第一部分中的常量
0x1C 0x08 第二个参数的类型(int32),参见第一部分中的常量
0x1D 0x0E 第三个参数的类型(string),参见第一部分中的常量

1.4 TypeSpec

TypeSpec 签名由 TypeSpec.Signature 列索引,并在以下情况下使用:实例化多维数组类型的类型、实例化前面带有自定义修饰符的单维数组类型的类型、实例化泛型类型以及其他操作,如下面图所示。由于某些元素尚未解释(如自定义修饰符、数组形状),我们仅使用 TypeSpec 签名的有限功能。在下一章中,我们将重点介绍 CustomModArrayShapeTypeDefOrRefEncoded 元素,然后回到 TypeSpec 签名并使用其剩余功能。另请注意,与前面的示例不同,其中也使用了 GENRICINST(缺少“E”)常量/前导符,TypeSpec 中,使用了 ELEMENT_TYPE_GENERICINST 常量,该常量在常规常量表(在文章第一部分)中定义

TypeSpecBlob ::=
  PTR      CustomMod*  VOID
| PTR      CustomMod*  Type
| FNPTR    MethodDefSig
| FNPTR    MethodRefSig
| ARRAY    Type  ArrayShape
| SZARRAY  CustomMod*  Type
| GENERICINST (CLASS | VALUETYPE) TypeDefOrRefEncoded GenArgCount Type Type*

示例 1

在此示例中,我们实例化了 TypeSpec 泛型类型,如下面的代码列表所示。

// Full source: TypeSpec\1.cs
// Binary: TypeSpec\1.dll
// (...)

public class TestClass<GenArg1, GenArg2> { }

public class TestRunClass
{
    public void TestRunMethod()
    {
        TestClass<int, string> TestVar = new TestClass<int, string>();
    }
}

这种情况下的 TypeSpec 如下所示:

偏移量 含义
0x13 0x06 签名大小
0x14 0x15 ELEMENT_TYPE_GENERICINST 常量,参见第一部分中的常量表
0x15 0x12 泛型类型的类型(CLASS),参见第一部分中的常量表
0x16 0x08 实例化的泛型类型在 TypeDef 元数据表的第 2 行中描述。这是当前章节未解释的 TypeDefOrRefEncoded 元素。
0x17 0x02 提供给该类型的泛型参数数量为两个。
0x18 0x08 第一个泛型参数的类型(int32),参见第一部分中的常量
0x19 0x0E 第二个泛型参数的类型(string),参见第一部分中的常量

1.5 MarshalSpec

当在字段、参数和返回参数上使用 MarshalAs 属性时,会生成 MarshalSpec 签名。它指定通过平台调用(Platform Invoke)与非托管代码调用/返回时数据的封送方式。该签名在 FieldMarshal.NativeType 列中索引。元数据表的名称有点误导,事实上,MarshalSpec 描述的是字段、参数还是返回参数并不重要,它始终由前面提到的列索引。下面的语法列表中的 ParamNumNumElem 元素分别描述了方法调用中提供元素数量的参数、元素数量或附加元素。这两个元素都在签名中以压缩整数形式存储,其目的是帮助计算数组在内存中占用的总字节大小。Microsoft 特有的封送描述符实现比这里描述的更丰富,并使用了额外的常量和扩展语法。如果您想了解更多关于 Microsoft 实现 MarshalSpec 的信息,请参阅元数据规范第 II 部分 - §23.4。

MarshalSpec ::=
  NativeIntrinsic
| ARRAY ArrayElemType
| ARRAY ArrayElemType ParamNum
| ARRAY ArrayElemType ParamNum NumElem

ArrayElemType ::=
   NativeIntrinsic 

NativeIntrinsic ::=
  BOOLEAN | I1 | U1 | I2 | U2 | I4 | U4 | I8 | U8 | R4 | R8
| LPSTR | LPSTR | INT | UINT | FUNC 

要计算数组的字节大小,将使用以下伪代码,其中 @ParamNum 代表为参数 ParamNum 传递的值。

if ParamNum = 0
   SizeInBytes = NumElem * sizeof (elem)
else
   SizeInBytes = ( @ParamNum +  NumElem ) * sizeof (elem)
endif

此签名的常量表如下表所示。在上面的语法描述和本小节的示例中,使用了常量的缩写而不是全名。

名称
NATIVE_TYPE_BOOLEAN 0x02
NATIVE_TYPE_I1 0x03
NATIVE_TYPE_U1 0x04
NATIVE_TYPE_I2 0x05
NATIVE_TYPE_U2 0x06
NATIVE_TYPE_I4 0x07
NATIVE_TYPE_U4 0x08
NATIVE_TYPE_I8 0x09
NATIVE_TYPE_U8 0x0A
NATIVE_TYPE_R4 0x0B
NATIVE_TYPE_R8 0x0C
NATIVE_TYPE_LPSTR 0x14
NATIVE_TYPE_LPWSTR 0x15
NATIVE_TYPE_INT 0x1F
NATIVE_TYPE_UINT 0x20
NATIVE_TYPE_FUNC 0x26
NATIVE_TYPE_ARRAY 0x2A
NATIVE_TYPE_MAX 0x50

示例 1

让我们从下面代码列表中所示的最简单的可能示例开始。

// Full source: MarshalSpec\1.cs
// Binary: MarshalSpec\1.dll
// (...)

[MarshalAs(UnmanagedType.LPWStr)]
public string TestField;

此代码生成了以下 MarshalSpec 签名:

偏移量 含义
0x1C 0x01 签名大小
0x1D 0x15 TestField 字段在非托管代码中被封送为LPWSTR

示例 2

现在是更复杂的示例了。我们将把 int32 类型数组封送为LPArray(C 风格数组第一个元素的指针)。由于此数组类型不提供关于关联数组数据秩(rank)和边界(bounds)的信息,因此我们必须指定方法的哪个参数负责提供数组元素数量的信息。这通过指定 SizeParamIndex 可选参数来实现。此外,我们还设置了 SizeConst 可选参数,该参数指定 Param1 数组包含的元素比 ArraySize 参数指定的元素多 10 个。请注意,还有一个 SafeArray 数组类型,它是一个自描述数组,包含类型、秩和边界信息,并且不需要在 MarshalAsAttribute 中设置任何可选参数,但它是 Microsoft 特有的,因此在此不作描述。

// Full source: MarshalSpec\2.cs
// Binary: MarshalSpec\2.dll
// (...)

 public void TestMethod(
    [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2, SizeConst = 10)] int[] Param1,
    int ArraySize)
{
    // nop
}

上述代码应生成以下 MarshalSpec 签名。

偏移量 含义
0x1B 0x05 签名大小
0x1C 0x2A 封送参数的类型(ARRAY),参见封送描述符的常量表
0x1D 0x50 MAX 常量(参见封送描述符的常量表)表示此数组不提供关于数组元素类型的信息。
0x1E 0x02 ParamNum 参数,存储为压缩整数
0x1F 0x0A NumElem 参数,存储为压缩整数
0x20 0x01 ElemMult 参数,存储为压缩整数。这是一个奇怪的参数,整个规范只提到它两次,说如果封送类型是 ARRAY,则 ElemMult 必须设置为 0x01,但没有指定其含义及其在 MarshalSpec 签名中的位置(请参阅元数据规范第 II 部分 §22.17)。

2. 元素

我们已经讨论了所有的签名,但这还不是全部。签名由更小的部分组成,称为“元素”(我这样称呼它们)。它们之所以分开,是因为它们构成多个签名的一部分,因此无需在每个签名中重复解释特定元素。在本章中,我们将仔细研究它们。

2.1 CustomMod

这个元素在讨论过的签名中经常重复出现,因此我们从它开始。自定义修饰符(custom modifiers)类似于自定义特性(custom attributes),但与它们不同的是,自定义修饰符是签名的一部分。自定义修饰符在 CIL 中使用 modreq(必需修饰符)和 modopt(可选修饰符)关键字在方法声明中定义。两者都需要提供一个类型(类或结构)作为其“参数”。仅因添加了自定义修饰符(必需或可选)而不同的两个签名被视为匹配,并且正如规范所说:

必需修饰符和可选修饰符之间的区别对于 CLI 以外处理元数据的工具(通常是编译器和程序分析器)很重要。必需修饰符表示被修饰项具有不应被忽略的特殊语义,而可选修饰符可以被忽略。例如,C 编程语言中的 const 限定符可以使用可选修饰符建模,因为具有 const 限定参数的方法的调用者不必以任何特殊方式处理它。另一方面,C++ 中应被复制构造的参数应标记为必需自定义属性,因为是调用者进行复制。

不幸的是,C# 在处理带有自定义修饰符的参数方面存在一些问题。您可以在 CodeBetter.com 上的Modopt、方法签名和不完整的规范!更多关于 modopt 的信息文章中阅读相关内容。

CMOD_OPTCMOD_REQD 只是在第一部分中的常量表中定义的常量。TypeDefEncodedTypeRefEncoded 元素实际上是单个 TypeDefOrRefEncoded 元素,将在下一个子节中得到全面讨论。请注意,字段、属性、参数或返回参数可以附加零个、一个或多个 CustomMod。据我所知,除了使用System.Reflection.Emit 之外,没有办法使用 C# 定义自定义修饰符。在System.Runtime.CompilerServices 命名空间中,您可以找到一些指示符(我这样称呼它们),可以应用于自定义修饰符,例如CallConvCdeclIsConstIsLong

The CustomMod element syntax diagram

图 3:CustomMod 元素语法图

示例 1

在下面的示例中,我们使用 modreq 修饰符标注了 TestField 字段,因此 CustomMod 位于 FieldSig 签名内,该签名在文章开头、图 2 中显示。IsLong 指示符区分了 C++ 中的 long 和整数,但实际上,在本例中,此自定义修饰符没有特殊语义,我们只是想演示签名中 CustomMod 元素 的格式。TypeDefOrRefEncoded 元素的值显示了两次,使用两种数字系统——十六进制(<sub>16</sub> 下标)和二进制(<sub>2</sub> 下标)。在下一节中,您将看到原因。

// Full source: CustomMod\1.il
// Binary: CustomMod\1.dll
// (...)

.field public int64 modreq([mscorlib]System.CompilerServices.IsLong) TestField

下表展示了由 modreq 关键字生成的、由 Field.Signature 列索引的整个 FieldSig 签名,以及嵌入的自定义修饰符。

偏移量 含义
0x01 0x04 签名大小
0x02 0x06 FieldSig 的前导符
0x03 0x1F 遇到自定义必需修饰符(modreq),参见第一部分中的常量
0x04 0x0516

000001012

TypeDefOrRefEncoded 元素,在此情况下,它指向 TypeRef 表的第一行,即IsLong 类。该元素将在下一节中描述。
0x05 0x0A 字段的类型(int64),参见第一部分中的常量

2.2 TypeDefOrRefEncoded

现在我们将尝试揭开目前最神秘的元素,幸运的是,即 TypeDefOrRefEncoded,它并不像看起来那么复杂。该元素确定引用的类型信息位于哪个元数据表以及该表的哪一行。前两个最低有效编码元数据表:0 表示 TypeDef(引用的类型位于当前程序集中),1 表示 TypeRef(引用的类型位于单独的程序集中),2 表示 TypeSpec(引用的类型是泛型类型、数组等,参见4.9 TypeSpec)。其余位编码行索引。请注意,索引是从 1 开始的,换句话说,每个元数据表中的第一行始终是 1,而不是 0

示例 1

在此示例中,我们声明了一个带有自定义必需修饰符的单个字段。modreq 接受声明在同一程序集中的 TestClass 类型作为参数,如下所示。

// Full source: TypeDefOrRefEncoded\1.il
// Binary: TypeDefOrRefEncoded\1.dll
// (...)

.class public TestClass extends [mscorlib]System.Object { }

.field public int64 modreq(TestClass) TestField

上面示例代码的 FieldSig 如下所示:

偏移量 含义
0x01 0x04 签名大小
0x02 0x06 FieldSig 的前导符
0x03 0x1F 遇到自定义必需修饰符(modreq),参见第一部分中的常量
0x04 0x0816

000010002

TypeDefOrRefEncoded 元素,这次它指向 TypeDef 表的第二行。前两位最低有效位表示表类型(002 - TypeDef),位 3 到 8 表示表中的行号(0000102 - 2),即 TestClass。现在,将其与上一节中的 TypeDefOrRefEncoded 元素进行比较。
0x05 0x0A 字段的类型(int64),参见第一部分中的常量

2.3 Param

此元素描述了提供给方法或属性的单个参数,因此它是 PropertySigMethodDefSigMethodRefSig 等的一部分。这是 Param 元素的语法图:

The Param element syntax diagram

图 4:Param 元素语法图

示例 1

在下面的 TestMethod 方法中,单个参数附加了两个自定义修饰符。此示例的目的是演示 Param 元素 的格式,并再次展示 TypeDefOrRefEncoded 元素 的工作方式。

// Full source: Param\1.il
// Binary: Param\1.dll
// (...)

.class public TestClass extends [mscorlib]System.Object { }

.method public static void TestMethod(int32 modopt(TestClass) 
        modreq([mscorlib]System.Runtime.CompilerServices.IsLong) Param1) 
{
    ret
}

该方法的关联 MethodDefSig 签名是:

偏移量 含义
0x01 0x08 签名大小
0x02 0x00 方法是static
0x03 0x01 参数数量
0x04 0x01 返回值的类型(void),参见第一部分中的常量
0x05 0x1F 遇到自定义必需修饰符(modreq),参见第一部分中的常量
0x06 0x0916

000010012

引用的行是 TypeRef 元数据表中的第 2 行,即 IsLong 类型
0x07 0x20 遇到自定义可选修饰符(modopt),参见第一部分中的常量
0x08 0x0816

000010002

引用的行是 TypeDef 元数据表中的第 2 行,即 TestClass 类型
0x09 0x08 第一个参数的类型(int32),参见第一部分中的常量

2.4 RetType

此元素几乎与 Param 元素相同,它多了一个可以包含 VOID 类型的额外路径。由于下面该元素的语法图具有自解释性,因此本小节不提供示例。

The RetType element syntax diagram

图 5:RetType 元素语法图

2.5 Type

Type 元素描述类型(int32boolstring 等)以及数组、泛型实例类型和复杂类型(类和结构),这难道不令人惊讶吗?下面的列表展示了该元素的语法图,当然,用大写字母书写的词是常量,其值可以在第一部分中的常量表中找到。您可能会想,为什么 GENERICINST 常量是该元素的一部分,但请记住,TypeSpecMethodSpecMethodDefSig 签名有不同的目的!

Type ::=	  
BOOLEAN | CHAR | I1 | U1 | I2 | U2 | I4 | U4 | I8 | U8 | R4 | R8 | I | U |
| ARRAY Type ArrayShape
| CLASS TypeDefOrRefEncoded
| FNPTR MethodDefSig
| FNPTR MethodRefSig
| GENERICINST (CLASS | VALUETYPE) TypeDefOrRefEncoded GenArgCount Type *
| MVAR number
| OBJECT
| PTR CustomMod* Type
| PTR CustomMod* VOID
| STRING
| SZARRAY CustomMod* Type
| VALUETYPE TypeDefOrRefEncoded
| VAR number

示例 1

让我们看看当方法接受泛型类型作为普通参数时,MethodDefSig 签名会发生什么。

// Full source: Type\1.cs
// Binary: Type\1.dll
// (...)

public class TestClass<GenArg1, GenArg2> { }

public class TestRunClass
{
    public void TestRunMethod()
    {
        TestMethod(new TestClass<int, string>());
    }

    public void TestMethod(TestClass<int, string> Param1) { }
}

剖析 TestMethod 方法的 MethodDefSig 签名。

偏移量 含义
0x0E 0x09 签名大小
0x0F 0x20 该方法是实例方法
0x10 0x01 普通参数的数量
0x11 0x01 返回值的类型(void),参见第一部分中的常量
0x12 0x15 第一个参数的类型是泛型类型(GENERICINST),参见第一部分中的常量
0x13 0x12 第一个参数的类型是泛型类(CLASS),参见第一部分中的常量

2.6 ArrayShape

我认为许多使用 .NET 平台的人知道数组可以有多个维度,但不知道数组中的每个维度都可以有一个下界,这可能是因为大多数开发人员使用 C# 语言,该语言不允许使用下界,除非使用 Array.CreateInstance 方法来创建此类数组类型。ArrayShape 元素保存了维数组的完整定义。它存储数组的维度数量、每个维度的长度和下边界。下面的语法图以及从规范中复制的简要描述。

The ArrayShape element syntax diagram

图 6:ArrayShape 元素语法图

Rank 是一个整数(以压缩形式存储,参见 §23.2),指定数组中的维度数(应为 1 或更多)。NumSizes 是一个压缩整数,表示有多少个维度具有指定的大小(应为 0 或更多)。Size 是一个压缩整数,指定该维度的长度——序列从第一个维度开始,总共包含 NumSizes 项。类似地,NumLoBounds 是一个压缩整数,表示有多少个维度具有指定的下界(应为 0 或更多)。LoBound 是一个压缩整数,指定该维度的下界——序列从第一个维度开始,总共包含 NumLoBounds 项。这两个序列中的任何维度都不能跳过,但指定维度的数量可以少于 Rank

注意:请勿混淆多维数组和交错数组(jagged arrays)。CIL 中的多维数组可以是例如 int32[,],而交错数组是 int32[][]。另请注意,ArrayShape 仅存储关于维数组的信息!单维数组表示为 SZARRAY 常量——仅此而已(参见Type 元素)。要了解有关 .NET 中数组的更多信息,请参阅 MSDN 杂志上的 .NET 中的数组类型文章。

重要提示:不幸的是,正如我们在第二个示例中将看到的,ILASM 编译器在处理数组的下界(图 6 上的 LoBound 字段)时存在一些问题,下界乘以了两次!这当然是不正确的,因为规范说明下界应在签名中存储而无需进行任何更改。下面,您可以看到一个从规范复制的表,其中显示了示例数组声明及其在 ArrayShape 元素中的正确参数。此外,规范没有指定 NumSizesNumLoBounds 字段在什么情况下可能小于 Rank 字段。根据我的观察,NumSizesNumLoBounds 字段仅在一种情况下小于 Rank——当所有维度指定下界时(这在下表第二行中表示);否则,NumSizesNumLoBounds 始终等于 Rank。这与下表中第三行和第五行的情况相矛盾。

声明 类型 NumSizes 大小 NumLoBounds LoBound
[0...2] I4 1 1 3 0 -
[,,,,,,] I4 7 0 - 0 -
[0...3, 0...2,,,,] I4 6 2 4 3 2 0 0
[1...2, 6...8] I4 2 2 2 3 2 1 6
[5, 3...5, , ] I4 4 2 5 3 2 0 3

示例 1

让我们看看 ArrayShape 的实际工作原理。

// Full source: ArrayShape\1.il
// Binary: ArrayShape\1.dll
// (...)

.field public int32[,,] TestField

上述多维数组应生成以下 FieldSig 签名。

偏移量 含义
0x01 0x06 签名大小
0x02 0x06 FieldSig 的前导符
0x03 0x14 字段类型值为 ARRAY,参见第一部分中的常量
0x04 0x08 数组类型为 int32,参见第一部分中的常量
0x05 0x03 数组的维度数(图 6 上的 Rank 字段)
0x06 0x00 未指定数组维度的大小(图 6 上的 NumSizes 字段)
0x07 0x00 未指定数组维度的下界(图 6 上的 NumLoBounds 字段)

示例 2

此示例旨在向您展示当声明带有指定下界的多维数组时 ArrayShape 元素 的行为。

// Full source: ArrayShape\2.il
// Binary: ArrayShape\2.dll
// (...)

.field public int32[0...5,,4...6] TestField

整个 FieldSig 签名如下:

偏移量 含义
0x01 0x0C 签名大小
0x02 0x06 FieldSig 的前导符
0x03 0x14 字段类型值为 ARRAY,参见第一部分中的常量
0x04 0x08 数组类型为 int32,参见第一部分中的常量
0x05 0x03 数组的维度数(图 6 上的 Rank 字段)
0x06 0x03 此数组的大小数(图 6 上的 NumSizes 字段)
0x07 0x06 数组第一个维度的长度(图 6 上的 Size 字段)
0x08 0x00 第二个数组维度的长度,零表示未指定(图 6 上的 Size 字段)
0x09 0x03 第三个数组维度的长度(图 6 上的 Size 字段)
0x0A 0x03 此数组的下界数量(图 6 上的 NumLoBounds 字段)
0x0B 0x00 数组第一个维度的下界(图 6 上的 LoBound 字段)
0x0C 0x00 数组第二个维度的下界(图 6 上的 LoBound 字段)
0x0D 0x08 数组第三个维度的下界(图 6 上的 LoBound 字段)。边界乘以二,参见当前小节开头的重要说明

示例 3

现在让我们看看 ArrayShape 元素 在现实中的样子,并将结果与规范进行比较。

// Full source: ArrayShape\3.il
// Binary: ArrayShape\3.dll
// (...)

.field public int32[0...2] TestField

是的,NumLoBounds 等于 Rank,尽管规范说明 NumLoBounds 应等于零。

偏移量 含义
0x01 0x08 签名大小
0x02 0x06 FieldSig 的前导符
0x03 0x14 字段类型值为 ARRAY,参见第一部分中的常量
0x04 0x08 数组类型为 int32,参见第一部分中的常量
0x05 0x01 数组的维度数(图 6 上的 Rank 字段)
0x06 0x01 此数组的大小数(图 6 上的 NumSizes 字段)
0x07 0x03 数组第一个维度的长度(图 6 上的 Size 字段)
0x08 0x01 此数组的下界数量(图 6 上的 NumLoBounds 字段)
0x09 0x00 数组第一个维度的下界(图 6 上的 LoBound 字段)

3. 结论

正如您所见,签名是一个复杂的怪物,但它使得 .NET 可执行文件小巧、紧凑且一致。如果您有任何疑问、提示或请求,请随时在下方添加评论,建设性评论始终受到欢迎。

4. 参考资料

5. 修订历史

  • 1.0:2009 年 9 月 26 日:初始发布
© . All rights reserved.