您的位置:首页 > 其它

调试技巧之调用堆栈

2012-10-24 21:03 309 查看
在计算机科学中,Callstack 是指存放某个程序的正在运行的函数的信息的栈。Call stack 由 stack frames 组成,每个 stack frame 对应于一个未完成运行的函数。

在当今流行的计算机体系架构中,大部分计算机的参数传递,局部变量的分配和释放都是通过操纵程序栈来实现的。栈用来传递函数参数,存储返回值信息,保存寄存器以供恢复调用前处理机状态。每次调用一个函数,都要为该次调用的函数实例分配栈空间。为单个函数分配的那部分栈空间就叫做 stack frame,也就是说,stack frame 这个说法主要是为了描述函数调用关系的。

Stack frame 组织方式的重要性和作用体现在两个方面:第一,它使调用者和被调用者达成某种约定。这个约定定义了函数调用时函数参数的传递方式,函数返回值的返回方式,寄存器如何在调用者和被调用者之间进行共享;第二,它定义了被调用者如何使用它自己的 stack frame 来完成局部变量的存储和使用。

简单介绍

调试是程序开发者必备技巧。如果不会调试,自己写的程序一旦出问题,往往无从下手。本人总结10年使用VC经验,对调试技巧做一个粗浅的介绍。希望对大家有所帮助。



今天简单的介绍介绍调用堆栈。调用堆栈在我的专栏的文章VC调试入门提了一下,但是没有详细介绍。



首先介绍一下什么叫调用堆栈:假设我们有几个函数,分别是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 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作为整个调用链中最后被调用的函数出现在call stack的最上方,而内核中程序的启动函数Kernel32! 7c816d4f()则作为栈底出现在最下方。



实例二:学习处理方法

微软提供了MDI/SDI模型提供文档处理的建议结构。有些时候,大家希望控制某个环节。例如,我们希望弹出自己的打开文件对话框,但是并不想自己实现整个文档的打开过程,而更愿意MFC完成其他部分的工作。可是,我们并不清楚MFC是怎么处理文档的,也不清楚如何插入自定义代码。



幸运的是,我们知道当一个文档被打开以后,系统会调用CDocument派生类的Serialize函数,我们可以利用这一点来跟踪MFC的处理过程。



我们首先创建一个缺省的SDI工程Test1,并在CTest1Doc::Serialize函数的开头增加一个断点,运行程序,并打开一个文件。这时,我们可以看到调用堆栈是(我只截取了感兴趣的一段):



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





从上面的调用堆栈看,这个过程由一个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 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 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)



点击Retry进入调试状态,我们可以看到调用堆栈为:



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句柄时出现问题,出问题行的代码为:



 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的编辑器中)还是很有用的。大家可以参见我的文章http://blog.vckbase.com/arong/archive/2005/11/10/14704.html中的方式进行处理,也许是一个基本的开端。

出错的原因:出错的原因很多只能由程序自己给出。如果出错只会问别人,那么你永远不可能成为一个合格的程序设计人员。很多函数失败时都会设置errno。我们可以用GetLastError获得错误码,并通过FormatMessage打印出具体错误的文字描述。  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: