Showing posts with label 调试. Show all posts
Showing posts with label 调试. Show all posts

Thursday, 9 August 2007

细谈VC程序调试的若干方法[zt]

细谈VC程序调试的若干方法


VC所依赖的开发平台Microsoft Developer Studio本身提供的调试功能并不弱,每当我们创建一个新的VC工程项目时,默认状态就是Debug(调试)版本,在"组建"(Build)菜单下的 Configurations中可以看到除了调试版本还可以设置成发行(Release)版本。调试版本由于包含了大量信息,所以它生成的可执行程序容量 会远远大于发行版。
具体地,调试版本主要增加了两个内容:其一,会执行编译命令_D_DEBUG,使头文件的调试语句ifdef及其代码附加到程 序中;其二,在可执行文件中加入的调试信息使开发人员能够观察变量,进行单步执行等。在VC"组建"(Build)菜单下的"开始调试"中有4条专用的调 试命令:Go,Step into,Run to Cursor,Attach to process…。在运行程序源代码时用Go命令(而不是Execute)才能处于调试状态, Go命令会使程序运行变得缓慢下来,但可以更好地控制运行程序,我们可以在任何时刻中断程序、单步执行、查看变量、检查调用栈。
有必要详细介绍一 下VC的调试功能:首先,再次强调要用Go命令运行一个将要调试的程序;如果要中止调试状态下的运行程序可以点击Stop Debugging命令,还可以通过Break选项以可恢复方式中断调试程序的运行流程(用Restart选项可以重新开始运行程序);Step Into选项表示每次只执行一行语句(单步执行),但如果当前代码是调用一个函数,那么Step Into表示进入该函数,全部函数语句执行完后返回,而Step Over则是跳出这个函数;Step To Cursor选项表示程序将执行到光标所在的可执行语句行上;在调试多线程程序时,可以在线程函数或主应用程序线程中设置断点,还可以用Break选项结 束线程后用Threads选项查看运行线程列表,也可以选择悬挂和恢复每个线程;在设置断点后,在VC "查看"菜单的"调试窗口"中可以查看变量、内存、调用堆栈、寄存器以及反汇编语句。在程序中设置断点的方法是,点击要设置的代码行并点击设置代码的工具 栏按钮,会出现在代码行最左边的一个小黑点即是断点标志,这时再选Go程序会在执行到端点处停下来,如果要继续执行可以再选Go。
通过选择VC" 工具"菜单下的"源浏览器"可以生成一个.BSC文件,使用浏览器可以从中发现多种信息:程序中任何一个变量、函数、类或宏在何处定义及引用;可以列出所 有声明的函数类、变量、宏;可以发现调用一个指定函数的所有函数;可以找到一个指定类的派生来源或者它派生出哪些类。
在使用微软程序开发库 MSDN时,我们会发现其中的VC示例经常采用看似多余的ASSERT语句,其作用就是使程序具有"维护"性。对于Debug版本的VC程序,在遇到布尔 值为FALSE的ASSERT语句处停止,并显示Assertion Failed对话;如果设置为发布版,所有ASSERT语句都会被预处理程序删除。一个地道的VC编程员,应该有意在自己的代码中通过"维护"特征去检测 任何设定,诸如输入参数、循环范围和变量值的设定。
在安装好VC系统之后,在VC之外的程序组中有一个程序Tracer是一个跟踪工具,在激活它 后使用Go运行VC代码,在输出窗口就能够看到程序运行过程中的内部过程,包括DLL调用等,你如果看不到任何输出,可以转到菜单"查看"(View)点 击"输出"(Output)。
其实,MFC自身就提供有错误查找和TRACE语句,而TRACE语句的语法与printf非常类似,所以我们可以在程序中直接加入这条跟踪命令,如下所示:
// Example for TRACE
int i = 1;
char sz[] = "one";
TRACE( "Integer = %d, String = %s\n", i, sz );
// Output: 'Integer = 1, String = one'
在Developer Studio中还提供了一个ERRLOOK工具,程序员只要输入错误号就能得到系统出错信息或模块错误内容.
MFC从Cobject派生的每个类都包含一个Dump函数,该函数可把当前状态转储(Dumping)到输出窗口,这在某些调试过程中会有用,以下代码是Dump函数的用法:
// Example for CObject::Dump
void CAge::Dump( CDumpContext &dc ) const
{
CObject::Dump( dc );
dc << "Age = " << m_years;
}
在MFC 中还有一个非常有用的类是CMemoryState,我们可以在程序的任何部分使用这个类检测内存冲突,并得到内存冲突的确切位置。 CMemoryState类有3个成员函数:CheckPoint可将堆的当前状态存入类的实体;Difference可以比较两个实体包含的堆之间的差 异;DumpStatistics用于标准化转储所有被CheckPoint捕获后分配到堆的对象,如CheckPoint未被调用实体未被初始化时,该 函数将转储当前堆的所有内容。以下代码表示了CMemoryState类的使用方法:
// Example for CMemoryState::CMemoryState,
// Includes all CMemoryState functions
CMemoryState msOld, msNew, msDif;
msOld.Checkpoint();
CAge* page1 = new CAge( 21 );
CAge* page2 = new CAge( 22 );
msOld.DumpAllObjectsSince();
msNew.Checkpoint();
msDif.Difference( msOld, msNew );
msDif.DumpStatistics();
代码运行的结果为


Dumping objects ->
{2} a CObject at $190A
{1} a CObject at $18EA
Object dump complete.
0 bytes in 0 Free Blocks
8 bytes in 2 Object Blocks
0 bytes in 0 Non-Object Blocks
Largest number used: 8 bytes
Total allocations: 8 bytes


在MFC类和VC中本身就有"异常情况"这个概念,并在此基础上形成它们处理系统错误和意外的主要机制。比如当系统内存分配殆尽时,你的运行程序就会收到内存异常的消息。这样就给了程序员消除异常的机会。


MFC中的异常情况主要有:CArchiveException表示档案文件载入或保存时出错,CDBException属于数据库错误, CFileException为文件错误,CMemoryException为调用new时发生分配错误,CNotSupportedException 表示指定操作不被支持,COleException表示在调用OLE操作时出错,COleDispatchException表示在OLE自动操作时出 错,CResourceException表示资源找不到或无法创建,CUserException用于通知用户错误。


MFC还包含一系列以Afx-为词头的调试函数:AfxAbort可以在发生致命错误时异常终止程序,AfxCheckMemory可以检查堆和剩 余缓冲池的受损部分;AfxDoForAllClasses重声明所有CObject的派生类;AfxDoForAllObject重声明堆上所有 CObject派生的对象;AfxEnableMemoryTracking启用或禁止内存追踪;AfxIsMemoryBlock用于确认指针所指内存 有效;AfxIsValidAddress用于确认地址是驻留在程序的内存区域内;AfxIsValidString用于确认地址所指字符串有效; AfxSetAllocHook用于内存分配前进行检测;AfxTraceEnabled启动或禁止输出跟踪,AfxTraceFlags则进一步定制跟 踪特征。


在我们随手编制的VC程序中,普遍存在着会发生内存泄漏的隐患,有些问题程序的痼疾症状是在处理数据量激增时陷入瘫痪,更糟的要发现内存泄漏并不容 易。首先,我们要明确VC中内存泄漏的含义:简单说就是一个程序申请得到了一段内存却没有及时释放。比如用new在堆中分配了一个对象或对象组却并没有调 用delete操作。灵活的指针技术使内存泄漏的原因变得复杂化,比如改变了保存在一变量中的指针的值后未能删除指针所指向的内存区;当内存泄漏是来自一 个带有指针类型成员变量的类时会更加困难,因为当调用分配指针时并没有复制构造函数/析构函数或运算符


为了防止发生内存泄漏这样棘手的故障,在VC编程时应当注意遵循几个规范:其一,如果一个类包含有指针并且分配了指针值,那么就需要构造相应的析构 函数以删除该指针;其二,如果一个函数分配了一块内存并把该内存块返回给调用它的函数使用,那么它返回的必须是一个指针而非一个引用,因为引用不能被程序 删除;其三,即使一个函数分配了一段内存并在同一函数的稍后部分删除了该内存段,也要尽可能将内存块分配到堆栈中;最后,就是决不要试图改变一个指针值, 除非已经删除指针所指的对象或通过数组指向了该指针所指向的内存,而且也不要对new返回的指针进行加1运算


每当编写VC程序时,我们都会处于一个琳琅满目的集成开发环境(IDE)中,现实的真相是我们很多人在这里编程多年,对开发环境了解并不全面和细 微。记得王朔的小说中有句话说"穿了多年的外套在不穿时才发现它原来还有一个兜!"。我们在安装VC时,得到的IDE即Developer Studio, VC其实是Developer Studio下激活的一个组件而已,比如微软的VJ++也是基于Developer Studio。很少有技术书籍会一一介绍Developer Studio界面元素,也许聪明的程序员轻易就能识别其含义,全部猜对界面图符的含义并非易事。可是它们对我们了解开发信息很重要,也与调试程序有关联。


在Developer Studio下会生成多个文件去保存项目的所有信息:一个是以.DSW为扩展名的项目工作区文件,它包含项目中所有文件的名称、文件所在目录、编译器和连 接器的选项以及项目工作的其它信息;以.DSP为扩展名的也是项目记录文件,.OPT是工作区选项文件,它包含Developer Studio的所有个人设置 - 包括颜色、字体、工具栏、哪个文件被打开以及MDI窗口如何被定位和最新调试中的断点等。在打开项目工作区文件时其它文件随即会自动打开。在 Developer Studio下可以按类查看代码,其中的ClassView显示了应用程序中所有的类,每个类下显示了成员函数和数据成员,在成员函数旁有粉红图标,数据 成员旁是蓝绿色图标,保护类成员的图标旁有一枚钥匙,私有类成员则有一个挂锁图标。


当然,在开发环境下最主要的工作是输入编辑程序源代码,源代码会显示"语法着色"。在缺省情况下,代码为黑色,夹以绿色的注释和蓝色的关键字(指 VC所保留的public、private、new和int等等)。--这些地球人都知道,但是为了调试需要,我们还可以指定颜色去显示字符串、数字和运 算符。定义方法是通过Tools菜单下的Options对话框中的Format选项卡设置。


在Developer Studio提供的诸多菜单项中,我们往往对少数菜单避而不用,因为不了解它们的作用唯恐好奇心会造成乱子;还有多个菜单项都可以达到目的,至于它们之间 的微小差别则不甚了了。比如在编译和调试时常用到Build菜单组,它具有和应用程序编译、运行调试相关的多项操作:其中的Compile菜单会编译当前 的聚焦文件;Build菜单会编译和链接所有在项目中修改的文件;Build All会编译链接项目中所有文件,包括最近编译后没有修改过的文件;Batch Build用于包含有Debug和Release配置的项目;Clean会删除所有的中间和输出文件,因而项目目录下仅包含源文件;Debugger Remote Connection用于远程调试,即在一台机器运行程序而在另一台调试;Set Active Configuration可设置某个配置为激活状态(Debug或Release);Profiler能够识别应用程序的瓶颈,即找到降低程序执行速度 的代码和有关模块,为此需要在"工程/设置/Link"下勾选Profiling。


VC中的警告信息是有级别的。在"工程/设置"下的"C/C++"选项卡中的警告级别Warning Level缺省值是3,如果改为更为严格的4级,往往会产生更多的警告类错误。在C/C++选项卡中还提供了代码优化栏Optimizations,在你 完成漫漫调试之旅准备正式发布前夕,你应当改动此项,它提供了一些适合建立发行版应用程序的优化设置。


其实,在多年以前,在软件业中针对开发具有一定规模软件项目的情况就出现了软件工程理论,以此来指导软件人员树立团队协作意识,进而保证软件项目的 协调性及进度质量等。笔者在从事VC开发中对此很有感触,而且觉得对于每个开发人员自己也应当具备一定的工程素养。比如,我们在开发初期时,应当对所编写 的有价值的源代码及时备份,即使有些代码在后续阶段似乎没有用处了也不妨“敝帚自珍”。在VC编程中我经常遇到的问题是,一个不算小的编写了很久的程序, 在稍微扩展一点功能时出现了故障,而最挠头的时花了很多时间也无法排故,所幸的是对原来的程序我还有备份,可以重新再来--功能还要加,只是还一种编程思 路去实现相同的功能。说实在的,本人在VC开发中,如果要总结“解决了多少问题”,不如说很多时候是采用"游击战"巧妙地"绕过"了一些棘手的问题。-- 尤其在软件交工处于倒计时的开发后期出现的故障,往往你已经没有时间去找到故障,而是用前一个完好的VC Project去尝试另一条捷径。


VC 调试

功能强大的vc6调试器


作者:yy2better



   要成为一位优秀的软件工程师,调试能力必不可缺。本文将较详细介绍VC6调试器的主要用法。
   windows平台的调试器主要分为两大类:
   1 用户模式(user-mode)调试器:它们都基于win32 Debugging API,有使用方便的界面,主要用于调试用户模式下的应用程序。这类调试器包括Visual C++调试器、WinDBG、BoundChecker、Borland C++ Builder调试器、NTSD等。
   2 内核模式(kernel-mode)调试器:内核调试器位于CPU和操作系统之间,一旦启动,操作系统也会中止运行,主要用于调试驱动程序或用户模式调试 器不易调试的程序。这类调试器包括WDEB386、WinDBG和softice等。其中WinDBG和softice也可以调试用户模式代码。
   国外一位调试高手曾说,他70%调试时间是在用VC++,其余时间是使用WinDBG和softice。毕竟,调试用户模式代码,VC6调试器的效率 是非常高的。因此,我将首先在本篇介绍VC6调试器的主要用法,其他调试器的用法及一些调试技能在后续文章中阐述。

一 位置断点(Location Breakpoint)
   大家最常用的断点是普通的位置断点,在源程序的某一行按F9就设置了一个位置断点。但对于很多问题,这种朴素的断点作用有限。譬如下面这段代码:


void CForDebugDlg::OnOK()  
{
for (int i = 0; i < 1000; i++) //A
{
int k = i * 10 - 2; //B
SendTo(k); //C
int tmp = DoSome(i); //D
int j = i / tmp; //E
}
}

   执行此函数,程序崩溃于E行,发现此时tmp为0,假设tmp本不应该为0,怎么这个时候为0呢?所以最好能够跟踪此次循环时DoSome函数是如何 运行的,但由于是在循环体内,如果在E行设置断点,可能需要按F5(GO)许多次。这样手要不停的按,很痛苦。使用VC6断点修饰条件就可以轻易解决此问 题。步骤如下。
   1 Ctrl+B打开断点设置框,如下图:

Figure 1设置高级位置断点
   2 然后选择D行所在的断点,然后点击condition按钮,在弹出对话框的最下面一个编辑框中输入一个很大数目,具体视应用而定,这里1000就够了。
   3 按F5重新运行程序,程序中断。Ctrl+B打开断点框,发现此断点后跟随一串说明:...487 times remaining。意思是还剩下487次没有执行,那就是说执行到513(1000-487)次时候出错的。因此,我们按步骤2所讲,更改此断点的 skip次数,将1000改为513。
   4 再次重新运行程序,程序执行了513次循环,然后自动停在断点处。这时,我们就可以仔细查看DoSome是如何返回0的。这样,你就避免了手指的痛苦,节省了时间。
   再看位置断点其他修饰条件。如Figure 1所示,在“Enter the expression to be evaluated:”下面,可以输入一些条件,当这些条件满足时,断点才启动。譬如,刚才的程序,我们需要i为100时程序停下来,我们就可以输入在编辑框中输入“i==100”。
   另外,如果在此编辑框中如果只输入变量名称,则变量发生改变时,断点才会启动。这对检测一个变量何时被修改很方便,特别对一些大程序。
   用好位置断点的修饰条件,可以大大方便解决某些问题。

二 数据断点(Data Breakpoint)
   软件调试过程中,有时会发现一些数据会莫名其妙的被修改掉(如一些数组的越界写导致覆盖了另外的变量),找出何处代码导致这块内存被更改是一件棘手的 事情(如果没有调试器的帮助)。恰当运用数据断点可以快速帮你定位何时何处这个数据被修改。譬如下面一段程序:
#include "stdafx.h"
#include

int main(int argc, char* argv[])
{
char szName1[10];
char szName2[4];
strcpy(szName1,"shenzhen");
printf("%s\n", szName1); //A

strcpy(szName2, "vckbase"); //B
printf("%s\n", szName1);
printf("%s\n", szName2);

return 0;
}

   这段程序的输出是
       szName1: shenzhen
szName1: ase
szName2: vckbase

szName1何时被修改呢?因为没有明显的修改szName1代码。我们可以首先在A行设置普通断点,F5运行程序,程序停在A行。然后我们再设置一个数据断点。如下图:

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

三 其他
   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断点不怎么实用。基本上可以用前面讲述的断点代替。
总结
   调试最重要的还是你要思考,要猜测你的程序可能出错的地方,然后运用你的调试器来证实你的猜测。而熟练使用上面这些技巧无疑会加快这个过程。最后,大家如果有关于调试方面的问题,我乐意参与探讨。