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 位元值),其中每個整數都是指向以 null 結尾的字串的 NamesBuffer 中的偏移量。

NamesBuffer - 包含實際原始程式碼檔案名稱的以 null 結尾的字串陣列。

類型伺服器地圖子串流

檔案資訊子串流 結束後立即開始的偏移量 0 處開始,並消耗 Header->TypeServerMapSize 個位元組。 儘管假定它以某種方式與 /Zimspdbsrv.exe 的使用有關,但此子串流的用途和佈局均不清楚。 此子串流將不再進一步討論。

EC 子串流

類型伺服器地圖子串流 結束後立即開始的偏移量 0 處開始,並消耗 Header->ECSubstreamSize 個位元組。 據推測,這與 MSVC 中的編輯和繼續支援有關。 LLVM 不支援編輯和繼續,因此將不再進一步討論此串流。

選用偵錯標頭串流

EC 子串流 結束後立即開始的偏移量 0 處開始,並消耗 Header->OptionalDbgHeaderSize 個位元組。 此欄位是一個串流索引陣列(例如,uint16_t),每個索引都識別較大的 MSF 檔案中的串流索引,其中包含一些額外的偵錯資訊。 此陣列的每個位置都有特殊的含義,允許人們確定參考串流中偵錯資訊的類型。 目前已理解 11 個索引,儘管有可能會有更多。 每個串流的佈局通常與 PE/COFF 檔案中的特定偵錯資料目錄類型完全對應。 這些欄位的格式可以在 Microsoft PE/COFF 規範中找到。 如果這些欄位中的任何一個為 -1,則表示 PDB 中不存在相應類型的偵錯資訊。

FPO Data - DbgStreamArray[0]。 參考串流中的資料是 FPO_DATA 結構的陣列。 這包含來自任何連結器輸入的任何 .debug$F 區段的重新定位內容。

Exception Data - DbgStreamArray[1]。 參考串流中的資料是 IMAGE_DEBUG_TYPE_EXCEPTION 類型的偵錯資料目錄。

Fixup Data - DbgStreamArray[2]。 參考串流中的資料是 IMAGE_DEBUG_TYPE_FIXUP 類型的偵錯資料目錄。

Omap To Src Data - DbgStreamArray[3]。 參考串流中的資料是 IMAGE_DEBUG_TYPE_OMAP_TO_SRC 類型的偵錯資料目錄。 這用於在檢測程式碼和未檢測程式碼之間映射位址。

Omap From Src Data - DbgStreamArray[4]。 參考串流中的資料是 IMAGE_DEBUG_TYPE_OMAP_FROM_SRC 類型的偵錯資料目錄。 這用於在檢測程式碼和未檢測程式碼之間映射位址。

Section Header Data - DbgStreamArray[5]。 來自原始可執行檔的所有區段標頭的傾印。

Token / RID Map - DbgStreamArray[6]。 此串流的佈局尚不清楚,但假定它是從 CLR TokenCLR Record ID 的映射。 有關更多資訊,請參閱 ECMA 335

Xdata - DbgStreamArray[7].xdata 區段從可執行檔的副本。

Pdata - DbgStreamArray[8]。 假定這是 .pdata 區段從可執行檔的副本,但這會使其與 DbgStreamArray[1] 相同。 這兩個索引之間的差異尚不清楚。

New FPO Data - DbgStreamArray[9]。 參考串流中的資料是 IMAGE_DEBUG_TYPE_FPO 類型的偵錯資料目錄。 請注意,這與 DbgStreamArray[0] 不同,因為 .debug$F 區段僅由 MASM 發出。 因此,如果 MASM 物件檔案和 cl 物件檔案都連結到同一個程式中,則兩者都可能出現在同一個 PDB 中。

Original Section Header Data - DbgStreamArray[10]。 類似於 DbgStreamArray[5],但包含在執行任何二進位翻譯之前的區段標頭。 這可以與 DebugStreamArray[3]DbgStreamArray[4] 結合使用,以映射檢測程式碼和未檢測位址。