關於 PDB 文件你需要知道什麼?
引言
大多數人知道 PDB 文件是用來幫助我們 debug 的,但也僅此而已。
本文主要介紹當你遇到 PDB 文件時(windows 開發中),你必須要知道的內容。
重要的事情說三遍
PDB 文件和源程式碼一樣重要!!!
PDB 文件和源程式碼一樣重要!!!
PDB 文件和源程式碼一樣重要!!!
開始之前
首先定義兩個概念:
- 本地編譯:在你本機開發環境中的編譯。
- 官方編譯:在編譯伺服器上的編譯。
這兩種編譯的區分很重要,因為調試本地編譯往往很簡單,但是問題往往出現在官方編譯中。
官方編譯至少需要有一個地方(Symbol Server)來存放編譯出來的二進位及 PDB 文件。這樣當某個版本發現任何問題,我們可以獲取到對應的 PDB 文件進行調試。
沒有匹配的 PDB 文件,調試器幾乎不可能完成調試任務,或者你將付出高昂的代價才能解決問題。
更多關於 Symbol Server 的內容,參考 //docs.microsoft.com/en-us/archive/msdn-magazine/2002/june/bugslayer-symbols-and-crash-dumps
Visual Studio 和 WinDBG 知道如何訪問 Symbol Server 並且如果二進位文件來自官方編譯,那麼調試器能夠自動載入匹配的 PDF 文件。
在將 PDB 文件放在 Symbol Server 上之前,還需要做一件事:在官方編譯出來的 PDB 文件上,通過 Source Server 工具,進行源文件索引(source indexing)。索引過程會嵌入版本控制命令用於拉取當前版本編譯的源文件。這樣,當你調試當前版本時,你就不用擔心找不到版本的源文件。
更多關於 Source Server 的內容,參考 //docs.microsoft.com/en-us/archive/msdn-magazine/2006/august/source-server-helps-you-kill-bugs-dead-in-visual-studio-2005
什麼是 PDB 文件?
現在,我們可以開始介紹什麼是 PDB 文件,以及調試器是如何查找 PDB 文件了。
實際的 PDB 文件格式是加密的,但是微軟提供了 API,為調試器提供 PDB 文件的數據。
非託管的 C++ PDB 文件包含了以下資訊:
- 公有函數,私有函數和靜態函數的地址
- 全局變數的名稱和地址
- 參數和局部變數的名稱以及棧上的偏移量
- 類,結構體以及數據定義的類型資訊
- FPO(Frame Pointer Omission) 數據
- 源文件的文件名及行資訊。
而 .NET 的 PDB 文件只包含了兩項內容:
- 源文件的文件名及行資訊
- 局部變數的名稱
其他所有資訊已經存放在 .NET 元數據中,所以沒有必要再 PDB 文件中冗餘。
PDB 文件的載入
當模組被載入到進程的地址空間後,調試器會使用兩種資訊找到匹配的 PDB 文件。首先,當然是文件名。如果你載入 ZZZ.DLL,那麼調試器就會查找 ZZZ.PDB。
更重要的是,調試器如何知道這就是匹配的 PDB 文件?這是通過比對內嵌於 PDB 文件和二進位文件中的 GUID 來確認的。
負責將 GUID 嵌入二進位和 PDB 文件的是編譯器(.NET)或者鏈接器(C++)。想想,歷史編譯的版本如果沒有保存 PDB 文件,你還能調試嗎?答案是否定的,哪怕你沒有修改源文件!你可能會想是否可以修改 PDB 文件的 GUID?很遺憾,答案也是否定的。
你可以查看二進位文件中的 GUID。使用 Visual Studio -> DUMPBIN 的命令行工具,你可以列出所有的 PE(Portable Executable) 文件內容。可以在 Visual Studio 的命令行工具中調用 DUMPBIN。
更多關於 DUMPBIN 的內容參考://docs.microsoft.com/en-us/archive/msdn-magazine/2002/february/inside-windows-win32-portable-executable-file-format-in-detail 和 //docs.microsoft.com/en-us/archive/msdn-magazine/2002/march/inside-windows-an-in-depth-look-into-the-win32-portable-executable-file-format-part-2
DUMPBIN 有很多命令行指令,其中顯示 GUID 的指令是 /HEADERS。在輸出內容中,對我們來說重要的是 Debug Directories 部分的內容:
Debug Directories
Time Type Size RVA Pointer
-------- ------- -------- -------- --------
6045C20E cv 60 00541AC8 5408C8 Format: RSDS, {DC80D058-127B-4379-B859-3F9F6978A4DB}, 1, C:ZZZ.pdb
知道了調試器如何確定匹配的 PDB 文件,下一步我們討論調試器從哪裡查找 PDB 文件。首先,調試器會在載入二進位文件的目錄查找對應的 PDB 文件,如果沒有找到,那麼就查找PE 文件中 Debug Directories 內容里硬編碼的 PDB 文件路徑,在上面的輸出示例中是 “C:ZZZ.pdb”(.NET 應用編譯工具 MSBUILD會將 PDB 文件編譯到 OBJ<Debug/Relase/…> 目錄下,如果編譯成功,再拷貝到 DEBUG 或者 RELEASE 目錄)。如果在上述兩個位置都沒有找到,但是建立了 Symbol Server,那麼調試器會在 Symbol Server 的快取目錄里繼續查找。這種查找順序也保證了本地編譯和官方編譯不會有衝突。
在 Visual Studio 中調試的時候,你可以在窗口 Modules 中的列 Symbol File 里看到 PDB 文件的位置。
對大多數應用來講這種載入方式都沒有問題。但是對於需要將程式集放入 GAC(Global Assembly Cache)的 .NET 應用,PDB 的載入就會變得有趣了。對於本地編譯,調試器會在編譯目錄找到 PDB 文件,所以沒什麼問題。問題來源於當你想要在其他機器上調試本地編譯版本。
在其他機器上調式,很多人會用 Gacutil.exe 將程式集放入 GAC,然後打開命令行在 “C:WINDOWSASSEMBLY” 下查找程式集的物理位置。但是基於 Any CPU 編譯的程式集實際上會放入類似 “C:WindowsassemblyGAC_MSILExample1.0.0.0__682bc775ff82796a” 的路徑。
上述路徑中,Example 是程式集名稱,1.0.0.0 是版本號,682bc775ff82796a 是公有秘鑰令牌值(public key token value)。當你推斷出這個路徑後,你可以將 PDB 文件拷入這個目錄然後調試器會載入它。
PDB 文件的內容
對於官方編譯,因為有源文件索引工具,所以 PDB 文件中會存儲版本控制命令,用於將源文件放入你配置的源文件快取池。對於本地編譯,PDB 文件中存儲的是二進位文件對應的源文件的完整路徑。換句話說,如果你使用 C:FOO 中的源文件 MYCODE.CPP,那麼 PDB 文件中存儲的就是 C:FOOMYCODE.CPP。
理論上,所有的官方編譯會自動立馬進行源文件索引,並將內容存儲於 Symbol Server,以至於你都不用考慮源文件在哪。然而,有些開發團隊在測試及其他環節中會考量編譯結果是否滿足使用的要求,在此之前,不會對 PDB 文件進行源文件索引。如果你確實需要調試未索引的版本,最好將源程式碼下載到本地時保證和編譯伺服器相同的磁碟和目錄,否則,你可能會在調試時遇到麻煩。儘管 Visual Studio 調試器和 WinDBG 有配置源文件搜索路徑的選項,但要配置正確並不容易。
引用
//www.wintellect.com/pdb-files-what-every-developer-must-know/