PDB TPI 和 IPI 流

简介

PDB TPI 流(索引 2)和 IPI 流(索引 4)包含程序中使用的所有类型的信息。它组织为头部,后跟CodeView 类型记录的列表。类型通过其类型索引从整个 PDB 中的各种流和记录中引用。通常,跟随头部的类型记录序列形成拓扑排序的 DAG(有向无环图),这意味着只有当 A.TypeIndex < B.TypeIndex 时,类型记录 B 才能引用类型 A。虽然在极少数情况下此属性不成立(尤其是在处理使用 MASM 编译的目标文件时),但实现应尽力使此属性成立,因为这意味着可以在单次遍历中构建整个类型图。

重要提示

类型记录形成拓扑排序的 DAG(有向无环图)。

TPI 与 IPI 流

PDB 格式的最新版本(又名本文档涵盖的所有版本)具有 2 个布局相同的流,以下称为 TPI 流和 IPI 流。本文档后续描述的磁盘格式内容同样适用于 TPI 流或 IPI 流。两者之间唯一的区别在于哪些 CodeView 记录允许出现在每个流中,总结如下表

TPI 流

IPI 流

LF_POINTER

LF_FUNC_ID

LF_MODIFIER

LF_MFUNC_ID

LF_PROCEDURE

LF_BUILDINFO

LF_MFUNCTION

LF_SUBSTR_LIST

LF_LABEL

LF_STRING_ID

LF_ARGLIST

LF_UDT_SRC_LINE

LF_FIELDLIST

LF_UDT_MOD_SRC_LINE

LF_ARRAY

LF_CLASS

LF_STRUCTURE

LF_INTERFACE

LF_UNION

LF_ENUM

LF_TYPESERVER2

LF_VFTABLE

LF_VTSHAPE

LF_BITFIELD

LF_METHODLIST

LF_PRECOMP

LF_ENDPRECOMP

这些记录的用法在CodeView 类型记录中有更详细的描述。

类型索引

类型索引是一个 32 位整数,用于唯一标识对象文件的 .debug$T 节或 PDB 文件的 TPI 或 IPI 流中的类型。来自 TPI 流的第一个类型记录的类型索引值由TPI 流头部TypeIndexBegin 成员给出,尽管实际上此值始终等于 0x1000 (4096)。

任何高位设置的类型索引都被认为来自 IPI 流,尽管这似乎更像是一种 hack,并且 LLVM 不会生成这种性质的类型索引。但是,偶尔可以在 Microsoft PDB 中观察到它们,因此应准备好处理它们。请注意,设置高位不是确定类型索引是否来自 IPI 流的必要条件,它只是充分条件。

一旦清除高位,任何类型索引 >= TypeIndexBegin 都被假定为来自相应的流,并且任何小于此值的类型索引都是一个位掩码,可以分解如下

.---------------------------.------.----------.
|           Unused          | Mode |   Kind   |
'---------------------------'------'----------'
|+32                        |+12   |+8        |+0
  • Kind - 来自以下枚举的值

enum class SimpleTypeKind : uint32_t {
  None = 0x0000,          // uncharacterized type (no type)
  Void = 0x0003,          // void
  NotTranslated = 0x0007, // type not translated by cvpack
  HResult = 0x0008,       // OLE/COM HRESULT

  SignedCharacter = 0x0010,   // 8 bit signed
  UnsignedCharacter = 0x0020, // 8 bit unsigned
  NarrowCharacter = 0x0070,   // really a char
  WideCharacter = 0x0071,     // wide char
  Character16 = 0x007a,       // char16_t
  Character32 = 0x007b,       // char32_t
  Character8 = 0x007c,        // char8_t

  SByte = 0x0068,       // 8 bit signed int
  Byte = 0x0069,        // 8 bit unsigned int
  Int16Short = 0x0011,  // 16 bit signed
  UInt16Short = 0x0021, // 16 bit unsigned
  Int16 = 0x0072,       // 16 bit signed int
  UInt16 = 0x0073,      // 16 bit unsigned int
  Int32Long = 0x0012,   // 32 bit signed
  UInt32Long = 0x0022,  // 32 bit unsigned
  Int32 = 0x0074,       // 32 bit signed int
  UInt32 = 0x0075,      // 32 bit unsigned int
  Int64Quad = 0x0013,   // 64 bit signed
  UInt64Quad = 0x0023,  // 64 bit unsigned
  Int64 = 0x0076,       // 64 bit signed int
  UInt64 = 0x0077,      // 64 bit unsigned int
  Int128Oct = 0x0014,   // 128 bit signed int
  UInt128Oct = 0x0024,  // 128 bit unsigned int
  Int128 = 0x0078,      // 128 bit signed int
  UInt128 = 0x0079,     // 128 bit unsigned int

  Float16 = 0x0046,                 // 16 bit real
  Float32 = 0x0040,                 // 32 bit real
  Float32PartialPrecision = 0x0045, // 32 bit PP real
  Float48 = 0x0044,                 // 48 bit real
  Float64 = 0x0041,                 // 64 bit real
  Float80 = 0x0042,                 // 80 bit real
  Float128 = 0x0043,                // 128 bit real

  Complex16 = 0x0056,                 // 16 bit complex
  Complex32 = 0x0050,                 // 32 bit complex
  Complex32PartialPrecision = 0x0055, // 32 bit PP complex
  Complex48 = 0x0054,                 // 48 bit complex
  Complex64 = 0x0051,                 // 64 bit complex
  Complex80 = 0x0052,                 // 80 bit complex
  Complex128 = 0x0053,                // 128 bit complex

  Boolean8 = 0x0030,   // 8 bit boolean
  Boolean16 = 0x0031,  // 16 bit boolean
  Boolean32 = 0x0032,  // 32 bit boolean
  Boolean64 = 0x0033,  // 64 bit boolean
  Boolean128 = 0x0034, // 128 bit boolean
};
  • Mode - 来自以下枚举的值

enum class SimpleTypeMode : uint32_t {
  Direct = 0,        // Not a pointer
  NearPointer = 1,   // Near pointer
  FarPointer = 2,    // Far pointer
  HugePointer = 3,   // Huge pointer
  NearPointer32 = 4, // 32 bit near pointer
  FarPointer32 = 5,  // 32 bit far pointer
  NearPointer64 = 6, // 64 bit near pointer
  NearPointer128 = 7 // 128 bit near pointer
};

请注意,对于指针,位宽在模式中表示。因此,如果为 32 位构建,则 void* 将具有 Mode=NearPointer32, Kind=Void 的类型索引,但如果为 64 位构建,则具有 Mode=NearPointer64, Kind=Void 的类型索引。

按照惯例,std::nullptr_t 的类型索引的构造方式与 void* 的类型索引相同,但使用无位枚举值 NearPointer

流头部

TPI 流的偏移量 0 处是一个具有以下布局的头部

struct TpiStreamHeader {
  uint32_t Version;
  uint32_t HeaderSize;
  uint32_t TypeIndexBegin;
  uint32_t TypeIndexEnd;
  uint32_t TypeRecordBytes;

  uint16_t HashStreamIndex;
  uint16_t HashAuxStreamIndex;
  uint32_t HashKeySize;
  uint32_t NumHashBuckets;

  int32_t HashValueBufferOffset;
  uint32_t HashValueBufferLength;

  int32_t IndexOffsetBufferOffset;
  uint32_t IndexOffsetBufferLength;

  int32_t HashAdjBufferOffset;
  uint32_t HashAdjBufferLength;
};
  • Version - 来自以下枚举的值。

enum class TpiStreamVersion : uint32_t {
  V40 = 19950410,
  V41 = 19951122,
  V50 = 19961031,
  V70 = 19990903,
  V80 = 20040203,
};

类似于PDB 流,此值似乎始终为 V80,并且未观察到其他值。假设如果观察到另一个值,则本文档描述的布局可能不准确。

  • HeaderSize - sizeof(TpiStreamHeader)

  • TypeIndexBegin - 表示 TPI 流中第一个类型记录的类型索引的数值。此值通常为 0x1000,因为低于此值的类型索引是保留的(有关保留类型索引的讨论,请参见类型索引)。

  • TypeIndexEnd - 大于表示 TPI 流中最后一个类型记录的类型索引的数值。TPI 流中类型记录的总数可以计算为 TypeIndexEnd - TypeIndexBegin

  • TypeRecordBytes - 头部后面的类型记录数据的字节数。

  • HashStreamIndex - 包含每个类型记录的哈希列表的流的索引。此值可能为 -1,表示哈希信息不存在。实际上,总是观察到有效的流索引,因此任何生产者实现都应准备好发出此流,以确保与可能期望此流存在的工具的兼容性。

  • HashAuxStreamIndex - 据推测,它是包含单独的哈希表的流的索引,尽管在实践中尚未观察到它,并且不清楚它可能用于什么。

  • HashKeySize - 哈希值的大小(通常为 4 个字节)。

  • NumHashBuckets - 用于生成上述哈希流中哈希值的桶数。

  • HashValueBufferOffset / HashValueBufferLength - TPI 哈希流中哈希值列表的偏移量和大小。应假定哈希值的数量为 0,或者等于 TPI 流中类型记录的数量 (TypeIndexEnd - TypeEndBegin)。因此,如果 HashBufferLength 不等于 (TypeIndexEnd - TypeEndBegin) * HashKeySize,我们可以认为 PDB 格式错误。

  • IndexOffsetBufferOffset / IndexOffsetBufferLength - TPI 哈希流中类型索引偏移缓冲区的偏移量和大小。这是一个 uint32_t 对列表,其中第一个值是类型索引,第二个值是具有此索引的类型的类型记录数据中的偏移量。这可用于执行二分查找,然后进行线性搜索,以获得按类型索引的 O(log n) 查找。

  • HashAdjBufferOffset / HashAdjBufferLength - TPI 哈希流中序列化哈希表的偏移量和大小,该哈希表的键是哈希值缓冲区中的哈希值,而其值是类型索引。这在增量链接场景中似乎很有用,因此,如果修改了类型,则可以创建一个条目,将旧哈希值映射到新类型索引,以便 PDB 文件使用者始终可以获得类型的最新版本,而无需强制增量链接器进行垃圾回收并更新指向旧版本的引用以现在指向新版本。此哈希表的布局在PDB 序列化哈希表格式中描述。

CodeView 类型记录列表

在头部之后,有 TypeRecordBytes 字节的数据,这些数据表示CodeView 类型记录的可变长度数组。此类记录的数量(例如,数组的长度)可以通过计算值 Header.TypeIndexEnd - Header.TypeIndexBegin 来确定。

O(log(n)) 访问是通过先前描述的类型索引偏移数组(如果存在)提供的。