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 行資訊的資料位元組數。 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
};

到目前為止,在 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 位元組。此子資料流的用途和佈局尚不清楚,儘管推測它與 /Zimspdbsrv.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] 結合使用,以對應已檢測和未檢測的位址。