正文

调试技巧之调用堆栈2008-04-09 22:45:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/vfdff/34046.html

分享到:

简单介绍调试是程序开发者必备技巧。如果不会调试,自己写的程序一旦出问题,往往无从下手。本人总结10年使用VC经验,对调试技巧做一个粗浅的介绍。希望对大家有所帮助。今天简单的介绍介绍调用堆栈。调用堆栈在我的专栏的文章VC调试入门http://www.vckbase.com/document/viewdoc/?id=924提了一下,但是没有详细介绍。首先介绍一下什么叫调用堆栈:假设我们有几个函数,分别是function1,function2,function3,funtion4,且function1调用function2,function2调用function3,function3调用function4。在function4运行过程中,我们可以从线程当前堆栈中了解到调用他的那几个函数分别是谁。把函数的顺序关系看,function4、function3、function2、function1呈现出一种“堆栈”的特征,最后被调用的函数出现在最上方。因此称呼这种关系为调用堆栈(call stack)。当故障发生时,如果程序被中断,我们基本上只可以看到最后出错的函数。利用call stack,我们可以知道当出错函数被谁调用的时候出错。这样一层层的看上去,有时可以猜测出错误的原因。常见的这种中断时ASSERT宏导致的中断。在程序被中断时,debug工具条的右侧倒数第二个按钮一般是call stack按钮,这个按钮被按下后,你就可以看到当前的调用堆栈。

实例一:介绍我们首先演示一下调用堆栈。首先我们创建一个名为Debug的对话框工程。工程创建好以后,双击OK按钮创建消息映射函数,并添加如下代码: void CDebugDlg::OnOK() { // TODO: Add extra validation here ASSERT(FALSE); } 我们按F5开始调试程序。程序运行后,点击OK按钮,程序就会被中断。这时查看call stack窗口,就会发现内容如下: 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 88CCmdTarget::OnCmdMsg(unsigned int 1, int 0, void * 0x00000000, AFX_CMDHANDLERINFO * 0x00000000) line 302 + 39 bytesCDialog::OnCmdMsg(unsigned int 1, int 0, void * 0x00000000, AFX_CMDHANDLERINFO * 0x00000000) line 97 + 24 bytesCWnd::OnCommand(unsigned int 1, long 656988) line 2088CWnd::OnWndMsg(unsigned int 273, unsigned int 1, long 656988, long * 0x0012f83c) line 1597 + 28 bytesCWnd::WindowProc(unsigned int 273, unsigned int 1, long 656988) line 1585 + 30 bytesAfxCallWndProc(CWnd * 0x0012fe74 {CDebugDlg hWnd=???}, HWND__ * 0x001204b0, unsigned int 273, unsigned int 1, long 656988) line 215 + 26 bytesAfxWndProc(HWND__ * 0x001204b0, unsigned int 273, unsigned int 1, long 656988) line 368AfxWndProcBase(HWND__ * 0x001204b0, unsigned int 273, unsigned int 1, long 656988) line 220 + 21 bytesUSER32! 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 182CWnd::PreTranslateInput(tagMSG * 0x004167d8 {msg=0x00000202 wp=0x00000000 lp=0x000f001c}) line 3424CDialog::PreTranslateMessage(tagMSG * 0x004167d8 {msg=0x00000202 wp=0x00000000 lp=0x000f001c}) line 92CWnd::WalkPreTranslateTree(HWND__ * 0x001204b0, tagMSG * 0x004167d8 {msg=0x00000202 wp=0x00000000 lp=0x000f001c}) line 2667 + 18 bytesCWinThread::PreTranslateMessage(tagMSG * 0x004167d8 {msg=0x00000202 wp=0x00000000 lp=0x000f001c}) line 665 + 18 bytesCWinThread::PumpMessage() line 841 + 30 bytesCWnd::RunModalLoop(unsigned long 4) line 3478 + 19 bytesCDialog::DoModal() line 536 + 12 bytesCDebugApp::InitInstance() line 59 + 8 bytesAfxWinMain(HINSTANCE__ * 0x00400000, HINSTANCE__ * 0x00000000, char * 0x00141f00, int 1) line 39 + 11 bytesWinMain(HINSTANCE__ * 0x00400000, HINSTANCE__ * 0x00000000, char * 0x00141f00, int 1) line 30WinMainCRTStartup() line 330 + 54 bytesKERNEL32! 7c816d4f()这里,CDebugDialog::OnOK作为整个调用链中最后被调用的函数出现在call stack的最上方,而内核中程序的启动函数Kernel32! 7c816d4f()则作为栈底出现在最下方。

实例二:学习处理方法微软提供了MDI/SDI模型提供文档处理的建议结构。有些时候,大家希望控制某个环节。例如,我们希望弹出自己的打开文件对话框,但是并不想自己实现整个文档的打开过程,而更愿意MFC完成其他部分的工作。可是,我们并不清楚MFC是怎么处理文档的,也不清楚如何插入自定义代码。幸运的是,我们知道当一个文档被打开以后,系统会调用CDocument派生类的Serialize函数,我们可以利用这一点来跟踪MFC的处理过程。我们首先创建一个缺省的SDI工程Test1,并在CTest1Doc::Serialize函数的开头增加一个断点,运行程序,并打开一个文件。这时,我们可以看到调用堆栈是(我只截取了感兴趣的一段): CTest1Doc::Serialize(CArchive & {...}) line 66CDocument::OnOpenDocument(const char * 0x0012f54c) line 714CSingleDocTemplate::OpenDocumentFile(const char * 0x0012f54c, int 1) line 168 + 15 bytesCDocManager::OpenDocumentFile(const char * 0x0042241c) line 953CWinApp::OpenDocumentFile(const char * 0x0042241c) line 93CDocManager::OnFileOpen() line 841CWinApp::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 88CCmdTarget::OnCmdMsg(unsigned int 57601, int 0, void * 0x00000000, AFX_CMDHANDLERINFO * 0x00000000) line 302 + 39 bytesCFrameWnd::OnCmdMsg(unsigned int 57601, int 0, void * 0x00000000, AFX_CMDHANDLERINFO * 0x00000000) line 899 + 33 bytesCWnd::OnCommand(unsigned int 57601, long 132158) line 2088CFrameWnd::OnCommand(unsigned int 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行,看该函数的实现: 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}很显然,该函数首先调用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 bytesoperator delete(void * 0x00421ab0) line 351 + 12 bytesCTest1App::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 Assertion Failed!Application:C:\temp\Debug\Debug\Debug.exeFile:Wincore.cppLine:311For information on how your program can cause an assertion failure, see Visual C++ documentation on asserts.(Press Retry to debug the application)点击Retry进入调试状态,我们可以看到调用堆栈为: CWnd::Attach(HWND__ * 0x000205a8) line 311 + 28 bytesCWnd::SubclassWindow(HWND__ * 0x000205a8) line 3845 + 12 bytesCWnd::SubclassDlgItem(unsigned int 1000, CWnd * 0x0012fe34 {CDebugDlg hWnd=0x001d058a}) line 3883 + 12 bytesCDebugDlg::OnInitDialog() line 120可以看出在Attach句柄时出现问题,出问题行的代码为: ASSERT(m_hWnd == NULL); 这说明我们在子类化时不应该绑定控件,我们删除CDebugDialog::DoDataExchange中的下面一行: DDX_Control(pDX, IDC_EDIT1, m_edit);问题就得到解决总结简而言之,call stack是调试中必须掌握的一个技术,但是程序员需要丰富的经验才能很好的掌握和使用它。你不仅仅需要熟知C++语法,还需要对相关的平台、软件设计思路有一定的了解。

阅读(4028) | 评论(0)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

暂无评论
您需要登录后才能评论,请 登录 或者 注册