文章目錄
文/玄魂
.PDB檔案,全稱為“程式資料庫”檔案。我們使用它(更確切的說是看到它被應用)大多數情境是調試應用程式。目前我們對.PDB檔案的普遍認知是它儲存了被編譯檔案的調試資訊,作為符號檔案存在。那麼,它具體包含哪些內容呢?在調試過程中是怎樣發揮作用的呢?我們有沒有辦法去操作這個檔案呢?
1. PDB檔案內容
.PDB檔案的內部格式,微軟並沒有公開,現在仍然是一個秘密,但是它提供了相關的API用於調試器來從中擷取資訊。
一個非託管C++程式的PDB檔案包含如下資訊:
- l Public, private,和static函數地址
- l 全域變數的名稱和地址
- l 參數和局部變數的名稱及它們在棧中的位移量
- l 類型定義,包括class, structure,和 data definitions
- l FPO(Frame Pointer Omission,幀指標省略)資料
- l 源檔案名稱和行號
說明:
從XP SP2起就不再啟用FPO。
對於.NET PDB檔案,只包含上面說到的兩種資訊:
.NET PDB檔案包含如此少的資訊,原因在於其他資訊我們可以從中繼資料中擷取,所以也就沒有必要重複儲存了。
2. PDB檔案匹配
當你講一個模組載入到當前進程的地址空間中,調試器根據兩個資訊去尋找找匹配的PDB檔案。第一個資訊很明了,根據模組的名稱。比如你載入的模組為“Test.DLL”,那麼調試器將會尋找Test.PDB檔案。但是但是通過名稱,是無法判斷模組和PDB檔案是否是完整匹配的,調試器通過第二個資訊來判斷——一個GUID值。這個GUID值同時存在於模組檔案和PDB檔案中,如果GUID值不匹配,那麼我們是無法在原始碼層級來偵錯工具的。
這個GUID值是編譯器和連結器放到檔案中的。目前我們沒有辦法來改變這個值,但是我們查看在編譯產生的檔案中的GUID值。這裡我們使用的工具是DUMPBIN,使用它我們可以列舉所有PE檔案的資訊。如果您安裝了VS2008或者VS2010的話,可以直接從VS命令列啟動該工具,1所示。
圖1 啟動DUMPBIN
DUMPBIN工具的命令列選項,您可以參閱相關的文檔,這裡我們使用它的/headers選項,看看會輸出什麼樣的結果。
在使用dumpbin之前,我們首先建立一個控制台程式,內容如代碼清單1所示。
代碼清單1 樣本程式
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace dumptest
{
class Program
{
static void Main(string[] args)
{
}
}
}
編譯清單1的代碼,obj目錄中的檔案2所示。
圖2 obj目錄中的檔案
Bin目錄下的檔案內容3所示。
圖3 Bin目錄下的檔案
準備工作做完之後,我們現在從命令列切換到bin目錄下,執行“dumpbin /headers dumptest.exe”命令,結果如代碼清單2所示。
代碼清單2 dump查看headers結果
E:\test\c#\dumptest\dumptest\obj\x86\Debug>dumpbin /headers dumptest.exe
Microsoft (R) COFF/PE Dumper Version 10.00.30319.01
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file dumptest.exe
PE signature found
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
14C machine (x86)
3 number of sections
4E92A178 time date stamp Mon Oct 10 15:40:40 2011
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
102 characteristics
Executable
32 bit word machine
OPTIONAL HEADER VALUES
10B magic # (PE32)
8.00 linker version
800 size of code
800 size of initialized data
0 size of uninitialized data
272E entry point (0040272E)
2000 base of code
4000 base of data
400000 image base (00400000 to 00407FFF)
2000 section alignment
200 file alignment
4.00 operating system version
0.00 image version
4.00 subsystem version
0 Win32 version
8000 size of image
200 size of headers
0 checksum
3 subsystem (Windows CUI)
8540 DLL characteristics
Dynamic base
NX compatible
No structured exception handler
Terminal Server Aware
100000 size of stack reserve
1000 size of stack commit
100000 size of heap reserve
1000 size of heap commit
0 loader flags
10 number of directories
0 [0] RVA [size] of Export Directory
26D4 [57] RVA [size] of Import Directory
4000 [588] RVA [size] of Resource Directory
0 [0] RVA [size] of Exception Directory
0 [0] RVA [size] of Certificates Directory
6000 [C] RVA [size] of Base Relocation Directory
2668 [1C] RVA [size] of Debug Directory
0 [0] RVA [size] of Architecture Directory
0 [0] RVA [size] of Global Pointer Directory
0 [0] RVA [size] of Thread Storage Directory
0 [0] RVA [size] of Load Configuration Directory
0 [0] RVA [size] of Bound Import Directory
2000 [8] RVA [size] of Import Address Table Directory
0 [0] RVA [size] of Delay Import Directory
2008 [48] RVA [size] of COM Descriptor Directory
0 [0] RVA [size] of Reserved Directory
SECTION HEADER #1
.text name
734 virtual size
2000 virtual address (00402000 to 00402733)
800 size of raw data
200 file pointer to raw data (00000200 to 000009FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
Execute Read
Debug Directories
Time Type Size RVA Pointer
-------- ------ -------- -------- --------
4E92A178 cv 50 00002684 884 Format: RSDS, {99F34C5E-5BC3-4043-AE11-D85F7990AF00}, 1, E:\test\c#\dumptest\dumptest\obj\x86\Debug\dumptest.pdb
SECTION HEADER #2
.rsrc name
588 virtual size
4000 virtual address (00404000 to 00404587)
600 size of raw data
A00 file pointer to raw data (00000A00 to 00000FFF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
Read Only
SECTION HEADER #3
.reloc name
C virtual size
6000 virtual address (00406000 to 0040600B)
200 size of raw data
1000 file pointer to raw data (00001000 to 000011FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
42000040 flags
Initialized Data
Discardable
Read Only
Summary
2000 .reloc
2000 .rsrc
2000 .text
現在我們將目光集中在代碼清單2中斜體加粗的部分,這裡我們可以找到調試路徑的資訊,一個GUID值(99F34C5E-5BC3-4043-AE11-D85F7990AF00)和一個路徑(E:\test\c#\dumptest\dumptest\obj\x86\Debug\dumptest.pdb)。現在我們已經清楚了調試器如何判斷PDB檔案是否匹配,下面我們再來看調試器是如何尋找PDB檔案的。
3. PDB檔案尋路
如果我們觀察VS啟動調試載入模組和符號檔案的過程,會發現它通常會從可執行檔或者DLL檔案的相同目錄中載入符號檔案。這正是調試器尋找PDB檔案的第一選擇。
如果在模組檔案的相同目錄下找不到匹配的PDB檔案,會發生什麼呢?我們在前文知道編譯器在PE檔案中寫入程式碼了一個路徑(比如:E:\test\c#\dumptest\dumptest\obj\x86\Debug\dumptest.pdb),這個路徑就是調試器的第二個選擇。對於對外發布的應用,很可能這兩個路徑下都找不到PDB檔案。此時調試器會在本地的符號伺服器緩衝路徑下尋找PDB檔案。如果本地的符號伺服器緩衝路徑下仍然找不到,它會調試器本身配置的符號伺服器中尋找符號檔案。圖4是VS2010配置符號伺服器和本地符號緩衝路徑的介面。
圖4 VS2010配置符號儲存
4. PDB與GAC
上面講到的調試器尋找PDB檔案的方式在大多數情況下都工作的很好,當我們遇到必須要講編製之後的檔案安裝的GAC中的時候,情況開始變得有意思起來。當我們在本地編譯並偵錯工具集的時候,即使程式集被安裝到GAC中,調試器仍然能在編譯目錄下找到PDB檔案,但是如果我們已經將Private Build的應用部署到其他機器上的時候,此時還想在被部署的機器上調試安裝到GAC上的程式集,將會是一件很麻煩的事情。我們有兩種方案來解決這個問題。
注:
Private Build與Public Build的區別
private build, 用來表示在開發人員自己機器上產生的build;public build,表示在公用的build機器上產生的build。對於public build,需要symbol server儲存所有的PDB,然後當使用者報告錯誤的時候,debugger才可以自動地找到binay相應的PDB檔案, visual studio 和 windbg都知道如何訪問symbol server。在將PDB和binay儲存到symbol server前,還需要對PDB運行進行source indexing, source indexing的作用是將PDB和source關聯起來。
第一種方案是我們在GAC的目錄中找到被安裝的程式集,然後將PDB檔案拷貝到該目錄下。通常我們安裝到GAC中的程式集會存在於類似這樣的路徑中:C:Windows\assembly\GAC_MSIL\Example\1.0.0.0__682bc775ff82796a,該樣本目錄中“Example”代表程式集的名稱,“1.0.0.0”代表版本號碼,“682bc775ff82796a”代表程式集的Public Token。當你找到確切的目錄,將PDB檔案放到該目錄下,調試器就可以載入符號檔案了。
這裡還有一種更好的方案,就是設定一個名為“DEVPATH”的系統內容變數。該環境變數設定一個磁碟目錄作為其值,該目錄將作為GAC的輔助目錄存在,在GAC中尋找程式集同樣會搜尋該目錄。但是在這樣的目錄中程式集並不會執行版本檢查,這是需要我們注意的地方。
使用DEVPATH,我們首先要選定一個目錄,然後確保應用程式對它持有讀、寫入權限。然後建立DEVPATH系統內容變數。當然這隻是準備工作,我們還要告知.NET運行時,應用程式啟用DEVPATH作為GAC的擴充目錄。所以接下來我們在設定檔(APP.CONFIG,WEB.CONFIG,MACHINE.CONFIG)中添加如下配置:
<configuration>
<runtime>
<developmentMode developerInstallation="true"/>
</runtime>
</configuration>
在你開啟了development模式後,如果DEVPATH沒有定義或路徑不存在的話會導致程式啟動時異常"Invalid value for registry"。而且如果在machine.config中開啟DEVPATH的使用會影響其他的所有的程式,所以要謹慎使用machine.config。
5. PDB與源檔案
現在我們再來討論一個開發人員經常問到的問題:源檔案的資訊是如何在PDB檔案中儲存的?對於Public Build,PDB檔案儲存體的是如何利用版本控制工具從代碼緩衝擷取源碼代碼的命令。對於Private Build,很顯然,符號檔案儲存體的是原始碼的完整路徑。
理想狀態下,對於Public Build而言原始碼索引和符號被緩衝到符號伺服器的操作都會自動執行,我們無需考慮原始碼和符號檔案在哪儲存的問題。事實上,很多Team Dev並沒有公用的符號伺服器或者原始碼索引服務。對於一個小型項目而言,每個開發人員都有足夠的磁碟空間來儲存原始碼和符號檔案,也許不會過多的考慮這個問題。但是也許會有這樣的情境:將一個Private Build項目轉移到另一台機器上有30M的源碼需要放到C盤,但是此時C盤只有20M的剩餘空間該怎麼辦?我們能修改PDB檔案中源碼的路徑嗎?
我們前面提到沒有辦法修改PDB檔案,但是這裡有一個取巧的方法可以嘗試,所謂山不向我走來,我向山走去。這裡有一個工具恰好可以派上用場,它就是subst.exe,它是Windows內建的命令列工具,可以從cmd視窗啟動。
說明:
subst用於路徑替換 ,將路徑與磁碟機代號關聯,就是把一個目錄當作一個磁碟機來看,不過不能格式化。運用一定技巧,subst命令還可以實現隱藏磁碟機、特殊軟體的安裝、類比光碟片自動運行等功能。
用法格式
一、subst [盤符] [路徑] 將指定的路徑替代盤符,該路徑將作為磁碟機使用
二、subst /d 解除替代
三、不加任何參數鍵入 SUBST,可以顯示當前虛擬磁碟機的清單。
[例子]
C:\DOS>subst a: c:\temp 將c:\temp虛擬化成a盤
C:\>subst a: /d? 解除替代
想到解決上面問題的方法了嗎?我們只需將本機源碼的路徑虛擬化成一個磁碟,例如M,之後無論你將代碼部署到任何機器上,只需將被部署的路徑虛擬成M就可以了,就不會出現符號檔案和目標代碼不匹配的情況了。
關於PDB檔案的資料,目前在互連網上還是鳳毛麟角,我這裡只是拋磚引玉,歡迎大家共同交流。