本文講述本機應用程序的一些常見調試問題和調試技術。 本節闡述的技術屬於高級別技術。
調試優化的代碼
當編譯器優化代碼時,它將重新定位並重組指令, 這會得到更高效的編譯的代碼。 由於這種調整,調試器並不總能確定與一組指令對應的源代碼。
優化可能影響到:
-
本地變量(可被優化器移除或移動到調試器無法識別的位置)。
-
函數內部的位置(當優化器合並代碼塊時發生變化的位置)。
-
調用堆棧上框架的函數名稱(如果優化器合並兩個函數,則函數名稱可能是錯誤的)。
但是,假定所有框架都有符號,則在調用堆棧上看到的框架幾乎總是正確的。 在下列情況下調用堆棧上的框架將是錯誤的:有堆棧損壞,有用匯編語言編寫的函數,或者有操作系統框架在調用堆棧上沒有匹配的符號。全局和靜態變量總是正確顯示。 結構布局也是這樣。 如果您有指向結構的指針而且指針的值是正確的,那么結構的每個成員變量都將顯示正確值。出於這些限制原因,只要有可能,就應使用程序的“未優化”版本進行調試。 默認情況下,優化在 Visual C++ 程序的“Debug”配置中關閉,在“Release”配置中打開。
但是,bug 可能僅在程序的優化版本中出現。 在此情況下,必須調試優化的代碼。
在“Debug”生成配置中打開優化
-
創建新項目時,請選擇 Win32 Debug目標。 接下來要一直使用 Win32 Debug目標,直至程序已進行全面調試,可以生成 Win32 Release目標為止。 調試器並不優化 Win32 Debug目標。
-
在解決方案資源管理器中選擇項目。
-
在“視圖”菜單上,單擊“屬性頁”。
-
在“屬性頁”對話框中,確保在“配置”下拉列表框中選擇了 Debug。
-
在左邊的文件夾視圖中,選擇 C/C++ 文件夾。
-
在“C++”文件夾下選擇 Optimization。
-
在右邊的屬性列表中找到“Optimization”。 它旁邊的設置可能顯示為“Disabled (/Od)”。 選擇其他選項(“Minimum Size (/O1)”、“Maximum Speed (/O2)”、“Full Optimization (/Ox)”或“Custom”)之一。

-
如果為“Optimization”選擇了“Custom”選項,現在便可為屬性列表中顯示的其他任何屬性設置選項。
調試優化的代碼時,請使用“反匯編”窗口以了解實際創建和執行了哪些指令。 設置斷點時,需要注意斷點可能隨指令一起移動。 例如,考慮以下代碼:
for (x=0; x<10; x++)
假定在該行設置了一個斷點。 可能希望該斷點被命中 10 次,但如果代碼進行了優化,則只會命中該斷點一次。 因為第一個指令將 x 的值設置為 0。 編譯器認定該指令只需執行一次,將其移出循環。 斷點隨之移動。 而比較和遞增 x 的指令仍留在循環內。 當查看“反匯編”窗口時,單步執行單元自動設置為“指令”以允許更大控制,這在逐句通過優化的代碼時很有用。
DebugBreak 和 __debugbreak
可以在代碼中的任意點調用 DebugBreak Win32 函數或 __debugbreak 內部類型。 DebugBreak 和 __debugbreak 具有與在該位置設置斷點相同的效果。因為 DebugBreak 是系統函數調用,所以必須安裝系統調試符號以確保中斷后顯示正確的調用堆棧信息。 否則,調試器可能在顯示一幀調用堆棧信息后就停止顯示。 如果使用 __debugbreak,則不需要符號。
斷言
斷言語句指定在程序的某些特定點應為真的條件。 如果該條件不為真,則斷言失敗,中斷程序的執行,並顯示“斷言失敗”對話框。
Visual C++ 支持基於下列構造的斷言語句:
-
MFC 程序的 MFC 斷言。
-
使用 ATL 的程序的 ATLASSERT。
-
使用 C 運行庫的程序的 CRT 斷言。
-
其他 C/C++ 程序的 ANSI assert 函數。
斷言可以用於:
-
捕捉邏輯錯誤。
-
檢查某操作的結果。
-
測試錯誤條件,這些錯誤條件應已處理。
MFC 和 C 運行庫斷言
當調試器由於 MFC 或 C 運行庫斷言而暫停時,它定位到源文件中的斷言發生點(如果源可用)。 斷言消息顯示在“輸出”窗口以及“斷言失敗”對話框中。 如果希望保存斷言消息以供將來參考,可以將斷言消息從“輸出”窗口復制到某個文本窗口。 “輸出”窗口可能還包含其他錯誤信息。 請仔細檢查這些消息,因為它們提供了有關確定斷言失敗原因的線索。
通過在代碼中大量使用斷言,可以在開發期間捕捉許多錯誤。 為所做的每個假定編寫一個斷言是很好的規則。 例如,如果假定某個參數不為 NULL,請使用一條斷言語句檢查該假定。
_DEBUG
僅當定義了 _DEBUG 時斷言語句才編譯。 未定義 _DEBUG 時,編譯器將斷言作為空語句處理。 因此,斷言語句在最終發布程序中系統開銷為零;可以在代碼中大量使用斷言語句,而不影響“Release”版本的性能,並且不必使用 #ifdef 指令。
使用斷言的副作用
當向代碼添加斷言時,請確保這些斷言沒有副作用。 例如,考慮以下斷言:
ASSERT(nM++ > 0); // Don't do this!
因為在程序的“Release”版本中不計算 ASSERT 表達式,所以 nM 在“Debug”版本和“Release”版本中會有不同值。 在 MFC 中,可以使用 VERIFY 宏代替 ASSERT。 在“Release”版本中,VERIFY 計算該表達式,但不檢查結果。
在斷言語句中使用函數調用時應特別小心,因為計算函數可能會有意外的副作用。
ASSERT ( myFnctn(0)==1 ) // unsafe if myFnctn has side effects
VERIFY ( myFnctn(0)==1 ) // safe
VERIFY 在“Debug”版本和“Release”版本中都調用 myFnctn,因此可以使用它。 但在“Release”版本中仍會有因不必要的函數調用而產生的系統開銷。
調試內聯匯編代碼
調試器提供了兩個用於調試內聯程序集代碼的窗口,即“反匯編”窗口和“寄存器”窗口。
調試內聯程序集代碼
-
使用“反匯編”窗口查看程序集指令。


-
使用“寄存器”窗口查看寄存器內容。

MFC 調試技術
AfxDebugBreak
MFC 提供特殊的 AfxDebugBreak 函數,以供在源代碼中對斷點進行硬編碼:
AfxDebugBreak( );
在 Intel 平台上,AfxDebugBreak 將生成以下代碼,它在源代碼而不是內核代碼中中斷:
_asm int 3
在其他平台上,AfxDebugBreak 僅調用 DebugBreak。
確保在創建發布版本時移除 AfxDebugBreak 語句,或使用 #ifdef _DEBUG 環繞這些語句。
TRACE 宏
若要在調試器的“輸出”窗口中顯示來自程序的消息,可以使用 ATLTRACE 宏或 MFC TRACE 宏。 與斷言類似,跟蹤宏只在程序的“Debug”版本中起作用,在“Release”版本中編譯時將消失。
下面的示例顯示幾種 TRACE 宏的用法。 與 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 宏可正確處理 char* 參數和 wchar_t* 參數。 下面的示例說明如何將 TRACE 宏與不同字符串參數類型配合使用。
TRACE( "This is a test of the TRACE macro that uses an ANSI string: %s %d\n", "The number is:", 2);
TRACE( L"This is a test of the TRACE macro that uses a UNICODE string: %s %d\n", L"The number is:", 2);
TRACE( _T("This is a test of the TRACE macro that uses a TCHAR string: %s %d\n"), _T("The number is:"), 2);
CRT 調試技術
使用CRT 調試庫
C 運行庫提供廣泛的調試支持。 若要使用 CRT 調試庫之一,必須鏈接 /DEBUG,並用 /MDd、/MTd 或 /LDd 編譯。CRT 調試的主要定義和宏可在 CRTDBG.h 頭文件中找到。
CRT 調試庫中的函數編譯時帶有調試信息(/Z7、/Zd、/Zi、/ZI(調試信息格式)),不進行優化。 某些函數包含斷言以驗證傳遞給它們的參數,並且提供源代碼。 使用此類源代碼,可以單步執行 CRT 函數,以確認這些函數按預期方式工作並檢查錯誤的參數或內存狀態。 (某些 CRT 技術是專有技術,不提供用於異常處理、浮點和少數其他例程的源代碼。)
安裝 Visual C++ 時,可以選擇在硬盤上安裝 C 運行庫源代碼。 如果不安裝源代碼,將需要 CD-ROM 才能單步執行 CRT 函數。
用於報告的宏
可以使用在 CRTDBG.H 中定義的 _RPTn 和 _RPTFn 宏替換 printf 語句進行調試。 未定義 _DEBUG 時,這些宏在發布版本中自動消失,因此不必將它們括在 #ifdef 內。
| 宏 |
函數 |
|---|---|
| _RPT0, _RPT1, _RPT2, _RPT3, _RPT4 |
向四個參數輸出一個消息字符串和零。 對於從 _RPT1 到 _RPT4,消息字符串作為參數的 printf 樣式的格式化字符串。 |
| _RPTF0、_RPTF1、_RPTF2、_RPTF4 |
與 _RPTn 相同,但這些宏還輸出其所在的文件名和行號。 |
請看下面的示例:
#ifdef _DEBUG
if ( someVar > MAX_SOMEVAR )
printf( "OVERFLOW! In NameOfThisFunc( ),
someVar=%d, otherVar=%d.\n",
someVar, otherVar );
#endif
該代碼將 someVar 和 otherVar 的值輸出到 stdout。 可以使用以下對 _RPTF2 的調用報告同樣的值另加文件名和行號:
if (someVar > MAX_SOMEVAR) _RPTF2(_CRT_WARN, "In NameOfThisFunc( ), someVar= %d, otherVar= %d\n", someVar, otherVar );
如果發現某特定應用程序需要調試報告,而 C 運行庫提供的宏不提供該報告,則可以編寫專門設計的宏來符合您自己的要求。 例如,可以在其中一個頭文件中包含以下代碼來定義名為 ALERT_IF2 的宏:
#ifndef _DEBUG /* For RELEASE builds */
#define ALERT_IF2(expr, msg, arg1, arg2) do {} while (0)
#else /* For DEBUG builds */
#define ALERT_IF2(expr, msg, arg1, arg2) \
do { \
if ((expr) && \
(1 == _CrtDbgReport(_CRT_ERROR, \
__FILE__, __LINE__, msg, arg1, arg2))) \
_CrtDbgBreak( ); \
} while (0)
#endif
對 ALERT_IF2 的一個調用可以執行本主題開始處的 printf 代碼的所有函數:
ALERT_IF2(someVar > MAX_SOMEVAR, "OVERFLOW! In NameOfThisFunc( ),
someVar=%d, otherVar=%d.\n", someVar, otherVar );
因為可以方便地更改自定義宏,以便向不同目標報告或多或少的信息(取決於怎樣更方便),所以該方法在調試要求不斷發展時尤其有用。
堆分配函數的“Debug”版本
C 運行庫包含堆分配函數的特殊“Debug”版本。 這些函數的名稱與發行版本相同,只是追加了“_dbg”。 本主題用 malloc 和 _malloc_dbg 作為示例,描述 CRT 函數的發行版本和 _dbg 版本之間的差異。
定義 _DEBUG 后,CRT 會將所有 malloc 調用映射到 _malloc_dbg。 因此,不需要用 _malloc_dbg 代替 malloc 來重寫代碼以獲得調試時的好處。
但您可能希望顯式調用 _malloc_dbg。 顯式調用 _malloc_dbg 具有一些附加的好處:
-
跟蹤 _CLIENT_BLOCK 類型分配。
-
存儲分配請求所在的源文件和行號。
如果不希望將 malloc 調用轉換為 _malloc_dbg,可以通過定義 _CRTDBG_MAP_ALLOC 來獲取源文件信息,而這導致預處理器將對 malloc 的所有調用直接映射到 _malloc_dbg,而不是依賴 malloc 周圍的包裝。
若要跟蹤客戶端塊中各種類型的分配,必須直接調用 _malloc_dbg,並將 blockType 參數設置為 _CLIENT_BLOCK。
未定義 _DEBUG 時,對 malloc 的調用將不受妨礙,並且對 _malloc_dbg 的調用將被解析為 malloc,忽略 _CRTDBG_MAP_ALLOC 的定義,並且不提供與分配請求有關的源文件信息。 因為 malloc 沒有塊類型參數,所以將對 _CLIENT_BLOCK 類型的請求作為標准分配處理。
調試本機 DLL
當調試 DLL 時,可以從以下開始調試:
- 用於創建調用 DLL 的可執行文件的項目。
- 或 -
- 用於創建 DLL 本身的項目。
如果有用於創建可執行文件的項目,則從該項目開始調試。 然后可以打開 DLL 的源文件,並在該文件中設置斷點,即使它不是用於創建可執行文件的項目的一部分。 有關更多信息,請參見斷點。
如果從創建 DLL 的項目開始調試,則必須指定在調試 DLL 時要使用的可執行文件。
為調試會話指定可執行文件
-
在“解決方案資源管理器”中,選擇用於創建 DLL 的項目。
-
從“視圖”菜單中,選定“屬性頁”。
-
在“屬性頁”對話框中,打開“配置屬性”文件夾並選擇“調試”類別。
-
在“命令”框中,指定用於容器的路徑名稱。 例如,C:\Program Files\MyApplication\MYAPP.EXE。
-
在“命令參數”框中,指定用於可執行文件的必要參數。
如果不在“項目 屬性頁”對話框中指定可執行文件,則在開始調試時將出現“調試會話的可執行文件”對話框。
調試注入的代碼
使用Attributes 可以大大簡化C++編程。一些Attributes由編譯器直接解釋。其他Attributes將代碼注入程序源代碼,然后由編譯器編譯。這段注入的代碼通過減少必須編寫的代碼量使編程變得更容易。但是,有時錯誤可能會導致應用程序在執行注入的代碼時失敗。當這種情況發生時,您可能需要查看注入的代碼。Visual Studio提供了兩種查看注入代碼的方法:
- 您可以在反匯編窗口中查看注入的代碼。
- 使用/Fx,可以創建包含原始代碼和注入代碼的合並源文件。
“反匯編”窗口顯示與源代碼和屬性注入的代碼相對應的匯編語言指令。此外,反匯編窗口可以顯示源代碼注釋。
打開源注釋
在“反匯編”窗口上單擊鼠標右鍵,然后從快捷菜單中選擇“顯示源代碼”。
如果知道源窗口中屬性的位置,可以使用快捷菜單在反匯編窗口中查找注入的代碼。
查看插入的代碼
-
調試器必須處於中斷模式。
-
在源代碼窗口中,將光標放在要查看其注入代碼的特性前面。
-
右擊並從快捷菜單中選定“轉到反匯編”。
如果特性位置在當前執行點附近,則可以從“調試”菜單選擇“反匯編”窗口。
查看當前執行點處的反匯編代碼
-
調試器必須處於中斷模式。
-
從“調試”菜單中選擇“窗口”,然后單擊“反匯編”。
