在計算機科學中,Callstack 是指存放某個程序的正在運行的函數的信息的棧。Call stack 由stack frames 組成,每個stack frame 對應於一個未完成運行的函數。
在當今流行的計算機體系架構中,大部分計算機的參數傳遞,局部變量的分配和釋放都是通過操縱程序棧來實現的。棧用來傳遞函數參數,存儲返回值信息,保存寄存器以供恢復調用前處理機狀態。每次調用一個函數,都要為該次調用的函數實例分配棧空間。為單個函數分配的那部分棧空間就叫做 stack frame,也就是說,stack frame 這個說法主要是為了描述函數調用關系的。
Stackframe 組織方式的重要性和作用體現在兩個方面:第一,它使調用者和被調用者達成某種約定。這個約定定義了函數調用時函數參數的傳遞方式,函數返回值的返回方式,寄存器如何在調用者和被調用者之間進行共享;第二,它定義了被調用者如何使用它自己的 stack frame 來完成局部變量的存儲和使用。
簡單介紹
調試是程序開發者必備技巧。如果不會調試,自己寫的程序一旦出問題,往往無從下手。本人總結10年使用VC經驗,對調試技巧做一個粗淺的介紹。希望對大家有所幫助。
今天簡單的介紹介紹調用堆棧。調用堆棧在我的專欄的文章VC調試入門提了一下,但是沒有詳細介紹。
首先介紹一下什麼叫調用堆棧:假設我們有幾個函數,分別是function1,function2,function3,funtion4,且function1調用function2,function2調用function3,function3調用function4。在function4運行過程中,我們可以從線程當前堆棧中了解到調用他的那幾個函數分別是誰。把函數的順序關系看,function4、function3、function2、function1呈現出一種“堆棧”的特征,最後被調用的函數出現在最上方。因此稱呼這種關系為調用堆棧(callstack)。
當故障發生時,如果程序被中斷,我們基本上只可以看到最後出錯的函數。利用call stack,我們可以知道當出錯函數被誰調用的時候出錯。這樣一層層的看上去,有時可以猜測出錯誤的原因。常見的這種中斷時ASSERT宏導致的中斷。
在程序被中斷時,debug工具條的右側倒數第二個按鈕一般是callstack按鈕,這個按鈕被按下後,你就可以看到當前的調用堆棧。
實例一:介紹
我們首先演示一下調用堆棧。首先我們創建一個名為Debug的對話框工程。工程創建好以後,雙擊OK按鈕創建消息映射函數,並添加如下代碼:
void CDebugDlg::OnOK()
{
// TODO: Add extravalidation here
ASSERT(FALSE);
}
我們按F5開始調試程序。程序運行後,點擊OK按鈕,程序就會被中斷。這時查看call stack窗口,就會發現內容如下:
CDebugDlg::OnOK() line176 + 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(unsignedint 1, int 0, void * 0x00000000, AFX_CMDHANDLERINFO * 0x00000000) line 97 + 24bytes
CWnd::OnCommand(unsignedint 1, long 656988) line 2088
CWnd::OnWndMsg(unsignedint 273, unsigned int 1, long 656988, long * 0x0012f83c) line 1597 + 28 bytes
CWnd::WindowProc(unsignedint 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 + 21bytes
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(unsignedlong 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 + 11bytes
WinMain(HINSTANCE__ *0x00400000, HINSTANCE__ * 0x00000000, char * 0x00141f00, int 1) line 30
WinMainCRTStartup()line 330 + 54 bytes
KERNEL32! 7c816d4f()
這裡,CDebugDialog::OnOK作為整個調用鏈中最後被調用的函數出現在callstack的最上方,而內核中程序的啟動函數Kernel32! 7c816d4f()則作為棧底出現在最下方。
實例二:學習處理方法
微軟提供了MDI/SDI模型提供文檔處理的建議結構。有些時候,大家希望控制某個環節。例如,我們希望彈出自己的打開文件對話框,但是並不想自己實現整個文檔的打開過程,而更願意MFC完成其他部分的工作。可是,我們並不清楚MFC是怎麼處理文檔的,也不清楚如何插入自定義代碼。
幸運的是,我們知道當一個文檔被打開以後,系統會調用CDocument派生類的Serialize函數,我們可以利用這一點來跟蹤MFC的處理過程。
我們首先創建一個缺省的SDI工程Test1,並在CTest1Doc::Serialize函數的開頭增加一個斷點,運行程序,並打開一個文件。這時,我們可以看到調用堆棧是(我只截取了感興趣的一段):
CTest1Doc::Serialize(CArchive& {...}) line 66
CDocument::OnOpenDocument(constchar * 0x0012f54c) line 714
CSingleDocTemplate::OpenDocumentFile(constchar * 0x0012f54c, int 1) line 168 + 15 bytes
CDocManager::OpenDocumentFile(constchar * 0x0042241c) line 953
CWinApp::OpenDocumentFile(constchar * 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(unsignedint 57601, int 0, void * 0x00000000, AFX_CMDHANDLERINFO * 0x00000000) line 302+ 39 bytes
CFrameWnd::OnCmdMsg(unsignedint 57601, int 0, void * 0x00000000, AFX_CMDHANDLERINFO * 0x00000000) line 899+ 33 bytes
CWnd::OnCommand(unsignedint 57601, long 132158) line 2088
CFrameWnd::OnCommand(unsignedint 57601, long 132158) line 317
從上面的調用堆棧看,這個過程由一個WM_COMMAND消息觸發(因為我們用菜單打開文件),由CWinApp::OnFileOpen最先開始實際處理過程,這個函數調用CDocManager::OnFileOpen打開文檔。
我們首先雙擊CWinApp::OnFileOpen() line 37打開CWinApp::OnFileOpen,它的處理過程是:
ASSERT(m_pDocManager != NULL);
m_pDocManager->OnFileOpen();
m_pDocManager是一個CDocManager類的實例指針,我們雙擊CDocManager::OnFileOpen行,看該函數的實現:
voidCDocManager::OnFileOpen()
{
// prompt the user (with all documenttemplates)
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 beenalerted
}
很顯然,該函數首先調用DoPromptFileName函數來獲得一個文件名,然後在繼續後續的打開過程。
順這這個線索下去,我們一定能找到插入我們文件打開對話框的位置。由於這不是我們研究的重點,後續的分析我就不再詳述。
實例三:內存訪問越界
在Debug版本的VC程序中,程序會給每塊new出來的內存,預留幾個字節作為越界檢測之用。在釋放內存時,系統會檢查這幾個字節,判斷是否有內存訪問越界的可能。
我們借用前一個實例程序,在CTest1App::InitInstance的開頭添加以下幾行代碼:
char * p = new char[10];
memset(p,0,100);
delete []p;
return FALSE;
很顯然,這段代碼申請了10字節內存,但是使用了100字節。我們在memset(p,0,100);這行加一個斷點,然後執行程序,斷點到達後,我們觀察p指向的內存的值(利用Debug工具條的Memory功能),可以發現它的值是:
CD CD CD CD CD CD CD CD
CD CD FD FD FD FD FD FD
00 00 00 00 00 00 00 00
......
根據經驗,p實際被分配了16個字節,後6個字節用於保護。我們按F5全速執行程序,會發現如下的錯誤信息被彈出:
Debug Error!
Program: c:\temp\test1\Debug\test1.exe
DAMAGE: after normal block (#55) at 0x00421AB0
Press Retry to debug the application
該信息提示,在正常內存塊0x00421AB0後的內存被破壞(內存訪問越界),我們點擊Retry進入調試狀態,發現調用堆棧是:
_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
很顯然,這個錯誤是在調用delete時遇到的,出現在CTest1App::InitInstance()line 54 + 15 bytes之處。我們很容易根據這個信息找到,是在釋放哪塊內存時出現問題,之後,我們只需要根據這個內存的訪問過程確定哪兒出錯,這將大大降低調試的難度。
實例四:子類化
子類化是我們修改一個現有控件實現新功能的常用方法,我們借用實例一中的Debug對話框工程來演示我過去學習子類化的一個故事。我們創建一個缺省的名為Debug的對話框工程,並按照下列步驟進行實例化:
在對話框資源中增加一個Edit控件
用class wizard為CEdit派生一個類CMyEdit(由於今天不關心子類化的具體細節,因此這個類不作任何修改)
為Edit控件,增加一個控件類型變量m_edit,其類型為CMyEdit
在OnInitDialog中增加如下語句:
m_edit.SubclassDlgItem(IDC_EDIT1,this);
我們運行這個程序,會遇到這樣的錯誤:
Debug AssertionFailed!
Application:C:\temp\Debug\Debug\Debug.exe
File:Wincore.cpp
Line:311
For information on howyour program can cause an assertion failure, see Visual C++ documentation onasserts.
(Press Retry to debugthe application)
點擊Retry進入調試狀態,我們可以看到調用堆棧為:
CWnd::Attach(HWND__ *0x000205a8) line 311 + 28 bytes
CWnd::SubclassWindow(HWND__* 0x000205a8) line 3845 + 12 bytes
CWnd::SubclassDlgItem(unsignedint 1000, CWnd * 0x0012fe34 {CDebugDlg hWnd=0x001d058a}) line 3883 + 12 bytes
CDebugDlg::OnInitDialog()line 120
可以看出在Attach句柄時出現問題,出問題行的代碼為:
ASSERT(m_hWnd == NULL);
這說明我們在子類化時不應該綁定控件,我們刪除CDebugDialog::DoDataExchange中的下面一行:
DDX_Control(pDX, IDC_EDIT1, m_edit);
問題就得到解決
總結
簡而言之,call stack是調試中必須掌握的一個技術,但是程序員需要豐富的經驗才能很好的掌握和使用它。你不僅僅需要熟知C++語法,還需要對相關的平台、軟件設計思路有一定的了解。我的文章只能算一個粗淺的介紹,畢竟我在這方面也不算高手。希望對新進有一定的幫助。
調試之編程准備
對於一個程序員而言,學習一種語言和一種算法是非常容易的(不包括那些上學花很多時間玩,上班說學習沒時間的人)。但是,任何程序都可能是有瑕疵的,尤其有過團隊協作編程經驗的人,對這個感觸尤為深刻。
在我前面的述及調試的文章裡,我側重於VC集成環境中的一些設置信息和調試所需要的一些基本技巧。但是,僅僅知道這些是不夠的。一個成功的調試的開端是編程中的准備。
分離錯誤
很多程序員喜歡寫下面這樣的式子:
CLeftView* pView =
((CFrameWnd*)AfxGetApp()->m_pMainWnd)->m_wndSplitterWnd.GetPane(0,0);
如果一切順利,這樣的式子當然是沒什麼問題。但是作為一個程序員,你應該時刻記得任何一個調用在某些特殊的情況下都可能失敗,一旦上面某個式子失敗,那麼整個級聯式就會出問題,而你很難弄清楚到底哪兒出錯了。這樣的式子的結果往往是:省了2分鐘編碼的時間,多了幾星期的調試時間。
對於上面的式子,應該盡可能的把式子分解成獨立的函數調用,這樣我們可以隨時確定是哪個函數調用出問題,進口縮小需要檢查的范圍。
檢查返回值
檢查返回值對於許多編程者來說似乎是一個很麻煩的事情。但是如果你能在每個可能出錯的函數調用處都檢查返回值,就可以立刻知道出錯的函數。
有些人已經意識到檢查返回值的重要性,但是要記住,只檢查函數是否失敗是不夠的,我們需要知道函數失敗的確切原因。例如下面的代碼:
if(connect(sock,(const sockaddr*)&addr,sizeof(addr)) == SOCKET_ERROR)
{
AfxMessageBox("connect failed");
}
盡管這裡已經檢查了返回值,實際上沒有多少幫助。正如很多在vckbase上提問的人一樣,大概這時候只能喊“為什麼連接失敗啊?”。這種情況下,其實只能猜測失敗的原因,即使高手,也無法准確說出失敗的原因。
增加診斷信息
在知道錯誤的情況下,應該盡可能的告訴測試、使用者更多的信息,這樣才能了解導致失敗的原因。如果程序員能提供如下錯誤信息,對於診斷錯誤是非常有幫助的:
出錯的文件:我們可以借助宏THIS_FILE和__FILE__。注意THIS_FILE是在cpp文件手工定義的,而__FILE__是編譯器定義的。當記錄錯誤的函數定義在.h中時,有時候用THIS_FILE更好,因為他能說明在哪個cpp中調用並導致失敗的。
出錯的行:我們可以借助宏__LINE__
出錯的函數:如果設計的好,有以上兩項已經足夠。當然我們可以直接打印出出錯的函數或者表達式,這樣在大堆代碼中搜索(尤其是不支持go to line的編輯器中)還是很有用的。
出錯的原因:出錯的原因很多只能由程序自己給出。如果出錯只會問別人,那麼你永遠不可能成為一個合格的程序設計人員。很多函數失敗時都會設置errno。我們可以用GetLastError獲得錯誤碼,並通過FormatMessage打印出具體錯誤的文字描述。
摘自 踏雪無痕