Windows調試的基石――符號(1)
當應用程序被鏈接以后,代碼被逐一地翻譯為一個個的地址,優化以后的代碼可能初看起來更是面目全非。每當我們使用vs或者windbg等微軟的調試工具進行調試的時候,我們可以方便地使用變量名來查看內存、可以使用函數名稱來下斷點、甚至可以指定某個文件的某一行來下斷點。這一切背后是什么在指導調試器工作呢?答案就是符號——pdb或者dbg文件(.NET自己有元數據,符號不需要元數據已有的信息)。
程序運行的時候,計算機只需要逐條執行指令即可。而與源代碼對應的關系是完全不需要知道的。這就給調試帶來了困難,所以無論什么編譯都有自己的一套用于對應代碼和可執行程序。各種編譯器都有自己保存類似這種對應關系的辦法,有的直接嵌入可執行文件,有的則是獨立出來的。而微軟的編譯器則是獨立產生了這種文件,它就被成為符號文件。
符號文件的歷史有興趣可以網上查查,dbg文件十分古老,微軟在新的產品中也不再使用了。所以今天我們新產生的符號文件一般都是pdb文件。而pdb可以理解成提供給調試器用于對應可執行文件和源代碼的東西,這個東西運行的時候是沒有任何作用的,但是對于調試器和我們調試則有很大的幫助。
那么pdb文件里面到底存儲了什么東西呢?根據微軟官方的解釋有:
1、 全局變量;
2、 局部變量;
3、 函數名及入口點;
4、 FPO記錄;
5、 源代碼行號。
如果使用vs2010隨便寫一個本機C語言,那么鏈接的時候編譯器就會幫我們產生一個pdb文件。里面包含大量的符號,包括上面提及的內容。
每次調試程序或者查dmp的時候,我們都必須使用正確的符號。否則我們看到的棧等信息可能不準確。同時我們也無法建立應用程序和源代碼之間的關系,沒有符號你所面對只有地址。
符號同時又分為兩種:public symbols and private symbols。至于他們的區別以后再具體介紹。
調試器是如何來判別EXE、DLL等是否和一個pdb文件匹配呢?每次我們鏈接EXE或者DLL或者SYS的時候,鏈接器都將產生一個唯一的GUID,然后將其寫入到PDB和可執行文件。調試器加載的時候將檢查兩者的GUID,如果一致就表示他們匹配。
很多時候我們對PDB都不夠重視。如果足夠自信發布的東西一定不會產生bug,而且確實也沒有產生bug,或者用戶為0。那么PDB對我們確實沒有多大作用,但是如果我們需要調試,我們需要查dmp文件,那么請妥善保管好自己的代碼和pdb。每次重新編譯,即使所有代碼均沒有變化,他們的GUID也不同(PDB還有age的概念,以后再解釋)。
想想每個版本從測試到發布得編譯多少次,每次都得辛苦去找PDB那么不是很痛苦啊。所幸我們有符號服務器這種東西。微軟有自己HTTP符號服務器,我們自己也可以在20s內迅速搭建(以后會介紹如何搭自己的建符號服務器)。而且較新的vs或者windbg都能智能得對符號服務器進行搜索,避免了自己找符號的麻煩。
為了提供一個基本的感性認識,我們看看符號和DLL之間的關系:
0:012> !lmi ntdll
Loaded Module Info: [ntdll]
Module: ntdll
Base Address: 77040000
Image Name: ntdll.dll
Machine Type: 332 (I386)
Time Stamp: 4a5bdadb Tue Jul 14 09:09:47 2009
Size: 13c000
CheckSum: 14033f
Characteristics: 2102 perf
Debug Data Dirs: Type Size VA Pointer
CODEVIEW 22, d5308, d4708 RSDS - GUID: {F0164DA7-1FAF-4765-B8F3-DB4F2D7650EA}
Age: 2, Pdb: ntdll.pdb
CLSID 4, d5304, d4704 [Data not mapped]
Image Type: FILE - Image read successfully from debugger.
C:\Windows\SYSTEM32\ntdll.dll
Symbol Type: PDB - Symbols loaded successfully from symbol server.
c:\symcache\ntdll.pdb\F0164DA71FAF4765B8F3DB4F2D7650EA2\ntdll.pdb
Load Report: public symbols , not source indexed
c:\symcache\ntdll.pdb\F0164DA71FAF4765B8F3DB4F2D7650EA2\ntdll.pdb
從上面紅色的地方我們可以看到ntdll里面的GUID、age等信息。同時我們從微軟的符號服務器下載了對應的符號,然后保存到了本地的c:\symcache里。
當我們使用vs進行調試的時候,編譯器總是能幫我們找到我們編譯的應用程序或者DLL的符號,所以往往我們不會遇到和符號相關的太多麻煩。但是如果我們使用的是其他調試工具,或者查dmp的時候,符號的問題就來了。如果我們給調試器指定了正確符號文件,那么一切都很正常,否則我們將看到令人困惑的東西。
本文簡單介紹了一下符號的概念,以后陸續會對符號做一個比較系統的介紹。
注:本文所提到的符號均是指微軟PDB格式的符號。
調試信息的歷史回顧
連接二進制指令和源代碼之間的紐帶——符號是如何被編譯器生成的呢?要具體了解這個內容我們需要先簡單回顧一下微軟調試信息格式的歷史。
COFF:
最早的調試信息格式是COFF格式,這種格式內嵌到可執行文件中的,它能記錄函數、變量、行號、FPO等信息。
CodeView:
隨后就是比較熟悉的CodeView了。這種調試信息的格式提供了內嵌和分離兩種形式,和PDB唯一的不同就是沒有編輯并繼續的功能。獨立的CodeView調試信息存儲在.dbg文件中。
PDB:
而微軟最新的調試信息格式就是PDB(Program Data Base)了。這種調試信息和可執行文件是完全分離的。他記錄了很多豐富的信息,同時還提供了調試并繼續、增量鏈接的功能。不過這種調試信息的格式并沒有官方的公開文檔,要操作它一般只有通過debughelp或者DIA。PDB又分為兩種格式,一種是vc6使用的PDB2.0,后來的版本則全是PDB7.0。PDB7.0是不能向下兼容的。
編譯器產生符號的過程
我們看到調試信息是逐步發展的,最新的調試信息格式為PDB7.0。這是一種和可執行文件分離的格式。對于可執行文件,一般只有幾百字節的額外負擔。下面我們僅討論PDB這種調試信息格式。
如果指定生成調試信息,編譯器在每次編譯完文件以后就會產生一個obj文件,然后同時產生它對應的調試信息。當我們進行連接的時候,編譯器就會幫我們把所有obj統一編譯為一個可執行文件,然后所有的調試信息統一生成一個PDB文件。
如果我們是生成靜態庫,那么編譯器編譯完各個源代碼以后會統一產生lib文件,同時也將所有的調試信息生成到一個pdb中。如果我們在編譯可執行文件的同時需要使用某一個靜態庫,那么編譯器也需要使用到靜態庫的調試信息,最終可執行文件和調試信息都被單獨地生成。
編譯器選項
對于VS系列編譯器,我們可以有一個總開關:/debug。如果沒有這個鏈接選項,所有調試信息均不會被生成。/pdb可以指定符號文件的名稱;/pdbstripped可以指定是否同時產生一個公共符號(public symbol)。
編譯選項則有:/Z7 /Zi /ZI 3種。其中/Z7表示生成CodeView格式的調試信息;/Zi表示生成不支持編輯并繼續的PDB調試信息;/ZI表示生成支持編輯并繼續的PDB調試信息。
上面提到的選項均有項目屬性的GUI設置與之對應:
靜態庫的符號問題
曾經遇到過一個問題,就是使用了vc6編譯的靜態庫,然后在vs2008中進行鏈接。結果每次鏈接的時候都產生警告,提示沒有找到靜態庫的符號,結果就像沒有調試信息一樣。這個問題研究很久無果。
后來自習研究了一下靜態庫的編譯方式才解決了問題。上面已經提到,靜態庫的PDB是每個文件的調試信息的集合,而默認情況下靜態庫生成的PDB文件都是VCX0.PDB,例如vs2008就是VC90.PDB,VS2010就是VC100.PDB。生成靜態庫以后,最終的可執行程序進行鏈接時候,就會根據lib中各個obj記錄的信息區找VCX0.PDB,而這個文件就是我們需要的。如果我們要鏈接很多個靜態庫,可能就需要在編譯靜態庫的時候/FD給靜態庫的符號重命名了。
這一點在.NET中解決得很好,所有依賴的程序集符號都會被自動保存,并且程序集之間的符號不會合并為一份。
符號的生成非常簡單,幾個編譯選項就搞定,默認情況下DEBUG模式都會產生編輯并繼續的符號,而Release模式建議也使用/Zi來產生對應調試信息。
RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成