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 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]
结合使用,以映射检测地址和未检测地址。