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),并且只能通过查询命名流映射来定位。

命名流映射的磁盘上布局由两个部分组成。第一个是字符串数据缓冲区,前面带有 32 位长度。第二个是序列化哈希表,其键和值类型均为 uint32_t。键是字符串数据缓冲区中指定流名称的空终止字符串的偏移量,值是具有该名称的流的 MSF 流索引。请注意,虽然键是整数,但用于查找正确存储桶的哈希函数会对字符串数据缓冲区中相应偏移量处的字符串进行哈希。

序列化哈希表的磁盘上布局在 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 是否匹配。