天天看點

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

  從上面紅色的地方我們可以看到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中。如果我們在編譯可執行檔案的同時需要使用某一個靜态庫,那麼編譯器也需要使用到靜态庫的調試資訊,最終可執行檔案和調試資訊都被單獨地生成。

Windows調試的基石——符号(1)

  對于VS系列編譯器,我們可以有一個總開關:/debug。如果沒有這個連結選項,所有調試資訊均不會被生成。/pdb可以指定符号檔案的名稱;/pdbstripped可以指定是否同時産生一個公共符号(public symbol)。

  編譯選項則有:/Z7 /Zi /ZI 3種。其中/Z7表示生成CodeView格式的調試資訊;/Zi表示生成不支援編輯并繼續的PDB調試資訊;/ZI表示生成支援編輯并繼續的PDB調試資訊。

  上面提到的選項均有項目屬性的GUI設定與之對應:

Windows調試的基石——符号(1)
Windows調試的基石——符号(1)

  曾經遇到過一個問題,就是使用了vc6編譯的靜态庫,然後在vs2008中進行連結。結果每次連結的時候都産生警告,提示沒有找到靜态庫的符号,結果就像沒有調試資訊一樣。這個問題研究很久無果。

  後來自習研究了一下靜态庫的編譯方式才解決了問題。上面已經提到,靜态庫的PDB是每個檔案的調試資訊的集合,而預設情況下靜态庫生成的PDB檔案都是VCX0.PDB,例如vs2008就是VC90.PDB,VS2010就是VC100.PDB。生成靜态庫以後,最終的可執行程式進行連結時候,就會根據lib中各個obj記錄的資訊區找VCX0.PDB,而這個檔案就是我們需要的。如果我們要連結很多個靜态庫,可能就需要在編譯靜态庫的時候/FD給靜态庫的符号重命名了。

  這一點在.NET中解決得很好,所有依賴的程式集符号都會被自動儲存,并且程式集之間的符号不會合并為一份。

  符号的生成非常簡單,幾個編譯選項就搞定,預設情況下DEBUG模式都會産生編輯并繼續的符号,而Release模式建議也使用/Zi來産生對應調試資訊。

繼續閱讀