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
欄位。旗標 - 具有以下格式的位元欄位
// ``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
};
到目前為止,在 PDB 中僅觀察到 Ver60
值。接下來是一系列固定長度的結構。如果版本是 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 不支援「編輯後繼續」,因此不會進一步討論此資料流。
選用偵錯標頭資料流¶
從 EC 子資料流 結束後的偏移量 0
開始,並佔用 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
的偵錯資料目錄。這用於映射已檢測和未檢測程式碼之間的位址。
從 Src 到 Omap 的資料 - 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]
結合使用,以對應已檢測和未檢測的位址。