调试技巧之调用堆栈 (三)

2014-11-24 12:03:58 · 作者: · 浏览: 3
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__是编译器定义的。当记录错