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 行信息的数据的字节数。C11ByteSize
和C13ByteSize
中最多只有一个非零。现代 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
字节。此子流的目的和布局都不清楚,尽管假设它与/Zi
和mspdbsrv.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]
一起使用以映射检测和未检测的地址。