`
blogfeifei
  • 浏览: 1191872 次
文章分类
社区版块
存档分类
最新评论

VC++程序调试

 
阅读更多

1 前言

当程序的运行结果与程序员预想的不一样,如死机,计算值不正确,出现内存访问冲突等,就需要进行调试

2 进行调试前的准备工作

因为程序调试是一项十分耗时的工作,很难估计出将要花费多长时间,因此在调试前,一定要做好充分准备,尽量避免做无用功:

1. 构造好的测试步骤,让程序出错有规律性或出错的概率越大越好

2. 被调试程序及相关库是最符合要求的版本

3. 工程临时文件如.ncb被删除

4. 整个工程被重新编译

5. 应用程序的链接路经与调试路径保持一致

6. 单体测试全部通过

3 出错位置和原因的确定

3.1 几种典型错误的原因

1 内存莫名其妙的失效

原因:内存指针被多处引用,被多处释放

2 多线程条件下死机

原因:线程中由于用了SendMessage而造成死锁,可人为加入消息循环

3 多线程条件下内存访问冲突

原因:内存被多个线程同时使用,可加入线程同步机制(用消息队列,信号灯等)

4 内存访问冲突

原因:内存越界(如字符串拷贝,内存拷贝)

5 窗口消息的次序问题

原因:如窗口未初始化就开始用

3.2 定位错误的位置

1 对代码的理解越深,对代码出错位置的确定越精确,必要时应画出相关代码的类图和时序图

2 从IDE调用堆栈判断出错位置和原因

3 从Win32 API或MFC类库函数的返回错误码判断出错原因,返回错误码的含义可以从MSDN或源代码中找到,还可以通过VC工具Error lookup找到

4 在代码中加入带编号的TRACE语句或MessageBox(release版),逐步缩小调试范围

5 对于死机现象或偶发现象,可通过逐步注释掉代码的方法确定死机的位置和原因

6 如果死机现象或偶发现象是新出现的,可以通过比较目前版本和上一版本的差异来确定位置和原因

4 debug方式下调试

4.1 调试的几种技巧

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

4.1.3 如何在循环语句中设置断点

比如在下面的代码中,当nRet == 0 时就认为程序出错。但是如何定位此时i的值为几呢。

1) 将光标定位在要调试的语句前面

2) CTRL+B, 在Break at处选择 行号

3) 选择Condition

[Enter the expression…] 输入 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行。然后我们再设置一个数据断点。如下图:

F5继续运行,程序停在B行,说明B处代码修改了szName1。B处明明没有修改szName1呀?但调试器指明是这一行,一般不会错,所以还是静下心来看看程序,哦,你发现了:szName2只有4个字节,而strcpy了7个字节,所以覆写了szName1。
数据断点不只是对变量改变有效,还可以设置变量是否等于某个值。譬如,你可以将Figure 2中红圈处改为条件”szName2[0]==''''y''''“,那么当szName2第一个字符为y时断点就会启动。
可以看出,数据断点相对位置断点一个很大的区别是不用明确指明在哪一行代码设置断点。

4.1.5 其他

1 在call stack窗口中设置断点,选择某个函数,按F9设置一个断点。这样可以从深层次的函数调用中迅速返回到需要的函数。
2 Set Next StateMent命令(debug过程中,右键菜单中的命令)
此命令的作用是将程序的指令指针(EIP)指向不同的代码行。譬如,你正在调试上面那段代码,运行在A行,但你不愿意运行B行和C行代码,这时,你就可以在 D行,右键,然后“Set Next StateMent”。调试器就不会执行B、C行。只要在同一函数内,此指令就可以随意跳前或跳后执行。灵活使用此功能可以大量节省调试时间。
3 watch窗口
watch窗口支持丰富的数据格式化功能。如输入0x65,u,则在右栏显示101。
实时显示windows API调用的错误:在左栏输入@err,hr。
在watch窗口中调用函数。提醒一下,调用完函数后马上在watch窗口中清除它,否则,单步调试时每一步调试器都会调用此函数。
4 messages断点不怎么实用。基本上可以用前面讲述的断点代替。

4.2 DLL的调试

4.2.1 DLL的测试与调试方法

DLL的测试与调试通常都要用到客户端,在客户端的调用DLL的API之前添加断点,可以直接进入到DLL内部调试。

4.2.2 LoadLibrary失败。

通常的原因是由于被加载的dll同时加载了其他dll或者组件,当这些dll或者组件不存在时loadLibrary不会成功。可以使用Visual studio tools-〉depends, 将要加载的dll拖入到depends中,从里面可以找出那些dll或者组件不存在。

4.2.3 注册dll内部的COM组件时(Regsvr32)失败。

绝大多数失败也是由于加载dll不成功造成的。这是因为在注册dll内部的组件时,首先要加载此dll,如果加载失败,也会导致regsvr32失败,也可以采用上面的办法。

4.3 COM的调试

4.3.1 跟踪引用计数

若要在 ATL 中跟踪引用数,请在包括 atlbase.h 之前添加以下代码行:

#define _ATL_DEBUG_INTERFACES

该语句导致在每次调用 AddRefRelease 时,“输出”窗口均显示接口的当前引用数以及对应的类名和接口名称。

可将断点设置于:void CDebugMainDlg::OnButtonTraceRefcnt()

4.3.2 QueryInterface 调用调试

若要在 ATL 中调试 QueryInterface 调用,请在包括 atlcom.h 之前添加以下定义:

#define _ATL_DEBUG_QI

然后在调试时,在“输出”窗口中查找在对象上查询的每个接口的名称。

可将断点设置于:void CDebugMainDlg::OnButtonTraceQI()

4.4 多线程的调试

当涉及到多个线程通信时,要注意在正确的位置设置断点。

见:

void CDebugMainDlg::OnButtonProcThd()

void CDebugMainDlg::OnButtonWinThd()

void CDebugMainDlg::OnButtonThdMsg()

在例子中首先启动一个运行线程函数的线程(Thread1),启动一个CWinThread类型的线程(Thread2),之后在OnButtonThdMsg函数中使用event通知Thread1,当 Thread1接收到event之后,使用ThreadMessage通知Thread2。实际上还有一个窗口的主线程,也就是 OnButtonThdMsg运行的线程,所以要想全程根中点击button之后的运行情况,就要在各个线程中的恰当位置设置好断点。

在程序的调试状态,选择debug->Threads,可以查看当前运行的线程,并可以让某个线程挂起,继续执行等。

5 Debug版与Release版的区别

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)类似

7 多进程同时调试

当一个应用程序是被另一个应用程序启动时,或者多个应用程序之间进行交互,就要用到多进程调试技术,方法是:

1 启动任务管理器,选择要调试的进程,点击鼠标右键,选择debug

2 启动VC IDE,选择下图所示的菜单,在选择相应的进程

注意:这里有一个问题,可能我们想要设断点的代码在进程能够调试前已经执行过,无法设断点,解决办法有两个:

1 在设断点的代码加入代码

ASSERT(FALSE);

进程运行时会出现下述对话框,选择retry就可以进行调试

2 在设断点的代码加入代码

AfxMessageBox(“debug”);//debug是任意的

进程运行时会出现对话框,此时attach改进成就可以进行调试

当多个进程同时处于调试状态,我们就可以从各自的调试窗口看到TRACE信息。

8 杂项

8.1 响应WM_MOUSEMOVE 消息的调试:

很多情况下如果不加条件的直接在MouseMove 响应函数内消息设置断点,并不方便调试,比如当鼠标在移动到某一区域内出错的情况,就无法利用断点跟踪,这时候可以在程序中,多添加一些TRACE语句,来定位,对于Release版还有可以用log工具输出到log文件,如果没有现成的log工具可以用,也可以自己编写一个非常简单的log工具。

PreTranslateMessage,HookMessage,与上面的情况类似。

8.2 使用Spy++中简单的功能

8.2.1 Finder Tool

Visual studio tools->Spy++->ctrl+F

将圆圈图标拖动到目标窗口上,可以在下面看到有关窗口的一些信息。

Caption:窗口的标题。

Class:类明。

Style:窗口风格:

Rect:窗口的位置和大小

8.2.2 察看窗口的各种属性

在上面的窗口中选择Properties radio button->OK,可以在下面的窗口中查看各种信息。

8.2.3 察看窗口的消息

若在途中选择Messages radio button->OK->ctrl+o,

上面的途中列出了所有可以看到的消息,若只关心某一类消息,比如,鼠标消息。

则点击Clear ALL,然后在右面只选中Mouse check box.

单击OK。

则所有有关鼠标的消息,都可以显示在输出窗口上。如果只想跟踪WM_LBUTTONDOWN的消息,则在左侧Messages to View框中,只选中WM_LBUTTONDOWN.

如果想监视某一个空间,比如button,则可以将finder tool定位在那个button上。然后按照上面的步骤进行设置。

9 总结

调试最重要的还是你要思考,要猜测你的程序可能出错的地方,然后运用你的调试器来证实你的猜测。而熟练使用上面这些技巧无疑会加快这个过程。另外在调试过程中,不要局限于一种方法,将几个方法结合在一起使用,可以加快错误定位的速度,以便较快的解决问题。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics