粗略察看一 下.pdb 文件,會發現在其起始位置存放的是這樣一個字符串“Microsoft C/C++ program database 2.00”。可以看出 PDB 是 Program Database 的首字母縮寫。在 MSDN 中或 Internet 上搜索一下有關 PDB 內部結構的信息,你會發現沒有任何有用的信息,唯一例外的是,在 微軟的基礎知識文章中,微軟申明此種格式是它有的(Microsoft Corporation, 2000d)。就連 Windows 的老大 Matt Pietrek 也承認:
“ PDB符 號 表 的 格 式 並 沒 有 公 開 的 文 檔 。( 就 連 我 也 不 知 道 其 確 切 的 格 式 , 唯 一 知 道 的 是,它會隨着 Visual C++ 的 更 新 而 更 新 。)”( Pietrek 1997a )
或許,pdb 格式會隨着 Visual C/C++一起更新,不過針對當前版本的 Windows 2000 我 可以確切的告訴你 PDB 符號文件的結構。這或許是首次公開的 PDB 格式文檔。但首先,還 是讓我們檢查一下.dbg 和.pdb 文件是如何鏈接到一起的。
Windows 2000 的.dbg 文件的一個顯著特性是:它們包含的數據很少,幾乎可以忽略它 們的 CodeView子節。下面示例給出了 ntsokrnl.exe 的.dbg 文件所包含的整個 CodeView數據, 只有區區 32 字節。
Address | 00 01 02 03-04 05 06 07 : 08 09 0A 0B-0C 0D 0E 0F | 0123456789ABCDEF ---------|-------------------------:-------------------------|----------------- 00006590 | 4E 42 31 30-00 00 00 00 : 20 7D 23 38-54 00 00 00 | NB10.... }#8T... 000065A0 | 6E 74 6F 73-6B 72 6E 6C : 2E 70 64 62-00 00 00 00 | ntoskrnl.pdb...
通常,子節總是以一個 CV_HEADER 結構開始,該結構中包 含 CodeView 的版本標識。這一次,該版本標識是 NB10MSDN(Microsoft 2000a)沒能告 訴我們有關這個特殊版本的更多信息: “ NB10 ,可執行文件的這一標識表示,其調試信息保存在獨立的 PDB文件中。相應的格式還有NB09或NB11。”( MSDN Library—April 2000\Specifications\Technologies and Languages\Visual C++ 5.0 Symbolic Debug Information Specification\Debug Information Format )
我並不知道 NB11 格式的內部細節,不過 PDB 格式和前面討論的 NB09 格式一樣幾乎 什么也沒有。第一句話很明確的說明了為什么 NB10 數據塊是如此的小。所有相關的信息都 被移到了獨立的文件中了,因此這個 CodeView 子節的主要作用就是提供指向實際數據的鏈 接。如示例 1-8 所暗示的,在 ntoskrnl.pdb 文件中一定可以找到實際的符號信息。
CV_HEADER 結構是自解釋的。其后的兩個成員的偏移量分別為:0x8 和 0xC,它們的 名字分別為:dSignature 和 dAge,在.dbg 和.pdb 文件鏈接的過程中它們將扮演重要角色。 dSignature 是一個 32 位的 UNIX 風格的時間戳,它保存了調試信息構建的日期和時間(自 01-01-1970 以來逝去的秒數)。w2k_img.dll 提供了兩個函數:imgTimeUnpack()和 imgTimePack()用來將 dSignature 和 Windows 風格的時間格式進行相互的轉化。我還不是非 常清楚 dAge 成員的確切含義。目前知道的是:dAge 成員的初始值為 1,每次修改 PDB 數 據后其值就會增一。dSignature 和 dAge 共同構成一個 64 位的 ID,調試器可以使用它來驗 證給定的 PDB 文件是否與它引用的.dbg 文件相匹配。PDB 文件在它的一個數據流中包含着 兩個值的一個副本,因此調試器可以拒絕處理不相匹配的.dbg/.pdb 文件。
無論你何時遇到格式未知的數據結構,你應該做的第一件事就是使用十六進制 Dump 瀏覽器察看這些結構。本書附帶的w2k_dump.exe可很好的完成這一工作。通過檢查Windows 2000 PDB 文件,如 ntoskrnl.pdb 或 ntfs.pdb,你會發現這些文件擁有如下一些共同特性:
- 這些文件似乎都被划分為多個大小固定的塊,一般情況下,每個塊的大小為 0x400 字節。
- 某些塊包含一長串 1,但偶而會被一小段連續的 0 打斷。
- 文件中的信息並不必須是連續的。有時,數據會在塊的邊界處突然結束,但又會在 文件的其它地方繼續開始。
- 有些數據塊會在文件中反復出現。
CodeView 的 NB10 子節 typedef struct _CV_NB10 // PDB reference { CV_HEADER Header; DWORD dSignature; // seconds since 01-01-1970 DWORD dAge; // 1++ BYTE abPdbName[]; // zero-terminated } CV_NB10, *PCV_NB10, **PPCV_NB10; #define CV_NB10_ sizeof(CV_NB10)
終弄清這些復合文件的典型特點花費了我不少時間。復合文件是將一個小型文件系統 打包到一個單一文件中。“文件系統”這一修飾詞可很好的解釋上面得到的觀察結果:
- 一個文件系統會將磁盤細化為大小固定的扇區,一組扇區又構成一個文件(此文件 件大小可變)。由扇區構成的文件可位於磁盤的任何位置上,並不要求必須是連續 的。文件/扇區的對應關系定義在文件目錄中。
- 一個復合文件將一個原始磁盤文件細化為大小固定的頁,一組頁構成一個流 (stream),並且流的大小可變。由頁構成的復合文件可位於原始文件中的任何位 置,這些頁並不必須是連續的。流和頁的對應關系定義在流目錄中。
很顯然,文件系統中的格式和復合文件格式差不多是一一對應的,只需簡單的將“扇區” 替換為“頁”,將“文件”替換為“流(Stream)”。對照文件系統可以很好的解釋為什么 PDB 文件是按大小固定的塊組織起來的,同時還解釋了為什么這些塊並不一定都是連續的。不過, 一頁中幾乎都是二進制 1 的塊又代表什么呢?實際上,這種類型的數據在文件系統中是很常 見的。為了跟蹤磁盤上已用和還未使用的扇區,很多文件系統都維護了一個二進制位的分配 數組,數組中的每個二進制位對應文件系統中的一個扇區(或一簇扇區)。如果一個扇區未 使用,其對應的二進制位就將被設置為 1。當文件系統為文件分配空間時,它就會掃描這個 分配位數組,以找出未使用的扇區。在將扇區加入到文件中后,文件系統就將對應得分配位 設為 0。復合文件的頁和流也采用了相似的處理方式。一長串的二進制 1 代表還未使用的頁, 二進制 0 表示對應的頁已分配給某個流。
現在唯一的問題就是為什么有些數據塊會在 PDB 文件中反復出現。同樣的事情也出現 在磁盤的扇區上。當文件系統中的一個文件被多次重寫時,每個寫操作可能會使用不同的扇 區來存放數據。因此,磁盤上某些空扇區中可能會包含舊數據的副本。這在文件系統中不算 是什么問題。如果扇區在分配數組中標識為未使用,那么該扇區上有什么數據就無所謂了。 這樣的扇區很快就會在另一個文件中被使用,其原有內容將被新的數據覆蓋掉。對應文件系 統的這一特性,我們再來看復合文件,這意味着我們觀察到的那些重復的頁應該是修改留下 的副本。可以安全地忽略它們;我們唯一需要關心的就是那些在流目錄(stream directory) 中被引用到的頁。
現在已經介紹完了PDB文件的基本結構,接下來我們將檢查構成PDB文件的那些基本的 數據塊。列表1-23給出了PDB頭部的布局。在PDB_HEADER的開始位置有一個文件字符串 給出了當前PDB的版本標識。該標識字符串以EOF字符(ASCII碼為0x1A)結束。在其后還 有一個附加的數字:0x0000474A,如果將該數字解釋為字符串的話,則為:”JG\0\0”。或許 這代表PDB格式的初設計者吧。嵌入的EOF字符有一個很好的作用:如果普通用戶在控制 台窗口中使用type ntoskrnl.pdb,那么將不會顯示其后的數據,顯示出來的信息只是:Microsoft C/C + + program database 2.00。Windows 2000所有的符號文件都是PDB 2.00 版。顯然,曾經存在過PDB 1.00格式,而且其結構似乎與現在的有很大不同。
#define PDB_SIGNATURE_200 \ "Microsoft C/C++ program database 2.00\r\n\x1AJG\0" #define PDB_SIGNATURE_TEXT 40 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef struct _PDB_SIGNATURE
{
BYTE abSignature [PDB_SIGNATURE_TEXT+4]; // PDB_SIGNATURE_nnn
} PDB_SIGNATURE, *PPDB_SIGNATURE, **PPPDB_SIGNATURE; #define PDB_SIGNATURE_ sizeof (PDB_SIGNATURE) // ----------------------------------------------------------------- #define PDB_STREAM_FREE -1 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef struct _PDB_STREAM
{
DWORD dStreamSize; // in bytes, -1 = free stream
PWORD pwStreamPages; // array of page numbers
} PDB_STREAM, *PPDB_STREAM, **PPPDB_STREAM; #define PDB_STREAM_ sizeof (PDB_STREAM) // ----------------------------------------------------------------- #define PDB_STREAM_MASK 0x0000FFFF
#define PDB_STREAM_MAX (PDB_STREAM_MASK+1) #define PDB_STREAM_DIRECTORY 0
#define PDB_STREAM_PDB 1
#define PDB_STREAM_TPI 2
#define PDB_STREAM_DBI 3
#define PDB_STREAM_PUBSYM 7 typedef struct _PDB_ROOT
{
WORD wCount; // < PDB_STREAM_MAX
WORD wReserved; // 0
PDB_STREAM aStreams []; // stream #0 reserved for stream table
} PDB_ROOT, *PPDB_ROOT, **PPPDB_ROOT; #define PDB_ROOT_ sizeof (PDB_ROOT) #define PDB_PAGES(_r) \
((PWORD) ((PBYTE) (_r) \
+ PDB_ROOT_ \
+ ((DWORD) (_r)->wCount * PDB_STREAM_))) // ----------------------------------------------------------------- #define PDB_PAGE_SIZE_1K 0x0400 // bytes per page
#define PDB_PAGE_SIZE_2K 0x0800
#define PDB_PAGE_SIZE_4K 0x1000 #define PDB_PAGE_SHIFT_1K 10 // log2 (PDB_PAGE_SIZE_*)
#define PDB_PAGE_SHIFT_2K 11
#define PDB_PAGE_SHIFT_4K 12 #define PDB_PAGE_COUNT_1K 0xFFFF // page number < PDB_PAGE_COUNT_*
#define PDB_PAGE_COUNT_2K 0xFFFF
#define PDB_PAGE_COUNT_4K 0x7FFF // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef struct _PDB_HEADER
{
PDB_SIGNATURE Signature; // PDB_SIGNATURE_200
DWORD dPageSize; // 0x0400, 0x0800, 0x1000
WORD wStartPage; // 0x0009, 0x0005, 0x0002
WORD wFilePages; // file size / dPageSize
PDB_STREAM RootStream; // stream directory
WORD awRootPages []; // pages containing PDB_ROOT
} PDB_HEADER, *PPDB_HEADER, **PPPDB_HEADER; #define PDB_HEADER_ sizeof (PDB_HEADER)
在標識字符串之后偏移量為 0x2C 處有一個名為 dPageSize 的 DWORD 類型的值,它代 表的是復合文件中每個頁所占的字節數。合法的值可以是:0x0400(1KB)、0x800(2KB) 和 0x1000(4KB)。wFilePages 成員記錄了 PDB 文件使用的頁的總數。將 wFilePages 與 dPageSize 相乘即可得到該 PDB 文件的大小。wStartPage 是一個從零開始的頁碼,它指向第 一個數據頁。該頁的字節偏移量可由該頁的頁碼乘以每頁的大小得到。通常的值為:頁號為 9 的 1KB 頁(字節偏移量為 0x2400),頁號為 5 的 2KB 頁(字節偏移量為 0x2800)或者頁 號為 2 的 4KB 頁(字節偏移量為 0x2000)。在 PDB_HEADER 和第一個數據頁之間的空間 保留給分配位數組,並總是從第二個頁開始。這意味着,如果頁大小為 1 或 2KB,則 PDB 文件使用 0x10000(64K)個分配位,每位對應 0x2000 字節(8KB)的頁,如果頁大小為 4KB,則使用 0x8000(32K)個分配位,每位對應 0x1000 字節(4KB)的頁。以此類推, 這意味着,在頁大小為 1KB 的情況下,PDB 文件可容納 64MB 數據,在頁大小為 2KB 或 4KB 的情況下,PDB 文件可容納 128MB 數據。
PDB_HEADER 后的 RootStream 和 wRootPages[]成員記錄了 PDB 文件中流目錄的位 置。就像前面提到的,PDB 文件是由一組長度可變的流構成的,這些流中才包含有實際的 數據。流的位置及其內容是由一個單一的流目錄管理的。流目錄自身也存儲在一個流中。我 稱這個特殊的流為“Root Stream”。Root Stream 中保存着流目錄(該流目錄可能位於 PDB 文件的任何位置)。PDB_HEADER 的 Rootstream 和 wRootPages[]成員提供了 Root Stream 的 位置和大小。PDB_STREAM 子結構的 dStreamSize 成員給出了流目錄占用的頁的數目,這 些頁的首地址保存在 wRootPages[]數組中,這些頁包含實際的數據。
現在讓我們用一個小例子來說明這一點。示例 1-9 給出了 ntoskrnl.pdb 的 PDB_HEADER 的十六進制 Dump 的部分內容。這里引用到的值由下划線標識出來。顯然,這個 PDB 文件 使用的頁的大小為 0x400 字節(1KB),一共使用了 0x02D1(721)個頁,這樣該文件的大 小則為 0xB4400(十進制 738,304)。使用 dir 命令可驗證這個大小是正確的。Root Stream 的 大小為 0x5B0 字節(1456 字節),由於每個頁的大小為 0x400 字節(1KB),則意味着 wRootPages[]數組中包含兩項,分別位於偏移量為 0x3C 和 0x3E 處。數組中的兩項內容都是 頁碼,需要將此頁碼與頁大小相乘才能得到對應的字節偏移量。此處,該字節偏移量為: 0xB2000 和 0xB2800。
上面后一行給出的計算結果是 ntoskrnl.pdb 文件的流目錄所占用的兩組文件頁的首地 址,其范圍分別為:0xB2000----0xB23FF 和 0xB2800----0xB29AF。示例 1-10 給出了這些范 圍的部分內容。 

流目錄由兩個部分構成:一個 PDB_ROOT 結構的文件頭部分,該結構定義在列表 1-24 中,另一部分是由 16 位頁碼構成的數組。PDB_ROOT 結構中的 wCount 成員記錄了保存在 PDB 文件中的流的數目。aStream[]數組包含多個 PDB_STREAM 結構(參見列表 1-23),每 個 PDB_STREAM 結構代表一個流,緊隨 aStream[]數組之后在就是頁碼數組。在示例 1-10 中,流的個數為 8,對應的偏移量為 0xB2000,該位置已用下划線標識出。隨后的 8 個 PDB_STREAM 結構分別給出了這 8 個流的大小:0x5B0、0x3A、0x38、0x402A9、0x0、0x4004、 0x19EB4 和 0x4DF3C。這些值也都以下划線標識出。在 1KB 頁模式下,流的大小為:0x2、 0x1、0x1、0x101、0x0、0x11、0x68 和 0x138,這樣可計算出這些流總共占用了 0x2B6 個 頁。在 PDB_STREAM 數組之后,第一個以下划線標識出來的值是頁碼列表中的第一個頁碼。 這里每個頁碼占用 2 個字節,這里需要考慮的是,頁目錄被屬於其他部分的一個頁截斷了, 故頁目錄隨后的偏移量為應為:0xB2044+0x400+(0x2B6*2)=0xB29B0,示例 1-10 很好的展 示了這一點。
PDB 的流目錄結構
#define PDB_STREAM_DIRECTORY 0
#define PDB_STREAM_PDB 1
#define PDB_STREAM_TPI 2
#define PDB_STREAM_DBI 3
#define PDB_STREAM_PUBSYM 7 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef struct _PDB_ROOT
{
WORD wCount; // < PDB_STREAM_MAX
WORD wReserved; // 0
PDB_STREAM aStreams []; // stream #0 reserved for stream table
} PDB_ROOT, *PPDB_ROOT, **PPPDB_ROOT; #define PDB_ROOT_ sizeof (PDB_ROOT) #define PDB_PAGES(_r) \
((PWORD) ((PBYTE) (_r) \
+ PDB_ROOT_ \
+ ((DWORD) (_r)->wCount * PDB_STREAM_)))
要找到給定的流所對應的頁碼需要一定的技巧,因為頁目錄除了流的大小之外,沒有提 供任何信息。如果你對 3 號流感興趣,那么你必須計算流 1 和流 2 所占用的頁的數目,以獲 取 3 號流在頁碼數組中的起始索引。一旦定位了指定流的頁碼列表,讀取流中的數據就很簡 單了。只需要遍歷頁碼列表,將列表中的每個頁碼和每頁的大小相乘,就可獲得此頁碼對應 頁的文件偏移量,然后從該偏移量處開始讀取頁的內容,反復如此,直到到達流的結束處, 就可讀取整個流的內容了。猛地一看,解析一個 PDB 文件似乎非常費勁。但從另一個角度 看卻十分簡單-----因為這要比解析一個.dbg 文件簡單的多。PDB 格式的這種清晰的隨機訪問 機制,將讀取一個流的任務簡化為讀取連續的大小固定的頁。這種優雅的數據訪問機制讓我 很是吃驚。
當更新一個已存在的 PDB 文件時,PDB 格式的優勢就非常明顯了。將使用連續的結構 體的數據插入到一個文件中,意味着將移動大量的原有數據。PDB 文件從文件系統借鑒來 的隨機訪問架構允許以小的開銷完成刪除或插入數據的操作,就像文件系統中的文件可以 很容易的修改一樣。當一個流在增大時,只需改動流目錄或則收縮頁的邊界。這種非常重要 的特性大為提高了 PDB 文件更新的靈活性。微軟在基本知識庫中正式提供這樣一片文章: “信息:PDB 和 DBG 文件-----它們是什么以及它們是如何工作的”: “ .PDB擴展了“ Program database ”架構。此種文件用來存放調式信息,這種格式隨 Visual C++ 1.0 一起引入。在將來, .PDB 文件還將包含其它的項目狀態信息。格式改變的一 個重要動機是為了允許程序調試版的增量鏈接,第一次改變隨 Visual C++ 2.0 引入。” ( Microsoft Corporation 2000e )
現在 PDB 文件的內部結構已經很清晰了,下一個問題是如何識別這些流的具體內容。 在檢查完 PDB 文件的各個方面后,我得出這樣一個結論:每一種流都用於特定的目的。第 一個流似乎總是包含一個流目錄,第二個流包含用於驗證該 PDB 文件是否與其關聯的.dbg 文件相匹配的信息。例如,該流中包含的 dSignature 和 dAge 成員應該和 NB10 CodeView 節 中的對應成員一致。
