PDB DBI (调试信息) 流

简介

PDB DBI 流(索引 3)是 PDB 文件中最大且最重要的流之一。它包含有关程序如何编译的信息(例如,编译标志等)、用于链接程序在一起的编译单元(例如,目标文件)、用于构建程序的源文件,以及对其他流的引用,这些流包含有关每个编译单元的更详细信息,例如每个编译单元中包含的 CodeView 符号记录以及函数和每个编译单元中其他符号的源和行信息。

流头部

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

struct DbiStreamHeader {
  int32_t VersionSignature;
  uint32_t VersionHeader;
  uint32_t Age;
  uint16_t GlobalStreamIndex;
  uint16_t BuildNumber;
  uint16_t PublicStreamIndex;
  uint16_t PdbDllVersion;
  uint16_t SymRecordStream;
  uint16_t PdbDllRbld;
  int32_t ModInfoSize;
  int32_t SectionContributionSize;
  int32_t SectionMapSize;
  int32_t SourceInfoSize;
  int32_t TypeServerMapSize;
  uint32_t MFCTypeServerIndex;
  int32_t OptionalDbgHeaderSize;
  int32_t ECSubstreamSize;
  uint16_t Flags;
  uint16_t Machine;
  uint32_t Padding;
};
  • VersionSignature - 未知含义。似乎始终为 -1

  • VersionHeader - 来自以下枚举的值。

enum class DbiStreamVersion : uint32_t {
  VC41 = 930803,
  V50 = 19960307,
  V60 = 19970606,
  V70 = 19990903,
  V110 = 20091201
};

类似于 PDB 流,此值似乎始终为 V70,并且不清楚其他值的用途。

  • Age - PDB 被写入的次数。等于 PDB 流头部 中的相同字段。

  • GlobalStreamIndex - 全局符号流 的索引,其中包含所有全局符号的 CodeView 符号记录。实际记录存储在符号记录流中,并从此流引用。

  • BuildNumber - 一个位字段,包含表示用于构建程序的工具链的主版本号和次版本号的值(例如,MSVC 2013 为 12.0),具有以下布局

uint16_t MinorVersion : 8;
uint16_t MajorVersion : 7;
uint16_t NewVersionFormat : 1;

为了 LLVM 的目的,我们假设 NewVersionFormat 始终为 true。如果为 false,则以上布局不适用,读者应查阅 Microsoft 源代码 以获得进一步指导。

  • PublicStreamIndex - 公共符号流 的索引,其中包含所有公共符号的 CodeView 符号记录。实际记录存储在符号记录流中,并从此流引用。

  • PdbDllVersion - 用于生成此 PDB 的 mspdbXXXX.dll 的版本号。请注意,这显然不适用于 LLVM,因为 LLVM 不使用 mspdb.dll

  • SymRecordStream - 包含程序使用的所有 CodeView 符号记录的流。这用于去重,以便许多不同的编译单元可以引用相同的符号,而无需在每个模块流中包含完整的记录内容。

  • PdbDllRbld - 未知

  • MFCTypeServerIndex - 类型服务器映射子流 中 MFC 类型服务器的索引。

  • Flags - 具有以下布局的位字段,包含有关程序如何构建的各种信息

uint16_t WasIncrementallyLinked : 1;
uint16_t ArePrivateSymbolsStripped : 1;
uint16_t HasConflictingTypes : 1;
uint16_t Reserved : 13;

其中唯一不是不言自明的是 HasConflictingTypes。虽然未记录在案,但 link.exe 包含一个隐藏标志 /DEBUG:CTYPES。如果将其传递给 link.exe,则将设置此字段。否则,将不会设置。目前尚不清楚此标志的作用,尽管它似乎对用于查找类型记录的算法有细微的影响。

  • Machine - 来自 CV_CPU_TYPE_e 枚举的值。常见值为 0x8664 (x86-64) 和 0x14C (x86)。

紧跟在固定大小的 DBI 流头部之后是 7 个可变长度的子流。DBI 流头部的以下 7 个字段指定了相应子流的字节数。每个子流的内容将在 下面 详细描述。整个 DBI 流的长度应等于 64(以上头部的长度)加上以下 7 个字段中每个字段的值。

子流

模块信息子流

0 偏移量开始,紧跟在 头部 之后。模块信息子流是一个可变长度记录的数组,每个记录描述链接到程序中的单个模块(例如,目标文件)。数组中的每个记录都具有以下格式

struct ModInfo {
  uint32_t Unused1;
  struct SectionContribEntry {
    uint16_t Section;
    char Padding1[2];
    int32_t Offset;
    int32_t Size;
    uint32_t Characteristics;
    uint16_t ModuleIndex;
    char Padding2[2];
    uint32_t DataCrc;
    uint32_t RelocCrc;
  } SectionContr;
  uint16_t Flags;
  uint16_t ModuleSymStream;
  uint32_t SymByteSize;
  uint32_t C11ByteSize;
  uint32_t C13ByteSize;
  uint16_t SourceFileCount;
  char Padding[2];
  uint32_t Unused2;
  uint32_t SourceFileNameIndex;
  uint32_t PdbFilePathNameIndex;
  char ModuleName[];
  char ObjFileName[];
};
  • SectionContr - 描述最终二进制文件中节的属性,其中包含来自此模块的代码和数据。

    SectionContr.Characteristics 对应于 IMAGE_SECTION_HEADER 结构的 Characteristics 字段。

  • Flags - 具有以下格式的位字段

// ``true`` if this ModInfo has been written since reading the PDB.  This is
// likely used to support incremental linking, so that the linker can decide
// if it needs to commit changes to disk.
uint16_t Dirty : 1;
// ``true`` if EC information is present for this module. EC is presumed to
// stand for "Edit & Continue", which LLVM does not support.  So this flag
// will always be false.
uint16_t EC : 1;
uint16_t Unused : 6;
// Type Server Index for this module.  This is assumed to be related to /Zi,
// but as LLVM treats /Zi as /Z7, this field will always be invalid for LLVM
// generated PDBs.
uint16_t TSM : 8;
  • ModuleSymStream - 包含此模块的符号信息的流的索引。这包括 CodeView 符号信息以及源和行信息。如果此字段为 -1,则此模块将不存在其他调试信息(例如,这是从 PDB 中剥离私有符号时发生的情况)。

  • SymByteSize - 由 ModuleSymStream 标识的流中表示 CodeView 符号记录的数据字节数。

  • C11ByteSize - 由 ModuleSymStream 标识的流中表示 C11 样式 CodeView 行信息的数据字节数。

  • C13ByteSize - 由 ModuleSymStream 标识的流中表示 C13 样式 CodeView 行信息的数据字节数。 C11ByteSizeC13ByteSize 中最多只有一个为非零。现代 PDB 始终使用 C13 而不是 C11。

  • SourceFileCount - 在编译期间为此模块贡献的源文件数。

  • SourceFileNameIndex - 用于构建此模块的主翻译单元的名称缓冲区中的偏移量。迄今为止观察到的所有 PDB 文件始终将此值等于 0。

  • PdbFilePathNameIndex - 包含此模块符号信息的 PDB 文件的名称缓冲区中的偏移量。仅对于特殊的 * Linker * 模块观察到此值为非零。

  • ModuleName - 模块名称。这通常是目标文件的完整路径(直接传递给 link.exe 或来自存档)或 Import:<dll name> 形式的字符串。

  • ObjFileName - 目标文件名。对于直接传递给 link.exe 的模块,这与 ModuleName 相同。对于来自存档的模块,这通常是存档的完整路径。

节贡献子流

0 偏移量开始,紧跟在 模块信息子流 结束后,并消耗 Header->SectionContributionSize 字节。此子流以单个 uint32_t 开头,它将是以下值之一

enum class SectionContrSubstreamVersion : uint32_t {
  Ver60 = 0xeffe0000 + 19970605,
  V2 = 0xeffe0000 + 20140516
};

Ver60 是迄今为止在 PDB 中观察到的唯一值。之后是一个固定长度结构的数组。如果版本是 Ver60,则它是 SectionContribEntry 结构的数组(这是 ModInfo 类型中的嵌套结构。如果版本是 V2,则它是 SectionContribEntry2 结构的数组,定义如下

struct SectionContribEntry2 {
  SectionContribEntry SC;
  uint32_t ISectCoff;
};

第二个字段的用途尚不清楚。名称暗示它是 COFF 节的索引,但这也描述了现有字段 SectionContribEntry::Section

节映射子流

0 偏移量开始,紧跟在 节贡献子流 结束后,并消耗 Header->SectionMapSize 字节。此子流以 4 字节头部开头,后跟一个固定长度记录数组。头部和记录具有以下布局

struct SectionMapHeader {
  uint16_t Count;    // Number of segment descriptors
  uint16_t LogCount; // Number of logical segment descriptors
};

struct SectionMapEntry {
  uint16_t Flags;         // See the SectionMapEntryFlags enum below.
  uint16_t Ovl;           // Logical overlay number
  uint16_t Group;         // Group index into descriptor array.
  uint16_t Frame;
  uint16_t SectionName;   // Byte index of segment / group name in string table, or 0xFFFF.
  uint16_t ClassName;     // Byte index of class in string table, or 0xFFFF.
  uint32_t Offset;        // Byte offset of the logical segment within physical segment.  If group is set in flags, this is the offset of the group.
  uint32_t SectionLength; // Byte count of the segment or group.
};

enum class SectionMapEntryFlags : uint16_t {
  Read = 1 << 0,              // Segment is readable.
  Write = 1 << 1,             // Segment is writable.
  Execute = 1 << 2,           // Segment is executable.
  AddressIs32Bit = 1 << 3,    // Descriptor describes a 32-bit linear address.
  IsSelector = 1 << 8,        // Frame represents a selector.
  IsAbsoluteAddress = 1 << 9, // Frame represents an absolute address.
  IsGroup = 1 << 10           // If set, descriptor represents a group.
};

这些字段中的许多字段尚不清楚,因此将不再进一步讨论。

文件信息子流

0 偏移量开始,紧跟在 节映射子流 结束后,并消耗 Header->SourceInfoSize 字节。此子流定义从模块到为此模块贡献的源文件的映射。由于多个模块可以使用相同的源文件(例如,头文件),因此此子流使用字符串表来仅存储每个唯一的文件名一次,然后让每个模块使用字符串表中的偏移量,而不是直接嵌入字符串的值。此子流的格式如下

struct FileInfoSubstream {
  uint16_t NumModules;
  uint16_t NumSourceFiles;

  uint16_t ModIndices[NumModules];
  uint16_t ModFileCounts[NumModules];
  uint32_t FileNameOffsets[NumSourceFiles];
  char NamesBuffer[][NumSourceFiles];
};

NumModules - 此子流中包含源文件信息的模块数。应与 ref:dbi_header 中的相应值匹配。

NumSourceFiles:理论上,这应该包含此子流包含信息的源文件数。但这会带来一个问题,即此字段的宽度为 16 位,这将阻止程序中包含超过 64K 个源文件。在文件格式的早期版本中,情况似乎就是这样。为了支持更多,此字段被简单地忽略,并通过对 ModFileCounts 数组(下面讨论)的值求和来动态计算。简而言之,应忽略此值。

ModIndices - 此数组存在,但似乎没有用。

ModFileCountArray - 一个 NumModules 整数的数组,每个整数包含为指定索引处的模块贡献的源文件数。虽然每个模块仅限于 64K 个贡献源文件,但所有模块的源文件的并集可能大于 64K。因此,源文件的实际数量是通过对该数组求和来计算的。请注意,对该数组求和并不能给出 唯一 源文件的数量,而仅给出对模块的源文件贡献的总数。

FileNameOffsets - 一个 NumSourceFiles 整数的数组(其中 NumSourceFiles 在这里指的是从对 ModFileCountArray 求和获得的 32 位值),其中每个整数都是指向 NamesBuffer 中以 null 结尾的字符串的偏移量。

NamesBuffer - 一个以 null 结尾的字符串数组,其中包含实际的源文件名。

类型服务器映射子流

0 偏移量开始,紧跟在 文件信息子流 结束后,并消耗 Header->TypeServerMapSize 字节。此子流的目的和布局均不清楚,尽管假定它以某种方式与 /Zimspdbsrv.exe 的使用相关。将不再进一步讨论此子流。

EC 子流

0 偏移量开始,紧跟在 类型服务器映射子流 结束后,并消耗 Header->ECSubstreamSize 字节。据推测,这与 MSVC 中的编辑 & 继续支持有关。 LLVM 不支持编辑 & 继续,因此将不再进一步讨论此流。

可选调试头部流

0 偏移量开始,紧跟在 EC 子流 结束后,并消耗 Header->OptionalDbgHeaderSize 字节。此字段是一个流索引数组(例如, uint16_t),每个索引都标识 MSF 文件中较大的流索引,其中包含一些额外的调试信息。此数组的每个位置都有特殊的含义,允许人们确定引用的流中调试信息的类型。目前已理解 11 个索引,尽管可能还有更多。每个流的布局通常与 PE/COFF 文件中的特定类型的调试数据目录完全对应。这些字段的格式可以在 Microsoft PE/COFF 规范 中找到。如果这些字段中的任何一个为 -1,则表示 PDB 中不存在相应类型的调试信息。

FPO 数据 - DbgStreamArray[0]。引用流中的数据是 FPO_DATA 结构的数组。这包含来自任何链接器输入的任何 .debug$F 节的重定位内容。

异常数据 - DbgStreamArray[1]。引用流中的数据是 IMAGE_DEBUG_TYPE_EXCEPTION 类型的调试数据目录。

修复数据 - DbgStreamArray[2]。引用流中的数据是 IMAGE_DEBUG_TYPE_FIXUP 类型的调试数据目录。

Omap To Src 数据 - DbgStreamArray[3]。引用流中的数据是 IMAGE_DEBUG_TYPE_OMAP_TO_SRC 类型的调试数据目录。这用于在检测代码和未检测代码之间映射地址。

Omap From Src 数据 - DbgStreamArray[4]。引用流中的数据是 IMAGE_DEBUG_TYPE_OMAP_FROM_SRC 类型的调试数据目录。这用于在检测代码和未检测代码之间映射地址。

节头部数据 - DbgStreamArray[5]。来自原始可执行文件的所有节头部的转储。

令牌 / RID 映射 - DbgStreamArray[6]。此流的布局尚不清楚,但假定它是从 CLR 令牌CLR 记录 ID 的映射。有关更多信息,请参阅 ECMA 335

Xdata - DbgStreamArray[7]。可执行文件中的 .xdata 节的副本。

Pdata - DbgStreamArray[8]。假定这是可执行文件中的 .pdata 节的副本,但这会使其与 DbgStreamArray[1] 相同。这两个索引之间的区别尚不清楚。

新 FPO 数据 - DbgStreamArray[9]。引用流中的数据是 IMAGE_DEBUG_TYPE_FPO 类型的调试数据目录。请注意,这与 DbgStreamArray[0] 不同,因为 .debug$F 节仅由 MASM 发出。因此,如果 MASM 目标文件和 cl 目标文件都链接到同一程序中,则两者都可能出现在同一 PDB 中。

原始节头部数据 - DbgStreamArray[10]。类似于 DbgStreamArray[5],但包含在执行任何二进制转换之前的节头部。这可以与 DebugStreamArray[3]DbgStreamArray[4] 结合使用,以映射检测地址和未检测地址。