Visual C++ 之 調試程序

一、Debug調試器
發佈版本沒有調試信息,不能進行程序調試,但程序進行了優化,DEBUG版本則包含調試信息(在Debug文件夾),但沒有優化。所以程序有時候能夠在調試版本運行,但不能運行於發佈版本。編程時一般先生成一個Debug版本程序,程序在經過調試確認無誤後,再編譯鏈接生成一個Release版本程序。

調試工具:

1. 調試窗口
用於顯示程序的調試信息。
Output窗口:顯示有關build和debug操作的信息,包括編譯鏈接錯誤信息和調試時一些宏的輸出信息
variable窗口:觀察和修改某個作用域內所有變量的當前值,調試器可根據當前程序運行過程中變量的變化情況自動選擇應顯示的變量。可以在Context下拉列表選擇要查看的函數,然後會顯示函數內部的局部變量的值。Auto頁面顯示當前語句或者前一條語句(當前語句沒有相關變量值);Locals顯示當前函數局部變量的值;This頁面以樹形方式顯示當前類的對象的所有成員。
Watch窗口:觀察和修改變量或表達式的值,但需要程序員在窗口設置要觀察的變量或表達式。

調試窗口用紅色表示變量的值在程序當前的執行過程中發生了變化。可以在調試窗口手工改變變量的值,程序採用新的變量繼續向後執行。另外,工具欄有工具按鈕可以是用戶在調試過程中改變的語句生效。
如果變量是一個對象、對象引用、指針,調試窗口將自動展開變量,顯示其成員信息。

2. 其他窗口
Register窗口:顯示通用寄存器和CPU狀態寄存器的內容(標誌值、浮點堆棧)
Memory窗口:顯示當前內存的內容;Address框允許你指定從哪個虛擬內存地址開始顯示。 
Call Stack窗口:列出所有調用未結束的函數,當前函數在堆棧的頂端。 
Disassembly窗口:列出反彙編後得到的由編譯器生成的對應於源代碼的彙編指令

3. 設置斷點:
在源程序的某語句設置一個暫停點,在調試器中運行程序時可以強制程序執行到斷點暫時停止運行。
單步執行:
(1)Step into:遇見函數調用語句,進入函數內部
(2)Step over:遇見函數調用語句,但不進入函數內部,跳過該函數。調試時,如果不能確定這個函數是否有錯,一般先跳過函數而不進入。
(3)Step out:從當前的函數中跳出,程序流程執行函數調用語句的下一步。
4.調試符號
程序數據庫文件(.pdb)包含了Visual C++調試器所需的調試信息和程序信息。調試信息包含了變量的名字和類型、函數原型、源代碼行號、類和結構的佈局、FPO調試信息(重建堆棧幀)以及進行增量鏈接所需的信息。


二、編譯選項

1.針對調試版本的編譯選項:
a) /MDd,/MLd或者/MTd 
調試版本的運行時刻庫(Runtime Library)有調試符號,使用了調試堆,調試堆的目的是發現內存破壞和內存泄漏,並且向用戶報告源代碼的哪個地方出了問題。
特性: 
(1)調試版本的運行時刻庫(Runtime Library)對內存的分配做了追蹤,允許用戶檢查內存泄漏;
(2)在剛分配的內存裏寫上0XCD的字節模式,用0XCD來填充剛分配的內存,有助於發現數據未被初始化的錯誤;
(3)在被釋放的內存寫上0xDD的字節模式,有助於發現已被釋放的內存。;
(4)在緩衝區的兩頭邊界分配了四字節的保護數據,並用0xFD的字節模式作初始化,來檢查寫內存的上溢出和下溢出;
(5)在每個內存分配的地方對源代碼文件名和行號作了記錄,有助於用戶在源代碼中對內存分配進行定位

b)/Od
用來關閉優化開關。
好處:
(1)容易理解:未被優化的代碼直接對應於源代碼,所以比優化後的代碼更容易讀懂;
(2)節省調試時間:未被優化的代碼編譯和鏈接會更快,會有更短的調試周期;
(3)優化可能帶來潛在錯誤:優化代碼要求編譯器做一些假設,去除冗餘,但有時這個假設是錯誤的,並且去掉的冗餘也有可能隱藏錯誤。所以優化不一定比調試版本更好。

c)/D “_DEBUG” 
打開條件編譯調試代碼開關。
實際上這也是一個條件編譯:只有這個符號被定義,調試代碼纔會被編譯。MFC使用_DEBUG符號來確定到底鏈接的是哪個版本的MFC類庫。參考之前寫過的條件編譯。

d)/ZI 
創建編輯繼續(Edit and Continue)的程序數據庫。這個選項會打開/GF編譯選項,/GF編譯選項會消除重複字符串,並將字符串放到只讀內存。

e)/GZ 
在調試版本中用來發現那些在發佈版本里才發現的錯誤。
(1)用0xCC模式初始化自動(本地)變量;不是0xCD
(2)在通過函數指針調用函數時,檢查棧指針,確認是否有調用規則不匹配;
(3)在函數最後檢查棧指針是否被改變。

f)/Gm 

打開最小化重新鏈接開關,減少鏈接時間。 



2.針對Release版本的編譯選項
a)/MD,/ML或者/MT
i.使用發佈版本的運行時刻庫(Runtime Library)。 
b)/O1或者/O2 
i.打開優化開關,使得程序會最小或者速度會最快;
ii.優化器還可能發現代碼中潛在的錯誤,而這些錯誤可能會被調試版本掩蓋。(調試版本的GZ) 
c)/D “NDEBUG” 
i.關閉條件編譯調試代碼開關。
d)/GF 
i.消除重複字符串並將它們放到只讀內存中以避免被錯誤地修改。
e)/ZI
i.創建包含調試符號的程序數據庫。 (如果一個錯誤只發生在發佈版本里,除非你是個彙編高手,否則你需要調試符號來提示你到底程序出現了什麼問題。)


     三、提高調試器的差錯能力
1. 儘量採用編譯時刻檢查而不是運行時刻檢查。

2. 使用最高的編譯警告級別/W4

象if(x=2)這樣的語句,默認的警告級別爲/W3時不顯示任何信息,但改成最高警告級別/W4時則會出現“waning C4706:assignment within conditional expression”的警告。/W4能給出一些/W3所不能給的警告。 

3. 在調試版本中使用/GZ編譯選項 
/GZ選項用來發現那些在發佈版本里才發現的錯誤,包括未被初始化的自動(局部)變量、堆棧錯誤、不正確的函數原型等。 
4. 使用#pragma warning編譯器指示 
可以使用#pragma warning編譯器指示來禁止整個程序、特定的頭文件、特定的代碼文件或是特定的某一行代碼的特定警告
5. 使用沒有警告的編譯法則/WX 
這個編譯選項把所有的警告當成錯誤來對待,只有在假警告被消除之後才能應用。

四、內存虛擬地址空間
Windows使用一組固定的範圍來分割進程的4GB虛擬地址空間,因此有時可通過查看指針的返回值來判斷指針是否有效。 

(1)Windows2000虛擬地址空間劃分 
0~0XFFFF(64KB):不能用來檢測空指針賦值(訪問衝突) 
0x10000(64KB)~0x7FFEFFFF(2GB-64KB):Win32進程私有的(非保留的),用於程序代碼和數據 
0x7FFF0000(2GB-64KB)~0x7FFFFFFF(2GB):不能用來防止覆蓋OS分區(訪問衝突) 
0x800000000(2GB)~0xFFFFFFFF(4GB):爲操作系統保留,不可訪問(訪問衝突) 

(2)Windows2000虛擬地址空間使用 
0x00030000~0x0012FFFF:線程棧 
0x00130000~0x003FFFFF:堆(有時堆位於此處) 
0x00400000~0x005FFFFF:可執行代碼 
0x00600000~0x0FFFFFFF:堆(有時堆位於此處) 
0x10000000~0x5FFFFFFF:App DLLs、Msvcrt.dll、Mfc42.dll 
0x77000000~0xFFFFFFFF:Advapi32.dll、Comctl32.dll、Gdi32.dll、Kernel32.dll、Ntdll.dll、Rpcrt4.dll、Shell32.dll、User32.dll 

其中,0x00400000是所有版本的Windows能使用的最低基地址。 


五、常見內存錯誤
1、內存分配錯誤:動態內存分配錯誤有兩種基本類型:內存錯誤和內存泄漏。 
a)內存錯誤
當一個指針或者該指針所指向的內存單元成爲無效單元,或者內存中的數據結構被破壞時,再使用原來的指針(懸垂指針/迷途指針,指針的值還是原來指向的內存的地址,但是該內存上的內容已經被釋放)去訪問對象就會造成內存錯誤。
無效指針的原因:指針未被初始化,指針被初始化爲一個無效地址,指針被不小心錯誤地修改,在與指針相關聯的內存區域被釋放後使用該指針(懸垂指針/迷途指針),這些都會使指針成爲無效指針。
內存分配錯誤:當通過一個錯誤的指針或者懸垂指針對內存進行寫入,或者將指針強制轉換爲不匹配的數據結構,又或者是寫數據越界,內存自身都會遭到破壞。刪除未被初始化的指針(釋放還沒被分配的內存)、刪除非堆指針(指向的不是new出來的對象所佔的內存,即不在堆區)、多次刪除同一指針或者覆蓋一個指針的內部數據結構,都會造成內存分配系統錯誤。

b)內存泄漏
內存泄漏在動態分配的內存沒有被釋放時產生,產生原因:(1)沒有在程序的全部執行路徑中釋放內存;(2)沒有在析構函數中釋放所有的內存;等等
一個程序在崩潰之前可運行的時間越長,導致崩潰的原因與內存泄漏關係越大(比如循環次數非常大的函數)
系統會在程序結束的時候將泄漏的內存收回(回收整個應用程序的所有內存),因此內存泄漏是個暫時性的問題。但消除內存泄漏仍然尤其必要性:(1)內存泄漏往往導致系統資源的泄漏,動態分配的內存常不僅僅代表一塊存儲區域,還代表某些類型的系統資源(如文件、窗口、設備等);(2)高質量的程序和特定的服務器程序必須能夠無限地運行下去;(3)往往是其他程序錯誤或者不良編程習慣的徵兆。

2、內存的初始化
在調試版本,堆裏面未被初始化的內存被0xCD字節填充,堆裏釋放的內存用0xDD字節填充。堆棧裏面被初始化的內存用0xCC字節填充,調試版本和發佈版本中,未被初始化的全局內存都被初始化爲0;

消除錯誤:使用調試版本的 /MDd,/MLd或者/MTd 編譯選項。利用斷點調試等在調試窗口觀察內存的相應值。


六、一些調試方法
1.調試死循環
使用Debug菜單下的Break命令,如果程序有輸入請求,可以使用F12中斷程序,然後檢查窗口的調用棧,或單步跟蹤代碼找到死循環的發生原因。
2.用Spy++調試與消息有關的問題
調試消息的最好方案是使用Visual C++提供的Spy++工具。Spy++允許程序員查看窗口、消息、進程和線程。Spy++默認的消息輸出:第一欄顯示行號。第二欄顯示接受消息的句柄。第三欄中的“S”表示消息是用SendMessage發出的,“P”代表消息是由PostMessage發出的,“R”是消息句柄的返回值。第四欄給出解碼後的消息名,消息參數或返回值。 
3.非常規方法 
(1)重新編連你的應用程序  rebuild all
  當你的程序表現出異常的或意外的行爲,或者Visual C++編譯器因爲一個內部編譯器錯誤而失敗時,最好刪除工程中的Debug或Release文件夾,從頭開始重新進行編連。 
(2)重新啓動Visual C++ 
  Visual C++有超強的能力,但編譯器的某些特性也會引起奇怪的錯誤。如果你的程序表現得很奇怪,你可是試着清除所有的斷點,關閉或隱藏觀察窗口,檢查工程設置對話框看最近做了什麼修改,直至重新啓動Visual C++以便消除由於Visual C++環境引起的異常行爲。 
(3)重新啓動Windows 

  當你發現Windows或者其他程序表現出異常的或出人意料的行爲時,就應該重新啓動Windows,以消除操作系統給調試帶來的干擾。


最後,想了解Visual Studio的調試工具以及方法的可以參考這篇文章,寫得十分詳細:  http://blog.csdn.net/kingzone_2008/article/details/8133048

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章