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 到 Src 数据 - DbgStreamArray[3]。引用流中的数据是类型为IMAGE_DEBUG_TYPE_OMAP_TO_SRC的调试数据目录。这用于映射检测代码和未检测代码之间的地址。

Omap 从 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]一起使用以映射检测和未检测的地址。