一、調試基礎
調試快速鍵
F5: 開始調試
Shift+F5: 停止調試
F10: 調試到下一句,這裡是單步跟蹤
F11: 調試到下一句,跟進函數內部
Shift+F11: 從當前函數中跳出
Ctrl+F10: 調試到游標所在位置
F9: 設定(取消)斷點
Alt+F9: 進階斷點設定
跟蹤調試
1、 盡量使用快速鍵時行調試
2、 觀察調試資訊
3、 進階中斷設定
異常調試
重試->取消->調試
函數堆棧,用variables或者call stack 視窗
Release調試
1、 經常測試你的Debug和Release版本
2、 不要移除調試代碼,如用ASSERT, TRACE等。
3、 初始設定變數,特別是全域變數,malloc的記憶體,new的記憶體
4、 當你移除某個資源時,確保你移除了所有跟這個資源相關的申明(主要是在resouce.h文中)
5、 使用3或者4級的警告級編譯你的代碼,並確保沒有警告,project->setting->c/c++->warninglevel(中文版是項目->屬性->C/C++->常規->警告層級)
6、 _debug改成NDEBUG進行調試,project->setting->C/C++->Preprocessordefinitions(中文版是項目->屬性->C/C++->前置處理器->預先處理定義)(這裡是debug和Release編譯的重要不同之一)
7、 在Release中調試原始碼,project->setting->C/C++->debug info選擇programDataBase(中文版是項目->屬性->C/C++->常規->調試資訊格式->用於“編輯並繼續”的程式資料庫),project->setting->link選上Generate debug info(中文版是項目->屬性->連結器->調試->產生調試資訊)
8、 走讀代碼,特別關注堆棧和指標
二、TRACE宏
當選擇了Debug目標,並且afxTraceEnabled變數被置為TRUE時,TRACE宏也就隨之被啟用了。但在程式的Release版本中,它們是被完全禁止的。下面是一個典型的TRACE語句:
…
int nCount =9;
CString strDesc("total");
TRACE("Count =%d,Description =%s\n",nCount,strDesc);
…
可以看到,TRACE語句的工作方式有點像C語言中的printf語句,TRACE宏參數的個數是可變的,因此使用起來非常容易。如果查看MFC的原始碼,你根本找不到TRACE宏,而只能看到TRACE0、TRACE1、TRACE2和TRACE3宏,它們的參數分別為0、1、2、3。
個人總結:最近看網路編程是碰到了TRACE語句,不知道在哪裡輸出,查了一晚上資料也沒找出來,今天終於找到了,方法如下:
1.在MFC中加入TRACE語句
2.在TOOLS->MFCTRACER中選擇 “ENABLE TRACING”點擊OK
3.進行調試運行,GO(F5)(特別注意:不是執行‘!’以前之所以不能看到TRACE內容,是因為不是調試執行,而是‘!’了,切記,切記)
4.然後就會在OUTPUT中的DEBUG視窗中看到TRACE內容了,調試執行會自動從BUILD視窗跳到DEBUG視窗,在那裡就看到TRACE的內容了,^_^
以下是找的TRACE的詳細介紹:
==============================
TRACE宏對於VC下程式調試來說是很有用的東西,有著類似printf的功能;該宏僅僅在程式的DEBUG版本中出現,當RELEASE的時候該宏就完全消失了,從而協助你調式也在RELEASE的時候減少代碼量。
使用非常簡單,格式如下:
TRACE("DDDDDDDDDDD");
TRACE("wewe%d",333);
同樣還存在TRACE0,TRACE1,TRACE2。。。分別對應0,1,2。。個參數
TRACE資訊輸出到VC IDE環境的輸出視窗(該視窗是你編譯項目出錯提示的哪個視窗),但僅限於你在VC中運行你的DEBUG版本的程式。
TRACE資訊還可以使用DEBUGVIEW來捕獲到。這種情況下,你不能在VC的IDE環境中運行你的程式,而將BUILD好的DEBUG版本的程式單獨運行,這個時候可以在DEBUGVIEW的視窗看到DEBUGVIE格式的輸出了。
VC中TRACE的用法有以下四種:
TRACE1 ,就是不帶動態參數輸出字串, 類似C的printf("輸出字串");
TRACE2: 中的字串可以帶一個參數輸出 ,類似C的printf("...%d",變數);
TRACE3:可以帶兩個參數輸出,類似C的printf("...%d...%f",變數1,變數2);
TRACE4 可以帶三個參數輸出,類似C的printf("...%d,%d,%d",變數1,變數2,變數3);
TRACE 宏有點象我們以前在C語言中用的Printf函數,使程式在運行過程中輸出一些調試資訊,使我們能瞭解程式的一些狀態。但有一點不同的是:
TRACE 宏只有在調試狀態下才有所輸出,而以前用的Printf 函數在任何情況下都有輸出。和Printf 函數一樣,TRACE函數可以接受多個參數如:
int x = 1;
int y = 16;
float z = 32.0;
TRACE( "This is a TRACE statement\n" );
TRACE( "The value of x is %d\n", x );
TRACE( "x = %d and y = %d\n", x, y );
TRACE( "x = %d and y = %x and z = %f\n", x, y, z );
要注意的是TRACE宏只對Debug 版本的工程產生作用,在Release 版本的工程中,TRACE宏將被忽略。
三、ASSERT宏
如果你設計了一個函數,該函數需要一個指向文檔對象的指標做參數,但是你卻錯誤地用一個視圖指標調用了這個函數。這個假的地址將導致視資料的破壞。現在,這種類型的問題可以被完全避免,只要在該函數的開始處實現一個ASSERT測試,用來檢測該指標是否真正指向一個文檔對象。一般來講,編程者在每個函數的開始處均應例行公事地使用assertion。ASSERT宏將會判斷運算式,如果一個運算式為真,執行將繼續,否則,程式將顯示一條訊息並且暫停,你可以選擇忽視這條錯誤並繼續、終止這個程式或者是跳到Debug器中。下面一例示範了如何使用一個ASSERT宏去驗證一個語句。
void foo(char p, int size )
{
ASSERT( p != 0 ); //確認緩衝區的指標是有效
ASSERT( ( size >= 100 ); //確認緩衝區至少有100個位元組
// Do the foo calculation
}
這些語句不產生任何代碼,除非—DEBUG處理器標誌被設定。Visual C++只在Debug版本設定這些標誌,而在Release版本不定義這些標誌。當—DEBUG被定義時,兩個assertions將產生如下代碼:
//ASSERT( p!= 0 );
do{
if( !(p !=0) && AfxAssertFailedLine(—FILE—,—LINE—) )
AfxDebugBreak();
}while(0);
//ASSERT((size 〉= 100);
do{
if(!(size 〉= 100) &&AfxAssertFailedLine(—FILE—,—LINE—))
AfxDebugBreak();
}while(0);
Do-while迴圈將整個assertion封裝在一個單獨的程式塊中,使得編譯器編譯起來很舒暢。If語句將求取運算式的值並且當結果為零時調用AfxAssertFailedLine()函數。這個函數將彈出一個對話方塊,其中提供三個選項“取消、重試或忽略”,當你選取“重試”時,它將返回TRUE。重試將導致對AfxDebugBreak()函數的調用,從而啟用調試器。
AfxAssertFailedLine()是一個未正式公布的函數,它的功能就是顯示一個訊息框。該函數的原始碼駐留在afxasert.cpp中。函數中的—FILE—和—LINE—語句是處理器標誌,它們分別指定了源檔案名稱和當前的行號。
AfxAssertFailedLine()是一個未正式公布的函數,它的功能就是顯示一個訊息框。該函數的原始碼駐留在afxasert.cpp中。函數中的—FILE—和—LINE—語句是處理器標誌,它們分別指定了源檔案名稱和當前的行號。
四、VERIFY 宏
因為assertion只能在程式的Debug版本中起作用,在運算式中不可以包含指派陳述式、增加語句(++)或者是減少語句(--),因為,這些語句實際改變資料。可有時你可能想要驗證一個能動的運算式,使用一個指派陳述式。那麼就到了用VERIFY宏來替代ASSERT。例如:
voidfoo(char p, int size )
{
char q;
VERIFY(q = p);
ASSERT((size 〉= 100);
//Do the foo calculation
//Do the foo calculation
}
在Debug模式下,ASSERT和VERIFY是一回事,但是在Release模式下,VERIFY宏仍然測試運算式而assertion卻不起任何作用。可以說,在Release模式下,ASSERT語句被刪除了。
請注意,如果你在一個ASSERT語句中錯誤地使用了一個能動的運算式,編譯器將不做任何警告地忽略它。在Release模式下,該運算式就會被無聲息地刪除掉,這將會導致程式的錯誤運行。由於Release版的程式通常不包含Debug資訊,這類錯誤將很難被發現。
五、VC進階調試方法-條件及資料斷點的設定
(一)位置斷點(LocationBreakpoint)
大家最常用的斷點是普通的位置斷點,在來源程式的某一行按F9就設定了一個位置斷點。但對於很多問題,這種樸素的斷點作用有限。譬如下面這段代碼:
void CForDebugDlg::OnOK()
{
for(int i = 0; i < 1000; i++) //A
{
intk = i * 10 - 2; //B
SendTo(k); //C
inttmp = DoSome(i); //D
Trace0("這裡要輸出的內容”);//在這裡可以輸出一些有用的資訊,你也可以輸出I的值,都是可以的
intj = i / tmp; //E
}
}
//其實我們還可以用其他方法調式也是一樣的,你可以用TRACE0宏來輸出迴圈中的每一個結果,我們也可以在debug中看見輸出的結果,當出現問題時,輸出的結果可能就不一樣了,我們可以分析一下debug中的結果找出問題的所在
執行此函數,程式崩潰於E行,發現此時tmp為0,假設tmp本不應該為0,怎麼這個時候為0呢?所以最好能夠跟蹤此次迴圈時DoSome函數是如何啟動並執行,但由於是在迴圈體內,如果在E行設定斷點,可能需要按F5(GO)許多次。這樣手要不停的按,很痛苦。使用VC6斷點修飾條件就可以輕易解決此問題。步驟如下。
1 Ctrl+B開啟斷點設定框,如:
Figure 1設定進階位置斷點
2 然後選擇D行所在的斷點,然後點擊condition按鈕,在彈出對話方塊的最下面一個編輯框中輸入一個很大數目,具體視應用而定,這裡1000就夠了。
3 按F5重新運行程式,程式中斷。Ctrl+B開啟斷點框,發現此斷點後跟隨一串說明:...487 times remaining。意思是還剩下487次沒有執行,那就是說執行到513(1000-487)次時候出錯的。因此,我們按步驟2所講,更改此斷點的skip次數,將1000改為513。
4 再次重新運行程式,程式執行了513次迴圈,然後自動停在斷點處。這時,我們就可以仔細查看DoSome是如何返回0的。這樣,你就避免了手指的痛苦,節省了時間。
再看位置斷點其他修飾條件。如Figure 1所示,在“Enter the expression to be evaluated:”下面,可以輸入一些條件,當這些條件滿足時,斷點才啟動。譬如,剛才的程式,我們需要i為100時程式停下來,我們就可以輸入在編輯框中輸入“i==100”。
另外,如果在此編輯框中如果只輸入變數名稱,則變數發生改變時,斷點才會啟動。這對檢測一個變數何時被修改很方便,特別對一些大程式。
用好位置斷點的修飾條件,可以大大方便解決某些問題。
(二) 資料斷點(DataBreakpoint)
軟體調試過程中,有時會發現一些資料會莫名其妙的被修改掉(如一些數組的越界寫導致覆蓋了另外的變數),找出何處代碼導致這塊記憶體被更改是一件棘手的事情(如果沒有調試器的協助)。恰當運用資料斷點可以快速幫你定位何時何處這個資料被修改。譬如下面一段程式:
#include "stdafx.h"
#include <string.h>
int main(int argc, char* argv[])
{
charszName1[10];
charszName2[4];
strcpy(szName1,"shenzhen");
printf("%s\n",szName1); //A
strcpy(szName2,"vckbase"); //B
printf("%s\n",szName1);
printf("%s\n",szName2);
return0;
}
這段程式的輸出是
szName1: shenzhen
szName1:ase
szName2:vckbase
首先我給你分析一下為什麼會是這樣的結果呢!首先你在strcpy(szName1,"shenzhen");這個地方F9設定一個斷點,然後F5運行程式,這是程式會斷到我們設定的斷點,如
看到了吧,問題出現的原因就在這裡,系統給szName2分配的地址是0x0012ff70這裡是4個位元組,然後呢,在0x0012ff70後面4個位元組處,開始分配szName1這10個位元組,也就是在0x0012ff74處開始分配10個位元組,
F10單步跟蹤,來到printf("%s\n", szName1)這一行,如
szName1分配的空間已經附上了值.
F10走到下一個printf("%s\n", szName1) 看,
因為szName1 和szName2分配的空間是連續的,所以給szName2賦值超過所容納的位元組時就開始覆蓋szName1的內容了,所以說當我們在輸出結果的時候就出現我們想不到的結果了,
那麼怎麼去調試呢,下面是具體的方法
szName1何時被修改呢?因為沒有明顯的修改szName1代碼。我們可以首先在A行設定普通斷點,F5運行程式,程式停在A行。然後我們再設定一個資料斷點。如:
Figure 2 資料斷點
F5繼續運行,程式停在B行,說明B處代碼修改了szName1。B處明明沒有修改szName1呀?但調試器指明是這一行,一般不會錯,所以還是靜下心來看看程式,哦,你發現了:szName2隻有4個位元組,而strcpy了7個位元組,所以覆寫了szName1。
資料斷點不只是對變數改變有效,還可以設定變數是否等於某個值。譬如,你可以將Figure 2中紅圈處改為條件”szName2[0]==''''y''''“,那麼當szName2第一個字元為y時斷點就會啟動。
可以看出,資料斷點相對位置斷點一個很大的區別是不用明確指明在哪一行代碼設定斷點。
(三) 其他
1 在call stack視窗中設定斷點,選擇某個函數,按F9設定一個斷點。這樣可以從深層次的函數調用中迅速返回到需要的函數。
2 Set Next StateMent命令(debug過程中,右鍵菜單中的命令)
此命令的作用是將程式的指令指標(EIP)指向不同的程式碼。譬如,你正在調試上面那段代碼,運行在A行,但你不願意運行B行和C行代碼,這時,你就可以在D行,右鍵,然後“Set Next StateMent”。調試器就不會執行B、C行。只要在同一函數內,此指令就可以隨意跳前或跳後執行。靈活使用此功能可以大量節省調試時間。
3 watch視窗
watch視窗支援豐富的資料格式化功能。如輸入0x65,u,則在右欄顯示101。
即時顯示windows API調用的錯誤:在左欄輸入@err,hr。
在watch視窗中調用函數。提醒一下,調用完函數後馬上在watch視窗中清除它,否則,單步調試時每一步調試器都會調用此函數。
4 messages斷點不怎麼實用。基本上可以用前面講述的斷點代替。
六。VC調試環境設定
為了調試一個程式,首先必須使程式中包含調試資訊。一般情況下,一個從AppWizard建立的工程中包含的Debug Configuration自動包含調試資訊,但是是不是Debug版本並不是程式包含調試資訊的決定因素,程式設計者可以在任意的Configuration中增加調試資訊,包括Release版本。
為了增加調試資訊,可以按照下述步驟進行:
開啟Projectsettings對話方塊(可以通過快速鍵ALT+F7開啟,也可以通過IDE菜單Project/Settings開啟)
選擇C/C++頁,Category中選擇general ,則出現一個Debug Info下拉式清單方塊,可供選擇的調試資訊 方式包括:
命令列 Project settings 說明
無 None 沒有調試資訊
/Zd Line Numbers Only 目標檔案或者可執行檔中只包含全域和匯出符號以及程式碼資訊,不包含符號調試資訊
/Z7 C7.0- Compatible 目標檔案或者可執行檔中包含行號和所有符號調試資訊,包括變數名及類型,函數及原型等
/Zi Program Database 建立一個程式庫(PDB),包括類型資訊和符號調試資訊。
/ZI Program Databasefor
Edit and Continue 除了前面/Zi的功能外,這個選項允許對代碼進行調試過程中的修改和繼續執行。這個選項同時使#pragma設定的最佳化功能無效
選擇Link頁,選中複選框"Generate DebugInfo",這個選項將使連接器把調試資訊寫進可執行檔和DLL
如果C/C++頁中設定了Program Database以上的選項,則Link incrementally可以選擇。選中這個選項,將使程式可以在上一次編譯的基礎上被編譯(即增量編譯),而不必每次都從頭開始編譯。
虢孑和deny和做收集整理