簡(jiǎn)單介紹
調(diào)試是程序開(kāi)發(fā)者必備技巧。如果不會(huì)調(diào)試,自己寫的程序一旦出問(wèn)題,往往無(wú)從下手。本人總結(jié)10年使用VC經(jīng)驗(yàn),對(duì)調(diào)試技巧做一個(gè)粗淺的介紹。希望對(duì)大家有所幫助。
今天簡(jiǎn)單的介紹介紹調(diào)用堆棧。調(diào)用堆棧在我的專欄的文章VC調(diào)試入門提了一下,但是沒(méi)有詳細(xì)介紹。
首先介紹一下什么叫調(diào)用堆棧:假設(shè)我們有幾個(gè)函數(shù),分別是function1,function2,function3,funtion4,且function1調(diào)用function2,function2調(diào)用function3,function3調(diào)用function4。在function4運(yùn)行過(guò)程中,我們可以從線程當(dāng)前堆棧中了解到調(diào)用他的那幾個(gè)函數(shù)分別是誰(shuí)。把函數(shù)的順序關(guān)系看,function4、function3、function2、function1呈現(xiàn)出一種“堆?!钡奶卣鳎詈蟊徽{(diào)用的函數(shù)出現(xiàn)在最上方。因此稱呼這種關(guān)系為調(diào)用堆棧(call stack)。
當(dāng)故障發(fā)生時(shí),如果程序被中斷,我們基本上只可以看到最后出錯(cuò)的函數(shù)。利用call stack,我們可以知道當(dāng)出錯(cuò)函數(shù)被誰(shuí)調(diào)用的時(shí)候出錯(cuò)。這樣一層層的看上去,有時(shí)可以猜測(cè)出錯(cuò)誤的原因。常見(jiàn)的這種中斷時(shí)ASSERT宏導(dǎo)致的中斷。
在程序被中斷時(shí),debug工具條的右側(cè)倒數(shù)第二個(gè)按鈕一般是call stack按鈕,這個(gè)按鈕被按下后,你就可以看到當(dāng)前的調(diào)用堆棧。
實(shí)例一:介紹
我們首先演示一下調(diào)用堆棧。首先我們創(chuàng)建一個(gè)名為Debug的對(duì)話框工程。工程創(chuàng)建好以后,雙擊OK按鈕創(chuàng)建消息映射函數(shù),并添加如下代碼:
void CDebugDlg::OnOK()
{
// TODO: Add extra validation here
ASSERT(FALSE);
}
我們按F5開(kāi)始調(diào)試程序。程序運(yùn)行后,點(diǎn)擊OK按鈕,程序就會(huì)被中斷。這時(shí)查看call stack窗口,就會(huì)發(fā)現(xiàn)內(nèi)容如下:
CDebugDlg::OnOK() line 176 + 34 bytes
_AfxDispatchCmdMsg(CCmdTarget * 0x0012fe74 {CDebugDlg}, unsigned int 1, int 0, void (void)* 0x5f402a00 `vcall'(void), void * 0x00000000, unsigned int 12, AFX_CMDHANDLERINFO * 0x00000000) line 88
CCmdTarget::OnCmdMsg(unsigned int 1, int 0, void * 0x00000000, AFX_CMDHANDLERINFO * 0x00000000) line 302 + 39 bytes
CDialog::OnCmdMsg(unsigned int 1, int 0, void * 0x00000000, AFX_CMDHANDLERINFO * 0x00000000) line 97 + 24 bytes
CWnd::OnCommand(unsigned int 1, long 656988) line 2088
CWnd::OnWndMsg(unsigned int 273, unsigned int 1, long 656988, long * 0x0012f83c) line 1597 + 28 bytes
CWnd::WindowProc(unsigned int 273, unsigned int 1, long 656988) line 1585 + 30 bytes
AfxCallWndProc(CWnd * 0x0012fe74 {CDebugDlg hWnd=???}, HWND__ * 0x001204b0, unsigned int 273, unsigned int 1, long 656988) line 215 + 26 bytes
AfxWndProc(HWND__ * 0x001204b0, unsigned int 273, unsigned int 1, long 656988) line 368
AfxWndProcBase(HWND__ * 0x001204b0, unsigned int 273, unsigned int 1, long 656988) line 220 + 21 bytes
USER32! 77d48709()
USER32! 77d487eb()
USER32! 77d4b368()
USER32! 77d4b3b4()
NTDLL! 7c90eae3()
USER32! 77d4b7ab()
USER32! 77d7fc9d()
USER32! 77d76530()
USER32! 77d58386()
USER32! 77d5887a()
USER32! 77d48709()
USER32! 77d487eb()
USER32! 77d489a5()
USER32! 77d489e8()
USER32! 77d6e819()
USER32! 77d65ce2()
CWnd::IsDialogMessageA(tagMSG * 0x004167d8 {msg=0x00000202 wp=0x00000000 lp=0x000f001c}) line 182
CWnd::PreTranslateInput(tagMSG * 0x004167d8 {msg=0x00000202 wp=0x00000000 lp=0x000f001c}) line 3424
CDialog::PreTranslateMessage(tagMSG * 0x004167d8 {msg=0x00000202 wp=0x00000000 lp=0x000f001c}) line 92
CWnd::WalkPreTranslateTree(HWND__ * 0x001204b0, tagMSG * 0x004167d8 {msg=0x00000202 wp=0x00000000 lp=0x000f001c}) line 2667 + 18 bytes
CWinThread::PreTranslateMessage(tagMSG * 0x004167d8 {msg=0x00000202 wp=0x00000000 lp=0x000f001c}) line 665 + 18 bytes
CWinThread::PumpMessage() line 841 + 30 bytes
CWnd::RunModalLoop(unsigned long 4) line 3478 + 19 bytes
CDialog::DoModal() line 536 + 12 bytes
CDebugApp::InitInstance() line 59 + 8 bytes
AfxWinMain(HINSTANCE__ * 0x00400000, HINSTANCE__ * 0x00000000, char * 0x00141f00, int 1) line 39 + 11 bytes
WinMain(HINSTANCE__ * 0x00400000, HINSTANCE__ * 0x00000000, char * 0x00141f00, int 1) line 30
WinMainCRTStartup() line 330 + 54 bytes
KERNEL32! 7c816d4f()
這里,CDebugDialog::OnOK作為整個(gè)調(diào)用鏈中最后被調(diào)用的函數(shù)出現(xiàn)在call stack的最上方,而內(nèi)核中程序的啟動(dòng)函數(shù)Kernel32! 7c816d4f()則作為棧底出現(xiàn)在最下方。
實(shí)例二:學(xué)習(xí)處理方法
微軟提供了MDI/SDI模型提供文檔處理的建議結(jié)構(gòu)。有些時(shí)候,大家希望控制某個(gè)環(huán)節(jié)。例如,我們希望彈出自己的打開(kāi)文件對(duì)話框,但是并不想自己實(shí)現(xiàn)整個(gè)文檔的打開(kāi)過(guò)程,而更愿意MFC完成其他部分的工作??墒牵覀儾⒉磺宄﨧FC是怎么處理文檔的,也不清楚如何插入自定義代碼。
幸運(yùn)的是,我們知道當(dāng)一個(gè)文檔被打開(kāi)以后,系統(tǒng)會(huì)調(diào)用CDocument派生類的Serialize函數(shù),我們可以利用這一點(diǎn)來(lái)跟蹤MFC的處理過(guò)程。
我們首先創(chuàng)建一個(gè)缺省的SDI工程Test1,并在CTest1Doc::Serialize函數(shù)的開(kāi)頭增加一個(gè)斷點(diǎn),運(yùn)行程序,并打開(kāi)一個(gè)文件。這時(shí),我們可以看到調(diào)用堆棧是(我只截取了感興趣的一段):
CTest1Doc::Serialize(CArchive & {...}) line 66
CDocument::OnOpenDocument(const char * 0x0012f54c) line 714
CSingleDocTemplate::OpenDocumentFile(const char * 0x0012f54c, int 1) line 168 + 15 bytes
CDocManager::OpenDocumentFile(const char * 0x0042241c) line 953
CWinApp::OpenDocumentFile(const char * 0x0042241c) line 93
CDocManager::OnFileOpen() line 841
CWinApp::OnFileOpen() line 37
_AfxDispatchCmdMsg(CCmdTarget * 0x004177f0 class CTest1App theApp, unsigned int 57601, int 0, void (void)* 0x00402898 CWinApp::OnFileOpen, void * 0x00000000, unsigned int 12, AFX_CMDHANDLERINFO * 0x00000000) line 88
CCmdTarget::OnCmdMsg(unsigned int 57601, int 0, void * 0x00000000, AFX_CMDHANDLERINFO * 0x00000000) line 302 + 39 bytes
CFrameWnd::OnCmdMsg(unsigned int 57601, int 0, void * 0x00000000, AFX_CMDHANDLERINFO * 0x00000000) line 899 + 33 bytes
CWnd::OnCommand(unsigned int 57601, long 132158) line 2088
CFrameWnd::OnCommand(unsigned int 57601, long 132158) line 317
從上面的調(diào)用堆棧看,這個(gè)過(guò)程由一個(gè)WM_COMMAND消息觸發(fā)(因?yàn)槲覀冇貌藛未蜷_(kāi)文件),由CWinApp::OnFileOpen最先開(kāi)始實(shí)際處理過(guò)程,這個(gè)函數(shù)調(diào)用CDocManager::OnFileOpen打開(kāi)文檔。
我們首先雙擊CWinApp::OnFileOpen() line 37打開(kāi)CWinApp::OnFileOpen,它的處理過(guò)程是:
ASSERT(m_pDocManager != NULL);
m_pDocManager->OnFileOpen();
m_pDocManager是一個(gè)CDocManager類的實(shí)例指針,我們雙擊CDocManager::OnFileOpen行,看該函數(shù)的實(shí)現(xiàn):
void CDocManager::OnFileOpen()
{
// prompt the user (with all document templates)
CString newName;
if (!DoPromptFileName(newName, AFX_IDS_OPENFILE,
OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, TRUE, NULL))
return; // open cancelled
AfxGetApp()->OpenDocumentFile(newName);
// if returns NULL, the user has already been alerted
}
很顯然,該函數(shù)首先調(diào)用DoPromptFileName函數(shù)來(lái)獲得一個(gè)文件名,然后在繼續(xù)后續(xù)的打開(kāi)過(guò)程。
順這這個(gè)線索下去,我們一定能找到插入我們文件打開(kāi)對(duì)話框的位置。由于這不是我們研究的重點(diǎn),后續(xù)的分析我就不再詳述。
實(shí)例三:內(nèi)存訪問(wèn)越界
在Debug版本的VC程序中,程序會(huì)給每塊new出來(lái)的內(nèi)存,預(yù)留幾個(gè)字節(jié)作為越界檢測(cè)之用。在釋放內(nèi)存時(shí),系統(tǒng)會(huì)檢查這幾個(gè)字節(jié),判斷是否有內(nèi)存訪問(wèn)越界的可能。
我們借用前一個(gè)實(shí)例程序,在CTest1App::InitInstance的開(kāi)頭添加以下幾行代碼:
char * p = new char[10];
memset(p,0,100);
delete []p;
return FALSE;
很顯然,這段代碼申請(qǐng)了10字節(jié)內(nèi)存,但是使用了100字節(jié)。我們?cè)趍emset(p,0,100);這行加一個(gè)斷點(diǎn),然后執(zhí)行程序,斷點(diǎn)到達(dá)后,我們觀察p指向的內(nèi)存的值(利用Debug工具條的Memory功能),可以發(fā)現(xiàn)它的值是:
CD CD CD CD CD CD CD CD
CD CD FD FD FD FD FD FD
00 00 00 00 00 00 00 00
......
根據(jù)經(jīng)驗(yàn),p實(shí)際被分配了16個(gè)字節(jié),后6個(gè)字節(jié)用于保護(hù)。我們按F5全速執(zhí)行程序,會(huì)發(fā)現(xiàn)如下的錯(cuò)誤信息被彈出:
Debug Error!
Program: c:\temp\test1\Debug\test1.exe
DAMAGE: after normal block (#55) at 0x00421AB0
Press Retry to debug the application
該信息提示,在正常內(nèi)存塊0x00421AB0后的內(nèi)存被破壞(內(nèi)存訪問(wèn)越界),我們點(diǎn)擊Retry進(jìn)入調(diào)試狀態(tài),發(fā)現(xiàn)調(diào)用堆棧是:
_free_dbg_lk(void * 0x00421ab0, int 1) line 1033 + 60 bytes
_free_dbg(void * 0x00421ab0, int 1) line 970 + 13 bytes
operator delete(void * 0x00421ab0) line 351 + 12 bytes
CTest1App::InitInstance() line 54 + 15 bytes
很顯然,這個(gè)錯(cuò)誤是在調(diào)用delete時(shí)遇到的,出現(xiàn)在CTest1App::InitInstance() line 54 + 15 bytes之處。我們很容易根據(jù)這個(gè)信息找到,是在釋放哪塊內(nèi)存時(shí)出現(xiàn)問(wèn)題,之后,我們只需要根據(jù)這個(gè)內(nèi)存的訪問(wèn)過(guò)程確定哪兒出錯(cuò),這將大大降低調(diào)試的難度。
實(shí)例四:子類化
子類化是我們修改一個(gè)現(xiàn)有控件實(shí)現(xiàn)新功能的常用方法,我們借用實(shí)例一中的Debug對(duì)話框工程來(lái)演示我過(guò)去學(xué)習(xí)子類化的一個(gè)故事。我們創(chuàng)建一個(gè)缺省的名為Debug的對(duì)話框工程,并按照下列步驟進(jìn)行實(shí)例化:
- 在對(duì)話框資源中增加一個(gè)Edit控件
- 用class wizard為CEdit派生一個(gè)類CMyEdit(由于今天不關(guān)心子類化的具體細(xì)節(jié),因此這個(gè)類不作任何修改)
- 為Edit控件,增加一個(gè)控件類型變量m_edit,其類型為CMyEdit
- 在OnInitDialog中增加如下語(yǔ)句:
m_edit.SubclassDlgItem(IDC_EDIT1,this);
我們運(yùn)行這個(gè)程序,會(huì)遇到這樣的錯(cuò)誤:
Debug Assertion Failed!
Application:C:\temp\Debug\Debug\Debug.exe
File:Wincore.cpp
Line:311
For information on how your program can cause an assertion failure, see Visual C++ documentation on asserts.
(Press Retry to debug the application)
點(diǎn)擊Retry進(jìn)入調(diào)試狀態(tài),我們可以看到調(diào)用堆棧為:
CWnd::Attach(HWND__ * 0x000205a8) line 311 + 28 bytes
CWnd::SubclassWindow(HWND__ * 0x000205a8) line 3845 + 12 bytes
CWnd::SubclassDlgItem(unsigned int 1000, CWnd * 0x0012fe34 {CDebugDlg hWnd=0x001d058a}) line 3883 + 12 bytes
CDebugDlg::OnInitDialog() line 120
可以看出在Attach句柄時(shí)出現(xiàn)問(wèn)題,出問(wèn)題行的代碼為:
ASSERT(m_hWnd == NULL);
這說(shuō)明我們?cè)谧宇惢瘯r(shí)不應(yīng)該綁定控件,我們刪除CDebugDialog::DoDataExchange中的下面一行:
DDX_Control(pDX, IDC_EDIT1, m_edit);
問(wèn)題就得到解決
總結(jié)
簡(jiǎn)而言之,call stack是調(diào)試中必須掌握的一個(gè)技術(shù),但是程序員需要豐富的經(jīng)驗(yàn)才能很好的掌握和使用它。你不僅僅需要熟知C++語(yǔ)法,還需要對(duì)相關(guān)的平臺(tái)、軟件設(shè)計(jì)思路有一定的了解。我的文章只能算一個(gè)粗淺的介紹,畢竟我在這方面也不算高手。希望對(duì)新進(jìn)有一定的幫助。