当程序的运行结果与程序员预想的不一样,如死机,计算值不正确,出现内存访问冲突等,就需要进行调试 因为程序调试是一项十分耗时的工作,很难估计出将要花费多长时间,因此在调试前,一定要做好充分准备,尽量避免做无用功: 1. 构造好的测试步骤,让程序出错有规律性或出错的概率越大越好 2. 被调试程序及相关库是最符合要求的版本 3. 工程临时文件如.ncb被删除 4. 整个工程被重新编译 5. 应用程序的链接路经与调试路径保持一致 6. 单体测试全部通过 1 内存莫名其妙的失效 原因:内存指针被多处引用,被多处释放 2 多线程条件下死机 原因:线程中由于用了SendMessage而造成死锁,可人为加入消息循环 3 多线程条件下内存访问冲突 原因:内存被多个线程同时使用,可加入线程同步机制(用消息队列,信号灯等) 4 内存访问冲突 原因:内存越界(如字符串拷贝,内存拷贝) 5 窗口消息的次序问题 原因:如窗口未初始化就开始用
1 对代码的理解越深,对代码出错位置的确定越精确,必要时应画出相关代码的类图和时序图 2 从IDE调用堆栈判断出错位置和原因 3 从Win32 API或MFC类库函数的返回错误码判断出错原因,返回错误码的含义可以从MSDN或源代码中找到,还可以通过VC工具Error lookup找到 4 在代码中加入带编号的TRACE语句或MessageBox(release版),逐步缩小调试范围 5 对于死机现象或偶发现象,可通过逐步注释掉代码的方法确定死机的位置和原因 6 如果死机现象或偶发现象是新出现的,可以通过比较目前版本和上一版本的差异来确定位置和原因 4 在debug方式下调试
4.1.1 使用ASSERT ASSERT(ASSERT_VALID)宏仅在程序的“Debug”版本中捕捉程序错误。该宏在“Release”版本中不生成任何代码。
4.1.2 使用TRACE 以下的例子只能在debug中显示, a) TRACE CString csTest = “test”; TRACE(“CString is %s\n”,csTest);
b) ATLTRACE
c) AfxDump AfxDump要求被dump的对象从CObject类继承,并且实现了Dump的方法。
CTime time = CTime::GetCurrentTime(); #ifdef _DEBUG afxDump << time << “\n”; #endif
比如在下面的代码中,当nRet == 0 时就认为程序出错。但是如何定位此时i的值为几呢。 1) 将光标定位在要调试的语句前面
2) CTRL+B, 在Break at处选择 行号 3) 选择Condition 在[Enter the …] 输入 nRet == 0;点击OK。运行程序,在弹出报错对话框后点击retry,程序执行将停在断点处。 这是可以看到循环变量的数值比如i此时等于9。表明I == 9的时候出的错误。 在CTRL+B,在[Enter the number of times…]输入7,程序将在循环变量i = 8 时停下来。就可以进入出错的函数进行调试了。 见: VCDebugSample\src\DebugMain\DebugMainDlg.cpp----- void CDebugMainDlg::OnButtonSetBkpt()
4.1.4 数据断点(Data Breakpoint) void CDebugMainDlg::OnButtonDataBkpt() { // TODO: Add your control notification handler code here char szName1[10]; char szName2[4]; strcpy(szName1,"shenzhen"); //A CString str1; str1.Format("%s\n", szName1); TRACE(str1);
strcpy(szName2, "vckbase"); //B
CString str2; str2.Format("%s\n", szName2); TRACE(str2);
str1.Format("%s\n", szName1); TRACE(str1);#include "stdafx.h"
这段程序的输出是 sz1: shenzhe sz21: vckbase sz1: ase
szName1何时被修改呢?因为没有明显的修改szName1代码。我们可以首先在A行设置普通断点,F5运行程序,程序停在A行。然后我们再设置一个数据断点。如下图:
1 在call stack窗口中设置断点,选择某个函数,按F9设置一个断点。这样可以从深层次的函数调用中迅速返回到需要的函数。
DLL的测试与调试通常都要用到客户端,在客户端的调用DLL的API之前添加断点,可以直接进入到DLL内部调试。
通常的原因是由于被加载的dll同时加载了其他dll或者组件,当这些dll或者组件不存在时loadLibrary不会成功。可以使用Visual studio tools-〉depends, 将要加载的dll拖入到depends中,从里面可以找出那些dll或者组件不存在。 4.2.3 注册dll内部的COM组件时(Regsvr32)失败。 绝大多数失败也是由于加载dll不成功造成的。这是因为在注册dll内部的组件时,首先要加载此dll,如果加载失败,也会导致regsvr32失败,也可以采用上面的办法。
若要在 ATL 中跟踪引用数,请在包括 atlbase.h 之前添加以下代码行: #define _ATL_DEBUG_INTERFACES 该语句导致在每次调用 AddRef 或 Release 时,“输出”窗口均显示接口的当前引用数以及对应的类名和接口名称。 可将断点设置于:void CDebugMainDlg::OnButtonTraceRefcnt()
若要在 ATL 中调试 QueryInterface 调用,请在包括 atlcom.h 之前添加以下定义: #define _ATL_DEBUG_QI 然后在调试时,在“输出”窗口中查找在对象上查询的每个接口的名称。 可将断点设置于:void CDebugMainDlg::OnButtonTraceQI()
当涉及到多个线程通信时,要注意在正确的位置设置断点。 见: void CDebugMainDlg::OnButtonProcThd() void CDebugMainDlg::OnButtonWinThd() void CDebugMainDlg::OnButtonThdMsg() 在例子中首先启动一个运行线程函数的线程(Thread1),启动一个CWinThread类型的线程(Thread2),之后在OnButtonThdMsg函数中使用event通知Thread1,当 Thread1接收到event之后,使用ThreadMessage通知Thread2。实际上还有一个窗口的主线程,也就是 OnButtonThdMsg运行的线程,所以要想全程根中点击button之后的运行情况,就要在各个线程中的恰当位置设置好断点。
在程序的调试状态,选择debug->Threads,可以查看当前运行的线程,并可以让某个线程挂起,继续执行等。 Debug通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
Debug 和 Release 的真正秘密,在于一组编译选项。下面列出了分别针对二者的选项(当然除此之外还有其他一些,如/Fd /Fo,但区别并不重要,通常他们也不会引起 Release 版错误,在此不讨论)
Debug 版本
参数 含义
/MDd /MLd 或 /MTd 使用 Debug runtime library (调试版本的运行时刻函数库)
/Od 关闭优化开关
/D "_DEBUG" 相当于 #define _DEBUG,打开编译调试代码开关 (主要针对assert函数)
/ZI 创建 Edit and continue(编辑继续)数据库,这样在调试过程中如果修改了源代码不需重新编译
/GZ 可以帮助捕获内存错误
Release 版本
参数 含义
/MD /ML 或 /MT 使用发布版本的运行时刻函数库
/O1 或 /O2 优化开关,使程序最小或最快
/D "NDEBUG" 关闭条件编译调试代码开关 (即不编译assert函数)
/GF 合并重复的字符串,并将字符串常量放到只读内存, 防止被修改
实际上,Debug 和 Release 并没有本质的界限,他们只是一组编译选项的集合,编译器只是按照预定的选项行动。事实上,我们甚至可以修改这些选项,从而得到优化过的调试版本或是带跟踪语句的发布版本。
哪些情况下 Release 版会出错
有了上面的介绍,我们再来逐个对照这些选项看看 Release 版错误是怎样产生的
1、Runtime Library:链接哪种运行时刻函数库通常只对程序的性能产生影响。调试版本的 Runtime Library 包含了调试信息,并采用了一些保护机制以帮助发现错误,因此性能不如发布版本。编译器提供的 Runtime Library 通常很稳定,不会造成 Release 版错误;倒是由于 Debug 的 Runtime Library 加强了对错误的检测,如堆内存分配,有时会出现 Debug 有错但 Release 正常的现象。应当指出的是,如果 Debug 有错,即使 Release 正常,程序肯定是有 Bug 的,只不过可能是 Release 版的某次运行没有表现出来而已。
2、优化:这是造成错误的主要原因,因为关闭优化时源程序基本上是直接翻译的,而打开优化后编译器会作出一系列假设。这类错误主要有以下几种:
1. 帧指针(Frame Pointer)省略(简称FPO):在函数调用过程中,所有调用信息(返回地址、参数)以及自动变量都是放在栈中的。若函数的声明与实现不同(参数、返回值、调用方式),就会产生错误,但 Debug 方式下,栈的访问通过 EBP 寄存器保存的地址实现,如果没有发生数组越界之类的错误(或是越界“不多”),函数通常能正常执行;Release 方式下,优化会省略 EBP 栈基址指针,这样通过一个全局指针访问栈就会造成返回地址错误是程序崩溃。
C++ 的强类型特性能检查出大多数这样的错误,但如果用了强制类型转换,就不行了。你可以在 Release 版本中强制加入/Oy-编译选项来关掉帧指针省略,以确定是否此类错误。此类错误通常有:MFC 消息响应函数书写错误。正确的应为:
afx_msg LRESULT OnMessageOwn (WPARAM wparam, LPARAM lparam);
ON_MESSAGE 宏包含强制类型转换。防止这种错误的方法之一是重定义 ON_MESSAGE 宏,把下列代码加到 stdafx.h 中(在#include "afxwin.h"之后),函数原形错误时编译会报错。
#undef ON_MESSAGE #define ON_MESSAGE(message, memberFxn) \ { message, 0, 0, 0, AfxSig_lwl, \ (AFX_PMSG)(AFX_PMSGW) (static_cast< LRESULT (AFX_MSG_CALL \ CWnd::*)(WPARAM, LPARAM) > (&memberFxn) },
2. volatile 型变量:volatile 告诉编译器该变量可能被程序之外的未知方式修改(如系统、其他进程和线程)。优化程序为了使程序性能提高,常把一些变量放在寄存器中(类似于 register 关键字),而其他进程只能对该变量所在的内存进行修改,而寄存器中的值没变。
如果你的程序是多线程的,或者你发现某个变量的值与预期的不符而你确信已正确的设置了,则很可能遇到这样的问题。这种错误有时会表现为程序在最快优化出错而最小优化正常。把你认为可疑的变量加上 volatile 试试。
3. 变量优化:优化程序会根据变量的使用情况优化变量。例如,函数中有一个未被使用的变量,在 Debug 版中它有可能掩盖一个数组越界,而在 Release 版中,这个变量很可能被优化调,此时数组越界会破坏栈中有用的数据。当然,实际的情况会比这复杂得多。与此有关的错误有非法访问,包括数组越界、指针错误等。例如:
void fn(void) { int i; i = 1; int a[4]; { int j; j = 1; } a[-1] = 1; //当然错误不会这么明显,例如下标是变量 a[4] = 1; }
j 虽然在数组越界时已出了作用域,但其空间并未收回,因而 i 和 j 就会掩盖越界。而 Release 版由于 i、j 并未其很大作用可能会被优化掉,从而使栈被破坏。
3. DEBUG 与 NDEBUG :当定义了 _DEBUG 时,assert() 函数会被编译,而 NDEBUG 时不被编译。此外,TRACE() 宏的编译也受 _DEBUG 控制。
所有这些断言都只在 Debug版中才被编译,而在 Release 版中被忽略。唯一的例外是 VERIFY()。事实上,这些宏都是调用了assert()函数,只不过附加了一些与库有关的调试代码。如果你在这些宏中加入了任何程序代码,而不只是布尔表达式(例如赋值、能改变变量值的函数调用等),那么Release版都不会执行这些操作,从而造成错误。初学者很容易犯这类错误,查找的方法也很简单,因为这些宏都已在上面列出,只要利用 VC++ 的 Find in Files 功能在工程所有文件中找到用这些宏的地方再一一检查即可。另外,有些高手可能还会加入 #ifdef _DEBUG 之类的条件编译,也要注意一下。
顺便值得一提的是VERIFY() 宏,这个宏允许你将程序代码放在布尔表达式里。这个宏通常用来检查 Windows API的返回值。有些人可能为这个原因而滥用VERIFY(),事实上这是危险的,因为VERIFY()违反了断言的思想,不能使程序代码和调试代码完全分离,最终可能会带来很多麻烦。因此,专家们建议尽量少用这个宏。
4. /GZ 选项:这个选项会做以下这些事:
1. 初始化内存和变量。包括用 0xCC 初始化所有自动变量,0xCD ( Cleared Data ) 初始化堆中分配的内存(即动态分配的内存,例如 new ),0xDD ( Dead Data ) 填充已被释放的堆内存(例如 delete ),0xFD( deFencde Data ) 初始化受保护的内存(debug 版在动态分配内存的前后加入保护内存以防止越界访问),其中括号中的词是微软建议的助记词。这样做的好处是这些值都很大,作为指针是不可能的(而且 32 位系统中指针很少是奇数值,在有些系统中奇数的指针会产生运行时错误),作为数值也很少遇到,而且这些值也很容易辨认,因此这很有利于在 Debug 版中发现 Release 版才会遇到的错误。要特别注意的是,很多人认为编译器会用0来初始化变量,这是错误的(而且这样很不利于查找错误)。
2. 通过函数指针调用函数时,会通过检查栈指针验证函数调用的匹配性。(防止原形不匹配)
3. 函数返回前检查栈指针,确认未被修改。(防止越界访问和原形不匹配,与第二项合在一起可大致模拟帧指针省略 FPO )通常 /GZ 选项会造成 Debug 版出错而 Release 版正常的现象,因为 Release 版中未初始化的变量是随机的,这有可能使指针指向一个有效地址而掩盖了非法访问。除此之外,/Gm/GF等选项造成错误的情况比较少,而且他们的效果显而易见,比较容易发现。
怎样“调试” Release 版的程序 2. 在编程过程中就要时常注意测试 Release 版本,以免最后代码太多,时间又很紧。
3. 在 Debug 版中使用 /W4 警告级别,这样可以从编译器获得最大限度的错误信息,比如 if( i =0 )就会引起 /W4 警告。不要忽略这些警告,通常这是你程序中的 Bug 引起的。但有时 /W4 会带来很多冗余信息,如 未使用的函数参数 警告,而很多消息处理函数都会忽略某些参数。我们可以用:
#progma warning(disable: 4702) //禁止 //... #progma warning(default: 4702) //重新允许来暂时禁止某个警告,或使用 #progma warning(push, 3) //设置警告级别为 /W3 //... #progma warning(pop) //重设为 /W4
来暂时改变警告级别,有时你可以只在认为可疑的那一部分代码使用 /W4。
6 在release方式下调试 当程序在Release下出错,而在debug下不出错时,就需要用到release方式下的调试技术。在用release方式下的调试技术之前,对代码进行如下检查: 1 release方式下没有代码被注释掉,如ASSERT(a=f()); TRACE(f());在release下是会被注释掉的。 2 检查所有的变量是否被初始化 3 检查边界错误,如以下代码: void func() { char buffer[10]; int counter;
lstrcpy(buffer, "abcdefghik"); // 11-byte copy, including NULL ... release方式下的调试技术: 1 在VC IDE中选择Project Settings (Alt-F2), 在 "C++/C tab" 中设置 category为 "General"将Debug Info setting 改为"Program Database". 2 在"Link tab"中选中"Generate Debug Info" tab. 3 执行"Rebuild All" 注意:1 有时也需要禁止release方式下的优化选项 2 如果有些代码段无法设置断点,可以加入如下代码: __asm {int 3}; 效果与Debug方式下的ASSERT(FALSE)类似 当一个应用程序是被另一个应用程序启动时,或者多个应用程序之间进行交互,就要用到多进程调试技术,方法是: 1 启动任务管理器,选择要调试的进程,点击鼠标右键,选择debug 2 启动VC IDE,选择下图所示的菜单,在选择相应的进程
注意:这里有一个问题,可能我们想要设断点的代码在进程能够调试前已经执行过,无法设断点,解决办法有两个: 1 在设断点的代码加入代码 ASSERT(FALSE); 进程运行时会出现下述对话框,选择retry就可以进行调试 2 在设断点的代码加入代码 AfxMessageBox(“debug”);//debug是任意的 进程运行时会出现对话框,此时attach改进成就可以进行调试 当多个进程同时处于调试状态,我们就可以从各自的调试窗口看到TRACE信息。
8.1 响应WM_MOUSEMOVE 消息的调试: 很多情况下如果不加条件的直接在MouseMove 响应函数内消息设置断点,并不方便调试,比如当鼠标在移动到某一区域内出错的情况,就无法利用断点跟踪,这时候可以在程序中,多添加一些TRACE语句,来定位,对于Release版还有可以用log工具输出到log文件,如果没有现成的log工具可以用,也可以自己编写一个非常简单的log工具。 PreTranslateMessage,HookMessage,与上面的情况类似。
8.2 使用Spy++中简单的功能 Visual studio tools->Spy++->ctrl+F 将圆圈图标拖动到目标窗口上,可以在下面看到有关窗口的一些信息。 Caption:窗口的标题。 Class:类明。 Style:窗口风格: Rect:窗口的位置和大小
在上面的窗口中选择Properties radio button->OK,可以在下面的窗口中查看各种信息。 若在途中选择Messages radio button->OK->ctrl+o, 上面的途中列出了所有可以看到的消息,若只关心某一类消息,比如,鼠标消息。 则点击Clear ALL,然后在右面只选中Mouse check box. 单击OK。 则所有有关鼠标的消息,都可以显示在输出窗口上。如果只想跟踪WM_LBUTTONDOWN的消息,则在左侧Messages to View框中,只选中WM_LBUTTONDOWN. 如果想监视某一个空间,比如button,则可以将finder tool定位在那个button上。然后按照上面的步骤进行设置。 调试最重要的还是你要思考,要猜测你的程序可能出错的地方,然后运用你的调试器来证实你的猜测。而熟练使用上面这些技巧无疑会加快这个过程。另外在调试过程中,不要局限于一种方法,将几个方法结合在一起使用,可以加快错误定位的速度,以便较快的解决问题。
|
|