使用VC++生成調試信息
ZhangTao,[email protected], 譯自 “Generating debug information with Visual C++”,Oleg Starodumov
引子
當我們使用調試器來調試程序時,我們希望能夠單步調試到源代碼中,在代碼中設置斷點,觀察變量的值(包括用戶自定義的複雜類型的值)。但是可執行文件只含有原始的字節數據——機器指令和操作系統執行程序時所使用的頭信息和表信息。操作系統加載並運行可執行文件後,它根據不同的需求使用不同片段的內存(棧、堆)存放數據,其中的存放的依然是原始的字節數據。那麼,調試器如何知道當前CPU指令對應哪一行代碼?如何知道堆棧中的地址對應哪一個函數的局部變量?答案是“調試信息”,調試信息是高級編程語言和運行程序的原始字節數據之間的橋樑。名詞解釋
位置(location): 在不同的情況有不同的含義。對於函數而言,是函數首字節的地址;對於全局和靜態變量而言,是內存中變量的首字節;對局部變量和函數參數而言,通常是該變量的首字節相對於函數堆棧的預先定義的基址的偏移。另外,其他類型的位置也可能出現,如:寄存器、TLS slot(參見:http://www.blogcn.com/u2/38/94/silannyukun/blog/37069531.html)、元數據標記(metadata token, 參見http://naoku.net/blogs/framesniper/archive/2005/04/12/1910.aspx)。
FPO (frame pointer omission): 幀指針省略,FPO用來鏈接CodeView或PDB符號。它在編譯器沒有用EBP寄存器生成標準堆棧楨(a standard stack frame) 的地方幫助調試器查找函數的參數和本地變量。調試信息的類型
我們只討論在Intel X86平臺上的現有的由微軟提供的調試器。
信息的類型 |
描述 |
公共函數和變量 |
用於描述在多個的編譯單元(源代碼文件)中可見的函數和變量,調試信息保存每個函數和變量的位置(location)和名稱。 |
私有函數和變量 |
用於描述除公共函數和變量以外的所有函數和變量,包括靜態函數、靜態和局部變量、函數參數),調試信息保存每個函數和變量的位置、大小和名稱。 |
源文件和代碼行信息 |
用於將每一行代碼映射到可執行文件的某個位置上。當然,某些代碼行不能做映射,如註釋行,這樣的代碼行在調試信息中不做體現。 |
類型信息 |
用於存儲每一個函數和變量的類型信息。對於變量或函數參數,類型信息能夠告訴調試器它是整型還是字符串類型,或是用戶自定義的類型。對於函數,類型信息記載了參數的個數、調用轉換和返回值的類型。 |
FPO信息 |
對於做了FPO優化的函數,調試信息保存了一些數據來幫助調試器確定函數堆棧幀的大小,甚至在幀指針無效時也能工作。 如果沒有FPO信息,調試器無法正確顯示被優化的程序的調用堆棧。 |
編輯和繼續執行信息 |
用於幫助Visual Studio IDE在調試時實現編輯和繼續執行的功能 |
調試信息格式
現在來探索調試信息是如何存儲的。在過去的十年中,微軟開發工具使用了幾種不同的格式來包裝調試信息。這裏我們討論COFF、CodeView和應用的最廣泛的PDB(Program Database)格式。在討論每種格式時,我們從下列幾個特性着手:
- 哪些類型的調試信息可以通過該格式保存?
- 調試信息究竟保存在哪裏(在可執行文件中,還是單獨的一個文件)?
- 該格式是否有文檔說明?
COFF
COFF是這裏要涉及的所有格式中最古老的一種,它只能保存三種調試信息: 公共函數和變量,源文件和代碼行信息,FPO信息。COFF總是保存在可執行文件中,不能夠單獨保存在其他文件中。該格式的文檔說明參見:微軟可移植可執行和通用對象文件格式規範.
CodeView
CodeView是較COFF更新的而且更復雜的一種格式,它可以存儲除編輯和繼續執行信息外的所有類型的調試信息。CodeView通常保存在可執行文件中,它也可從可執行文件中導出到一個單獨的文件(.DGB文件)。CodeView文檔不全,其文檔可以在MSDN中的VC++5.0符號調試信息規範(Symbolic Debug Information Specification)中找到。
Program Database 程序數據庫
這是三種中最新的一種調試信息格式,可以存儲所有類型的調試信息(包括編輯和繼續執行信息),也支持增量編譯(其餘兩種格式不支持)。程序數據庫信息保存在一個單獨的.PDB文件中。遺憾的是,微軟沒有提供程序數據庫格式的文檔,只提供特殊的編程接口DbgHelp 和DIA來訪問它。目前,程序數據庫格式有兩個版本,第一版(PDB2.0)爲VC6.0所用,第二版(PDB 7.0)被Visual Studio.NET採用。PDB 7.0不能向上兼容,也就是說:VC6.0不能讀取PDB 7.0格式。
三種格式對比如下:
格式 |
是否有文檔 |
存儲 |
公共函數和變量 |
私有函數和變量 |
源文件和代碼行信息 |
類型信息 |
FPO 信息 |
編輯和繼續執行信息 |
COFF |
有 |
可執行文件中 |
+ |
- |
+ |
- |
+ |
- |
CodeView |
部分 |
可執行文件中 或.DBG文件中 |
+ |
+ |
+ |
+ |
+ |
- |
Program Database |
無 |
.PDB文件中 |
+ |
+ |
+ |
+ |
+ |
+ |
生成調試信息
構造(build)過程
一個典型的可執行文件的構造過程包含兩步:編譯和鏈接。首先,編譯器分析源文件,生成機器指令(保存在.obj對象文件中);然後鏈接器將所有可用的對象文件合併到最終的可執行文件。在對象文件之外,鏈接器也會用到庫文件(庫文件也是其他一些對象文件的彙集)。整個構造過程如下圖:
如果我們想要爲可執行文件生成調試信息,也得經歷兩步:首先,編譯器爲每一個源文件創建調試信息;然後,鏈接器合併由編譯器創建得調試信息,如下圖:
缺省狀態下,編譯器和鏈接器不會產生調試信息。因此我們必須通過編譯和鏈接選項來要求編譯器和鏈接器生成調試信息,我們也可以指定生成哪些類型得調試信息,使用什麼調試信息格式,將調試信息保存在什麼地方。
接下來,我討論具體得編譯器和鏈接器選項。
Visual C++ 6.0
編譯器 Compiler
有下列選項:
/Zd 生成COFF格式的調試信息,保存在對象文件中
/Z7 生成CodeView格式的調試信息,保存在對象文件中
/Zi 生成程序數據庫格式的調試信息,保存在.PDB文件中
/ZI 與 /Zi 基本一致, 唯一不同的是調試信息中包含編輯和繼續執行信息
缺省時,/Zi 和 /ZI 選項生成的PDB文件名爲VC60.PDB,也可以使用/Fd指定文件名。
選項 |
格式 |
存儲文件 |
內容 |
/Zd |
COFF |
.OBJ |
|
/Z7 |
CodeView |
.OBJ |
|
/Zi |
Program Database |
.PDB |
|
/ZI |
Program Database |
.PDB |
|
鏈接器Linker
下列選項可用:
/debug 告訴鏈接器生成調試信息,如果該選項不使用,則其他所有選項都無效
/debugtype 指定調試信息格式,可能的用法包括:
/debugtype:coff COFF格式。注意:該選項下,調試信息中不包含源文件和代碼行信息
/debugtype:cv CodeView或程序數據庫格式。究竟是哪一種格式,由/pdb決定
/debugtype:both 同時使用COFF格式和CodeView/程序數據庫格式
/pdb 決定是CodeView還是程序數據庫格式。/pdb:none 表示CodeView格式,/pdb:filename(如/pdb:myexe.pdb)表示使用程序數據庫格式,文件名爲myexe.pdb。在/debugtype:coff 選項下,/pdb 選項無效。
/pdbtype 該選項只在一個或多個對象文件或庫文件的調試信息也保存在一個單獨的PDB文件中。/pdbtype:sept 選項可以使得調試信息各自保存在各自的PDB文件中,這樣可以加快鏈接速度,不利的是調試信息分散,調試時需要多個PDB文件。相對的,/pdbtype:con 選項使得所有調試信息都保存在與可執行文件對應的最終的PDB文件中。
爲便於理解各個選項的配對使用,請見下表:
/debugtype |
/pdb |
格式 |
存儲 |
coff |
/pdb:none (無效) |
COFF |
在可執行文件中 |
coff |
/pdb:filename (無效) |
COFF |
在可執行文件中 |
cv |
/pdb:none |
CodeView |
在可執行文件中 |
cv |
/pdb:filename |
Program Database |
.PDB 文件 |
both |
/pdb:none |
COFF and CodeView |
在可執行文件中 |
both |
/pdb:filename |
COFF and Program Database |
COFF 信息在可執行文件中, 程序數據庫信息在 .PDB 文件中 |
Visual C++.NET (2002 and 2003)
編譯器 Compiler
下列選項可用:
/Z7 生成CodeView格式的調試信息,保存在對象文件中
/Zd, /Zi 和 /ZI都表示生成程序數據庫格式的調試信息,保存在.PDB文件中. 不同之處是調試信息的內容(見下表)。
缺省時,/Zd,/Zi 和 /ZI 選項生成的PDB文件名爲VC70.PDB或VC71.PDB,也可以使用/Fd指定文件名。
注意: VC++.NET 編譯器不支COFF。
選項 |
格式 |
存儲 |
內容 |
/Z7 |
CodeView |
.OBJ |
|
/Zd |
Program Database |
.PDB |
|
/Zi |
Program Database |
.PDB |
|
/ZI |
Program Database |
.PDB |
|
鏈接器Linker
下列選項可用:
/debug告訴鏈接器生成調試信息,如果該選項不使用,則其他所有選項都無效。調試信息的格式總是程序數據庫格式,保存在PDB文件中。缺省的,鏈接器使用可執行文件名生成PDB文件名。PDB文件名可包含所有調試信息的變量內容。
/pdb 指定PDB文件名.
/pdbstripped 允許鏈接器生成附加的PDB文件,該文件的內容限定於:
- 公共函數和變量
- FPO信息
注意: COFF 和 CodeView 格式不被 VC++.NET鏈接器支持。
靜態庫的調試信息
由於沒有連接過程,靜態庫的調試信息的生成比可執行文件要簡單的多。不考慮編譯器版本(VC6 或 VS.NET),我們可以使用(/Zd, /Z7, /Zi, /ZI)中一個選項通知編譯器爲靜態庫生成調試信息。
關鍵問題是將調試信息保存在什麼地方。當使用/Z7或/Zd選項時,調試信息保存在.LIB文件中;當使用/Zi或/ZI選項時,調試信息保存在.PDB文件中(當然可以使用/Fd指定文件名)。
調試信息對可執行文件的大小的影響
調試信息對可執行文件的大小的影響,決定於存儲調試信息的地方,也間接的決定於所使用的格式。
COFF和CodeView格式下,調試信息保存在可執行文件中,因此可執行文件的大小將顯著增長(通常要增長一倍以上,甚至更大)。
程序數據庫格式下,調試信息單獨保存,對可執行文件的大小幾乎沒有影響。在這種情況下,可執行文件需要保存一個頭信息方便調試器對調試信息進行定位,因此需要增長大約幾百個字節。
要避免可執行文件的膨脹,我們需要在使用/debug 同時,將/opt:ref 選項改爲opt:noref。這樣做,有一個另外的結果就是關閉了鏈接器的大小優化。如果要恢復大小優化,需要改回/opt:ref。
.DBG 文件
使用一個小工具——Rebase——可以將CodeView格式的內容從可執行文件中導出,存入到DBG文件中。Rebase包含在Visual Studio中。除了用於導出DBG文件外,它還有其他的一些用途。如果用於導出DBG文件,其命令行格式爲:
rebase –b BaseAddr –x SymbolDir [-p] ExeName
選項 |
描述 |
-b BaseAddr |
指定可執行文件的基地址,如果你不想更改基地址,就指定當前可執行文件所使用的地址 |
-x SymbolDir |
制定存放.DBG文件的目錄, 使用“.”表示當前目錄 |
-p |
如果該選項被使用,DBG文件只包含公共函數和變量和FPO信息 |
例如:下面的命令行從DLL中導出調試信息到當前目錄下的DBG文件中: rebase –b 0x60000000 –x . MyDll.dll
調試器和調試信息的格式
通用的調試器支持的格式如下:
調試器 |
COFF |
CodeView |
Program Database (2.0) |
Program Database (7.0) |
Visual Studio.NET |
- |
+ |
+ |
+ |
Visual C++ 6.0 |
+ |
+ |
+ |
- |
WinDbg 6.3 |
+ |
+ |
+ |
WinDbg 6.3 部分支持CodeView格式,它只能讀取下列信息:
- 公共函數和變量
- FPO信息
- 源文件和代碼行信息
它可以單步進入源代碼,看到調用堆棧,但無法觀察變量的值(因此類型信息不被支持).
操作系統符號文件(symbols)
Windows操作系統所公開的調試系統格式如下:
操作系統 |
格式 |
Windows NT 4.0 |
CodeView (.DBG files) |
Windows 2000 |
CodeView (.DBG files) and Program Database (2.0) |
Windows XP (including SP1 and SP1a) |
Program Database (2.0) |
Windows XP SP2 |
Program Database (7.0) |
Windows 2003 Server |
Program Database (2.0) |