PDB 信息流(又称 PDB 流)

流头部

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

struct PdbStreamHeader {
  ulittle32_t Version;
  ulittle32_t Signature;
  ulittle32_t Age;
  Guid UniqueId;
};
  • 版本 - 来自以下枚举的值

enum class PdbStreamVersion : uint32_t {
  VC2 = 19941610,
  VC4 = 19950623,
  VC41 = 19950814,
  VC50 = 19960307,
  VC98 = 19970604,
  VC70Dep = 19990604,
  VC70 = 20000404,
  VC80 = 20030901,
  VC110 = 20091201,
  VC140 = 20140508,
};

虽然此字段的含义似乎很明显,但在实践中,即使使用现代版本的工具链,我们也从未观察到除 VC70 之外的值,并且不清楚其他值存在的原因。假设如果该值不是 VC70,则 PDB 流的布局,甚至可能是其他流的布局的某些方面将会改变。

  • 签名 - 在写入 PDB 文件时,通过调用 time() 生成的 32 位时间戳。请注意,由于使用 1 秒粒度的时间戳固有的唯一性问题,此字段实际上并未达到其预期目的,因此通常被忽略,而倾向于使用下面描述的 Guid 字段。

  • 年龄 - PDB 文件已被写入的次数。它可以与 Guid 一起用于将 PDB 与其相应的可执行文件匹配。

  • Guid - 一个 128 位标识符,保证在空间和时间上都是唯一的。一般来说,这可以被认为是调用 Win32 API UuidCreate 的结果,尽管 LLVM 不能依赖于此,因为它必须在非 Windows 平台上工作。

命名流映射

头部之后是一个序列化的哈希表,其键类型是字符串,值类型是整数。映射 X -> Y 的存在意味着名称为 X 的流在底层 MSF 文件中具有流索引 Y。请注意,并非所有流都已命名(例如,TPI 流 具有固定的索引,因此无需按名称查找其索引)。实际上,通常只有少量命名流,这些流在 PDB 文件格式 的流表中枚举。由此推论,如果流确实有名称(因此在命名流映射中),那么查阅命名流映射可能是发现流的 MSF 流索引的唯一方法。几个重要的流(例如全局字符串表,称为 /names)只能通过这种方式定位,因此正确生成和使用它是很重要的,因为工具在没有它的情况下将无法正常工作。

重要提示

有些流通过固定索引定位(例如 TPI 流的索引为 2),但其他流通过固定名称定位(例如字符串表称为 /names),并且只能通过查阅命名流映射来定位。

命名流映射的磁盘布局由 2 个组件组成。第一个是字符串数据缓冲区,前缀为 32 位长度。第二个是序列化的哈希表,其键和值类型均为 uint32_t。键是字符串数据缓冲区中以 null 结尾的字符串的偏移量,指定流的名称,值是具有所述名称的流的 MSF 流索引。请注意,尽管键是整数,但用于查找正确 bucket 的哈希函数会对字符串数据缓冲区中相应偏移量处的字符串进行哈希处理。

序列化哈希表的磁盘布局在 PDB 序列化哈希表格式 中描述。

请注意,整个命名流映射没有长度前缀,因此获取其后数据的唯一方法是完全反序列化它。

PDB 特性代码

在命名流映射之后,并消耗 PDB 流的所有剩余字节的是来自以下枚举的值列表

enum class PdbRaw_FeatureSig : uint32_t {
  VC110 = 20091201,
  VC140 = 20140508,
  NoTypeMerge = 0x4D544F4E,
  MinimalDebugInfo = 0x494E494D,
};

这些值的含义总结在下表中

标志

含义

VC110

  • 不存在其他特性标志

  • PDB 包含 IPI 流

VC140

  • 可能存在其他特性标志

  • PDB 包含 IPI 流

NoTypeMerge

  • 据推测,重复类型可能出现在 TPI 流中,尽管不清楚为什么会发生这种情况。

MinimalDebugInfo

  • 程序使用 /DEBUG:FASTLINK 链接

  • 没有 TPI / IPI 流,所有类型信息都包含在原始目标文件中。

将 PDB 与其可执行文件匹配

链接器负责写入 PDB 和最终的可执行文件,因此是唯一能够写入将 PDB 与可执行文件匹配所需信息的实体。

为了完成此操作,链接器为 PDB 生成一个 guid(或者如果它正在增量链接,则重用现有的 guid)并递增 Age 字段。

可执行文件是一个 PE/COFF 文件,PE/COFF 文件的一部分是存在许多“目录”。对于我们这里的目的,我们对“调试目录”感兴趣。调试目录的确切格式由 IMAGE_DEBUG_DIRECTORY 结构 描述。对于这种特定情况,链接器发出类型为 IMAGE_DEBUG_TYPE_CODEVIEW 的调试目录。此记录的格式在 llvm/DebugInfo/CodeView/CVDebugRecord.h 中定义,但这里只需说它包括相同的 GuidAge 字段。在运行时,调试器或工具可以扫描 COFF 可执行映像是否存在正确类型的调试目录,并验证 Guid 和 Age 是否匹配。