Inside VC debug
本文內容來源於我在2004年給所在部門的新員工做的一次內部培訓。在對新員工的培訓過程中,發現對於新員工來說,在進入工作崗位後,關注編程的技巧比較多,而對於VC/Windows環境下的程序調試,以及相關工具的使用掌握的不好。從我自己學習的過程來看,調試和工具的使用主要靠長期的積累和摸索,相關的資料非常少,部分調試技術,對於工作多年的老員工來說,可能也從來沒有接觸過。下面我就將自己在日常工作中使用的一些調試經驗作一些介紹,希望對大家有所幫助。
一、 調試
1. 基本技巧
1) 斷點的設置與跟蹤
a) 普通斷點設置 F9
b) 斷點進入條件(Ctrl+B)
2) 跟蹤
Step Into 進入函數、模塊調試
Step Over 不進入函數調試
Step Out 從函數中返回
Run to Cursor 運行到鼠標所在行
3) 察看數據
a) Watch
可以自由添加需要查看的變量
b) Call Stack
運行堆棧,對出錯(紅叉)時的錯誤定位非常有效
c) Memory
內存查看器。主要用於查看內存塊內的數據。圖中查看了b變量在內存中的值(第一個字節0A爲變量b的值,即10)
d) Variables
變量查看器。自動顯示當前代碼用到的變量的值。
e) Registers
寄存器查看器。用於查看寄存器內的數據值。用於底層代碼調試。
f) Disassembly
查看彙編代碼。
下面爲函數aa()的彙編代碼
8: void aa()
9: {
00401000 push ebp
00401001 mov ebp,esp
00401003 sub esp,48h
00401006 push ebx
00401007 push esi
00401008 push edi
00401009 lea edi,[ebp-48h]
0040100C mov ecx,12h
00401011 mov eax,0CCCCCCCCh
00401016 rep stos dword ptr [edi]
10: for(int i=0;i<100000;i++)
00401018 mov dword ptr [ebp-4],0
0040101F jmp aa+2Ah (0040102a)
00401021 mov eax,dword ptr [ebp-4]
00401024 add eax,1
00401027 mov dword ptr [ebp-4],eax
0040102A cmp dword ptr [ebp-4],186A0h
00401031 jge aa+3Bh (0040103b)
11: {
12: int b=i;
00401033 mov ecx,dword ptr [ebp-4]
00401036 mov dword ptr [b],ecx
13: }
00401039 jmp aa+21h (00401021)
14: }
4) 高級技巧
a) 僞寄存器
可以直接在Watch窗口中查看,特別是前2個,可以大大減少GetLastError的使用,免於調試過程中反覆改動代碼。
@hr 最後出錯編號。等於GetLastError()
@hr,err 最後出錯編號的文字描述
@EIP 指令寄存器
@寄存器名稱 對應的寄存器
b) 逆向運行
X86 CPU運行時,當前運行代碼所對應的地址保存於IP寄存器,32位CPU保存在EIP,所以,我們可以理論上可以依靠修改EIP的值來讓我們的代碼跳轉到任意代碼行(考慮到函數調用時的參數需要壓棧,如果要跨函數恢復到完全一樣運行環境,比較困難,所以在本函數體內跳轉比較合適,超出函數範圍就不建議使用本技巧了)
實例:
程序運行情況如下圖。
1. 當前運行到第23行代碼a=2。
2. 我們可以在編輯區和Watch的@EIP僞寄存器中看到23行代碼對應的內存地址是0x401096。
3. 現在我們想讓代碼跳轉到0x0040108F,讓程序再運行一次22行的代碼。那我們只需要將@EIP值改爲0x0040108F。
4.在下圖中,我們可以看到代碼被反向運行了。此技巧主要用於反覆運行同一行代碼或反覆調用同一個函數。
c) 性能測試(Profilling)
可以統計每個函數在運行期間被調用了多少次,佔用了多少CPU百分比。
但是此功能在VC6中不是很穩定,經常無法運行。另外,BoundsCheck的最新版本中,增加了Performance測試,可以進行函數和代碼行級的性能統計。所以推薦使用BoundsCheck來進行測試。(7.0以上版本才包含此功能,但是由於版權問題,不能大範圍使用,可以使用專用電腦來進行性能統計測試)
d) 遠程調試(Debugger remote connection)
需要設置:
1.對方IP
2.附加DLL
要求2臺調試機器的操作系統(包括補丁)版本完全一樣。否則無法調試。一般用於調試圖形程序等調試器(VC)會對代碼運行產生影響的程序。
2. 錯誤定位
1) 基本技巧
開發機:運行碰到錯誤。
調試運行。碰到紅叉後根據Stack信息定位到調試代碼具體位置。
2) 高級技巧
測試機、用戶機器:運行錯誤。
有代碼行數時:直接調出源代碼查看。
只有出錯內存地址: 可以使用.map文件定位錯誤函數即行數。在大型程序中,往往不能提供完備的.map文件,無法定位到代碼行數或函數。這個時候可以使用ProcessInfo工具查看程序模塊映射關係來推斷出錯模塊。
例如:在ATNotes.exe運行過程中出錯,出錯地址爲 7C920100,則根據上圖,可以發現出錯地址位於ntdll.dll模塊中,因爲ntdll.dll的基地址(BaseAddr)等於7C920000,模塊所佔內存大小爲94000,說明7C920000~7D9B400爲ntdll.dll的代碼段。所以可以定位此模塊出錯。
只有出錯框,沒有顯示是哪個程序出錯。有時候會有多個程序同時運行,當出錯時,如果出錯框沒有顯示程序名稱,很難定位到底哪個程序出錯了。
使用Spy++查看對話框信息,獲得進程PID
打開進程管理器,查看進程和PID對應關係
可以發現出錯對話框屬於TestESP(8B0 = 2224)
3. 工具
1) BoundsCheck
主要提供詳細的調試信息。常用來檢查內存、資源泄漏。
2) Process Explorer
主要用來查看進程詳細信息,如進程加載了哪些DLL等。
3) VC Tools
VC所帶的工具,常用的有:
SPY++:查看窗口信息
Dependency:查看DLL接口
OleView:查看COM組件信息
TestContainer:用於調試控件
ErrorLookup: 用於查看出錯代碼對應的錯誤說明
4) DebugView
用於查看TRACE輸出的調試信息,目前被軟件開發部門大量使用。
5) SoftIce/Ollydbg
這2個工具功能非常強大,在調試中,主要用於已經在運行的程序調試。可以在指定的函數入口設置斷點(包括系統API)。當函數被調用時,就可以進入斷點開始彙編級的跟蹤。
舉例:在一次程序故障中,發現某臺計算機中的程序出現運行不正常情況,但是出現概率極小,同時,在不穩定時,不允許退出程序進行代碼調試,最後,經過分析,在開發機上大致定位了出錯可能的範圍,然後在出錯機器上使用Ollydbg,定位到了可能出錯的代碼區,加入斷點,跟蹤運行期的彙編代碼,最後查明瞭故障原因。
OllyDbg
本節涉及到的工具可以在我的ftp下載:ftp://10.10.20.60/tools
二、 單元測試
1. DLL、OCX模塊介紹
1) DLL的類型:
常規DLL:生成時自動生成CwinApp類,幫助管理DLL
擴展DLL:沒有生成CwinApp,DLL入口主要依靠DLLMain的DLL_PROCESS_ATTACH
和DLL_PROCESS_DETACH消息
2) DLL的常用加載方法:
靜態加載
VC工程設置加載(命令行加載)
代碼加載
#pragma comment (lib,"vfprojectd.lib")
動態加載
調用ci_r_IsServer()函數
HINSTANCE hDLLHandle = AfxLoadLibrary (“vfprojectd.dll”)
if(hDLLHandle)
{
typedef BOOL (*CI_R_ISSERVER)(void);
CI_R_ISSERVER ci_r_IsServer = (CI_R_ISSERVER)GetProcAddress(hTempDll,"ci_r_IsServer");
if(ci_r_IsServer)
{
m_bSelfDS = ci_r_IsServer();
}
FreeLibrary(hDLLHandle);
}
3) OCX常用調試方法:
Ocx主要使用TestContainer或VB調試,不建議使用VC
2. DLL單元測試舉例
模塊名稱:VFProjectD.dll
頭文件:OtherUseDll.h
庫文件:VFProjectD.lib
加載方式:靜態加載
VFProject共有10個接口函數。
對一個接口的測試,需要包含:邊界測試、中值測試、隨機數測試、非法數據測試
/*******************************************************************
*
* 函數名稱: CTestVFProjectDlg::OnTest1()
* 描 述: test FileDlg()
* 創 建: 何軍[2006-4-20 13:15:32]
*
* 返 回: [void] -
*
* 函數參數 :
*
*******************************************************************/
void CTestVFProjectDlg::OnTest1()
{
// open
CString pathn,filen;
FileDlg(TRUE,"pic","pic",pathn,filen);
Trace("Test1.1 Open Dialog:pathname=%s,filename=%s/n",pathn,filen);
// save
FileDlg(FALSE,"pic","pic",pathn,filen);
Trace("Test1.2 Save Dialog:pathname=%s,filename=%s/n",pathn,filen);
// 非法參數
FileDlg(TRUE,"","",pathn,filen);
Trace("Test1.3 Save Dialog:pathname=%s,filename=%s/n",pathn,filen);
FileDlg(FALSE,"","",pathn,filen);
Trace("Test1.4 Save Dialog:pathname=%s,filename=%s/n",pathn,filen);
}
測試結果
一些補充:
Go命令(F5):向下執行程序直到斷點,或結束;
Restart命令(Ctrl+Shift+F5):重新開始調試,直到第一個斷點或結束;
Stop Debugging(Shift+F5):結束調試工作,返回原工作環境;
Break:停止調試;
Step Into命令(F11):單步向下運行,該命令可以深入到Call調用的函數裏,
執行每一步彙編指令,此時可以打開Registers窗口,觀察各寄存器的變化;
Step Over命令(F10): 單步狀態結束;
Step Out(Shift+F11):從Call函數中跳出來;
Run to Cursor(Ctrl+F10):執行到光標處;
Step Into Specific Function :單步執行特殊的功能;
Exception命令:將列出異常中斷程序執行的情況,可以進行修改;
Threads命令:將列出被調試程序中的線程信息,包括線程的標號、名稱、地址、
優先級等。
Show Next Statement命令:將當前正在調試的語句標識出來
Quick Watch:快速觀察的功能。