VC调试入门

转自:http://penguin7.blog.51cto.com/966026/439868
作者:
阿荣


 概述
调试是一个程序员最基本的技能,其重要性甚至超过学习一门语言。不会调试的程序员就意味着他即使会一门语言,却不能编制出任何好的软件。
这里我简要的根据自己的经验列出调试中比较常用的技巧,希望对大家有用。
本文约定,在选择菜单时,通过/表示分级菜单,例如File/Open表示顶级菜单File的子菜单Open。
 
 设置
为了调试一个程序,首先必须使程序中包含调试信息。一般情况下,一个从AppWizard创建的工程中包含的Debug Configuration自动包含调试信息,但是是不是Debug版本并不是程序包含调试信息的决定因素,程序设计者可以在任意的Configuration中增加调试信息,包括Release版本。
为了增加调试信息,可以按照下述步骤进行:

  • 打开Project settings对话框(可以通过快捷键ALT+F7打开,也可以通过IDE菜单Project/Settings打开)
  • 选择C/C++页,Category中选择general ,则出现一个Debug Info下拉列表框,可供选择的调试信息 方式包括: 
    命令行Project settings说明
    None没有调试信息
    /ZdLine Numbers Only目标文件或者可执行文件中只包含全局和导出符号以及代码行信息,不包含符号调试信息
    /Z7C 7.0- Compatible目标文件或者可执行文件中包含行号和所有符号调试信息,包括变量名及类型,函数及原型等
    /ZiProgram Database创建一个程序库(PDB),包括类型信息和符号调试信息。
    /ZIProgram Database for Edit and Continue除了前面/Zi的功能外,这个选项允许对代码进行调试过程中的修改和继续执行。这个选项同时使#pragma设置的优化功能无效
  • 选择Link页,选中复选框"Generate Debug Info",这个选项将使连接器把调试信息写进可执行文件和DLL
  • 如果C/C++页中设置了Program Database以上的选项,则Link incrementally可以选择。选中这个选项,将使程序可以在上一次编译的基础上被编译(即增量编译),而不必每次都从头开始编译。

 断点
断点是调试器设置的一个代码位置。当程序运行到断点时,程序中断执行,回到调试器。断点是 最常用的技巧。调试时,只有设置了断点并使程序回到调试器,才能对程序进行在线调试。

设置断点:可以通过下述方法设置一个断点。首先把光标移动到需要设置断点的代码行上,然后

  • 按F9快捷键
  • 弹出Breakpoints对话框,方法是按快捷键CTRL+B或ALT+F9,或者通过菜单Edit/Breakpoints打开。打开后点击Break at编辑框的右侧的箭头,选择 合适的位置信息。一般情况下,直接选择line xxx就足够了,如果想设置不是当前位置的断点,可以选择Advanced,然后填写函数、行号和可执行文件信息。

去掉断点:把光标移动到给定断点所在的行,再次按F9就可以取消断点。同前面所述,打开Breakpoints对话框后,也可以按照界面提示去掉断点。

条件断点:可以为断点设置一个条件,这样的断点称为条件断点。对于新加的断点,可以单击Conditions按钮,为断点设置一个表达式。当这个表达式发生改变时,程序就 被中断。底下设置包括“观察数组或者结构的元素个数”,似乎可以设置一个指针所指向的内存区的大小,但是我设置一个比较的值但是改动 范围之外的内存区似乎也导致断点起效。最后一个设置可以让程序先执行多少次然后才到达断点。

数据断点:数据断点只能在Breakpoints对话框中设置。选择“Data”页,就显示了设置数据断点的对话框。在编辑框中输入一个表达式,当这个 表达式的值发生变化时,数据断点就到达。一般情况下,这个表达式应该由运算符和全局变量构成,例如:在编辑框中输入 g_bFlag这个全局变量的名字,那么当程序中有g_bFlag= !g_bFlag时,程序就将停在这个语句处。

消息断点:VC也支持对Windows消息进行截获。他有两种方式进行截获:窗口消息处理函数和特定消息中断。
在Breakpoints对话框中选择Messages页,就可以设置消息断点。如果在上面那个对话框中写入消息处理函数的名字,那么 每次消息被这个函数处理,断点就到达(我觉得如果采用普通断点在这个函数中截获,效果应该一样)。如果在底下的下拉 列表框选择一个消息,则每次这种消息到达,程序就中断。

 值
Watch
VC支持查看变量、表达式和内存的值。所有这些观察都必须是在断点中断的情况下进行。
观看变量的值最简单,当断点到达时,把光标移动到这个变量上,停留一会就可以看到变量的值。
VC提供一种被成为Watch的机制来观看变量和表达式的值。在断点状态下,在变量上单击右键,选择Quick Watch, 就弹出一个对话框,显示这个变量的值。
单击Debug工具条上的Watch按钮,就出现一个Watch视图(Watch1,Watch2,Watch3,Watch4),在该视图中输入变量或者表达式,就可以观察 变量或者表达式的值。注意:这个表达式不能有副作用,例如++运算符绝对禁止用于这个表达式中,因为这个运算符将修改变量的值,导致 软件的逻辑被破坏。

Memory
由于指针指向的数组,Watch只能显示第一个元素的值。为了显示数组的后续内容,或者要显示一片内存的内容,可以使用memory功能。在 Debug工具条上点memory按钮,就弹出一个对话框,在其中输入地址,就可以显示该地址指向的内存的内容。


Varibles

Debug工具条上的Varibles按钮弹出一个框,显示所有当前执行上下文中可见的变量的值。特别是当前指令涉及的变量,以红色显示。

寄存器
Debug工具条上的Reigsters按钮弹出一个框,显示当前的所有寄存器的值。

 进程控制
VC允许被中断的程序继续运行、单步运行和运行到指定光标处,分别对应快捷键F5、F10/F11和CTRL+F10。各个快捷键功能如下: 

快捷键说明
F5继续运行
F10单步,如果涉及到子函数,不进入子函数内部
F11单步,如果涉及到子函数,进入子函数内部
CTRL+F10运行到当前光标处。

 

 Call Stack
调用堆栈反映了当前断点处函数是被那些函数按照什么顺序调用的。单击Debug工具条上的Call stack就显示Call Stack对话框。在CallStack对话框中显示了一个调用系列,最上面的是当前函数,往下依次是调用函数的上级函数。单击这些函数名可以跳到对应的函数中去。

 其他调试手段
系统提供一系列特殊的函数或者宏来处理Debug版本相关的信息,如下:

 

宏名/函数名说明
TRACE使用方法和printf完全一致,他在output框中输出调试信息
ASSERT它接收一个表达式,如果这个表达式为TRUE,则无动作,否则中断当前程序执行。对于系统中出现这个宏 导致的中断,应该认为你的函数调用未能满足系统的调用此函数的前提条件。例如,对于一个还没有创建的窗口调用SetWindowText等。
VERIFY和ASSERT功能类似,所不同的是,在Release版本中,ASSERT不计算输入的表达式的值,而VERIFY计算表达式的值。

 

 关注
一个好的程序员不应该把所有的判断交给编译器和调试器,应该在程序中自己加以程序保护和错误定位,具体措施包括:

  • 对于所有有返回值的函数,都应该检查返回值,除非你确信这个函数调用绝对不会出错,或者不关心它是否出错。
  • 一些函数返回错误,需要用其他函数获得错误的具体信息。例如accept返回INVALID_SOCKET表示accept失败,为了查明 具体的失败原因,应该立刻用WSAGetLastError获得错误码,并针对性的解决问题。
  • 有些函数通过异常机制抛出错误,应该用TRY-CATCH语句来检查错误
  • 程序员对于能处理的错误,应该自己在底层处理,对于不能处理的,应该报告给用户让他们决定怎么处理。如果程序出了异常, 却不对返回值和其他机制返回的错误信息进行判断,只能是加大了找错误的难度。

另外:VC中要编制程序不应该一开始就写cpp/h文件,而应该首先创建一个合适的工程。因为只有这样,VC才能选择合适的编译、连接 选项。对于加入到工程中的cpp文件,应该检查是否在第一行显式的包含stdafx.h头文件,这是Microsoft Visual Studio为了加快编译 速度而设置的预编译头文件。在这个#include "stdafx.h"行前面的所有代码将被忽略,所以其他头文件应该在这一行后面被包含。
对于.c文件,由于不能包含stdafx.h,因此可以通过Project settings把它的预编译头设置为“不使用”,方法是:

  • 弹出Project settings对话框
  • 选择C/C++
  • Category选择Precompilation Header
  • 选择不使用预编译头。

关于调试时输出的字符串信息

作者:
①塌糊涂

下载源代码

使用工具:VC6.0,IDA

当我们要在程序中输出调试信息时,常常以字符串的形式来输出,例如:

      printf("Some debug information here!/n");

这段代码在Debug和Release版下都输出调试信息,这不是我们所要的,一般地大家都会添加
预编译指令,如下所示:

      #if _DEBUG         printf("Some debug information here!/n");        #endif

这样就达到了在Debug版里程序输出调试信息,在Release版下不输出调试信息的目的。(在Release版里
连printf函数都没有调用)可如果要在程序里的许多地方输出调试信息,若采用上面的方式会很麻烦;
(至于为什么麻烦,可能就是不愿多敲几次键盘吧,呵呵。。。)

于是大家都想到写个输出函数,代码如下:

      void printInfo(char *strInfo)           {       #if _DEBUG               printf(strInfo);       #endif       }

注:该函数只是演示用的,很简单,没有其他检查字符串功能。

在要输出调试信息的地方,调用如下语句就行:

      printInfo("Some debug information here!/n");       

确实,在Debug模式下运行该程序,则输出如下信息:

      Some debug information here!

在Release模式下,则没输出什么信息;

我们往往在这个时候认为一切都OK了;如果你认为是,就没必要往下看了;呵呵。。。

虽然在Release版下运行程序没有输出调试信息来,可这些调试信息却留在了二进制的可执行文件里;
我们可以用IDA来打开该Release版的可执行文件,看到如图一所示的信息:

 
图一:IDA反汇编后的main函数

注:该函数就是main函数


可见调试信息字符串(“Some debug information here!/n”)确实存在于Release版的可执行文件里; 
我们当然不希望别人看到这些调试信息,那有没有办法来防止该调试信息被编译进Release版的可执行文件里呢?
办法是有的,这里来描述2个方法。

办法一:
定义如下宏:

      #if _DEBUG        #define _D(str) str        #else       #define _D(str) NULL          #endif

此时输出语句变为:

      printInfo(_D("Some debug information here!/n"));           

在Debug模式下运行程序,依然输出调试信息:

“Some debug information here!”;

在Release下,则什么都不输出,此时我们用IDA看一下Release版的二进制文件,则没有发现该调试信息字符串。
如图二示:


图二:IDA反汇编后的main函数

方法二:
定义如下宏:

      #if _DEBUG          void printInfo(char *strInfo)       {     	  printf(strInfo);       }       #else       #define printInfo(str)       #endif

注意:该宏把函数printInfo的定义也放进去了; 
在Debug模式下运行程序,也同样输出调试信息:

“Some debug information here!”;

在Release下,也什么都不输出,此时我们用IDA看一下Release版的二进制文件,也没有发现该调试信息字符串。

如图三示:

 
图三:IDA反汇编后的main函数

既然方法一和方法二都能实现同样的功能,那究竟那个方法好呢?

方法一和方法二确实都没在可执行文件里留下调试信息,比较一下图二和图三,我们不难发现:
图二当中多了一个函数调用 call nullsub_1,该函数就是printInfo,虽然该函数什么都不做,
但它却调用了,我们一般也不希望该函数调用,所以方法一中多了一个函数调用,增加了开销,
而方法二当中却没有调用该函数。

个人认为方法二较好。

结束语:

若要转载该文章,请保持原文章的完整性,谢谢!
文中如有不妥之处,请指正,谢谢!
E-mail:
grapeky@etang.com

调用规范与可变参数表

作者:
阿半

  语言调用规范是指进行一次函数调用所采用的传递参数的方法,返回值的处理以及调用堆栈的清理。Microsoft C/C++ 语言中采用了五种调用规范,分别是__cdecl, __stdcall, __fastcall,thiscall和nake每一中调用规范都是利用eax作为返回值,如果函数返回值是64位的,则利用edx:eax对来返回值。Nake调用规范非常的灵活,足以独立的一篇文章描述,这里就不再描述nake调用规范。下表列出了前面四种规范调用的特点:

关键字堆栈清理者参数传递顺序
__cdecl调用者从右至左
__stdcall被调用者从右至左
__fastcall被调用者从右至左,前两个参数由寄存器ecx,edx传递
thiscall被调用者或者调用者从右至左

 

  __cdecl 最大好处在于由于是调用者清理栈,它可以处理可变参数,缺点则在于它增加了程序的大小,因为在每个调用返回的时候,需要多执行一条清理栈的指令。
__stdcall 是在windows程序设计中出现的最多的调用规则,所有的不可变参数的API调用都使用这个规则。
__fastcall 在windows内核设计中被广泛的使用,由于两个参数由寄存器直接传递,采用这种规则的函数效率要比以上两种规则高。
thiscall是C++成员函数的默认调用规范,编译期间,这种调用会根据函数是否支持可变参数表来决定采用什么方式清理堆栈。如果成员函数不支持可变参数,那么它就是用参数入栈,ecx保存this指针的方式进行调用,如果成员函数支持可变参数,那么它的调用和__cdecl类似,唯一不同的是将this指针最后压入栈中进行传递。
调用者和被调用者必须采用同样的规则才能保证程序的正常执行,曾经看到很多程序员犯的错误就是由于调用规范的不一样,致使程序异常,比如:

DWORD ThreadFunc(LPVOID lpParam) { //… }  CreateThread(..,(LPTHREAD_START_ROUTINE)ThreadFunc, …);

  如果在编译期间没有指定编译选项/Gz(指定未指明调用规范的函数采用__stdcall方式),那么编译器自动将ThreadFunc处理成__cdecl调用规范(/Gd),这样可能在线程开始的时候正常执行,然而退出的时候由于堆栈没有正常清理,造成访问违例或者非法指令错误。
以上说了很多清理栈的问题,那么为什么清理栈很重要呢。堆栈是线程相关的,也就是说每一个线程含有一个堆栈,这个堆栈上保存了局部变量,调用返回地址等很多线程相关的数据,这也是为什么独立运行的线程可以调用同样一个函数而互不干扰的原因。堆栈的特点恐怕大家已经非常熟悉了,那么根据上面的每一种调用,我给出一个简单的图示来说明清理堆栈的重要性,以及为什么上面的例子代码会出错。


图一 这是线程堆栈在运行的时候的样子

调用前和后esp的差值中间包含了函数参数表,返回地址这样的重要信息,举个简单的调用例子.假设有某个函数定义是这样的:

Int __cdecl func(void* p);

再假设esp调用函数前的数值为0x1234,那么在进入这个函数体内看到的堆栈是这样的:

122C 1230 1234 Next p 

这里的next指调用函数后的下一条指令的位置。调用函数的汇编码:

Push p Call func Add esp,4 《--注意这里,由于是cdecl调用,需要调用者清栈。

而一个__stdcall调用的汇编码:

Push p Call func

  这里没有了add esp,4这个指令,因为在func函数返回的时候自己将esp已经复原了。再来看刚才举的错误的例子,由于强制转换的作用,线程开始函数被设置成了stdcall调用,而实际的线程函数被编译后,并没有执行堆栈的清理工作,线程函数返回的时候,由于堆栈的不正确,当然会发生错误。修改这个bug的方法只要在线程函数的定义前把__cdecl改成_stdcall即可。
有了上面的例子做基础来理解可变参数表就简单的多了,由于各种调用规范的限定,致使只有__cdecl调用规范可以采用可变参数表。先来看看可变参数表的定义(可以参考sdk目录下src/crt/varargs.h):

typedef char *va_list; #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define va_dcl va_list va_alist; #define va_start(ap) ap = (va_list)&va_alist #define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) #define va_end(ap) ap = (va_list)0

  va_list居然被定义成char* ?没错,这实际是用来定义了一个指针,指针的sizeof()就是操作系统可访问的地址空间的大小,也就是CPU相关的字长。_INTSIZEOF宏很简单,就是用来将数据以n的数据大小对齐。va_start宏有点模糊,可是如果你看懂了上面的堆栈数据结构,那么显然它就是获得最后一个固定参数的地址,也就是堆栈上的地址,va_arg先使得ap指向下一个参数,然后取得当前参数的值(注意,这个值正是堆栈上的值),va_end使得取参数过程结束。
这几个宏完成的动作很简单了,实际就是取得可变参数表在堆栈上的起始位置,然后根据参数类型,依次从堆栈上取出每一个参数。
本文简单的介绍了微软C/C++支持的调用类型,结合实例描述了规范的实际应用,最后根据CRT提供的源代码分析了可变参数表的实现。

仅通过崩溃地址找出源代码的出错行
作者:老罗


提交者:eastvc 发布日期:2003-10-23 9:16:11
原文出处:
http://www.luocong.com/articles/show_article.asp?Article_ID=29


作为程序员,我们平时最担心见到的事情是什么?是内存泄漏?是界面不好看?……错啦!我相信我的看法是不会有人反对的——那就是,程序发生了崩溃!

“该程序执行了非法操作,即将关闭。请与你的软件供应商联系。”,呵呵,这句 M$ 的“名言”,恐怕就是程序员最担心见到的东西了。有的时候,自己的程序在自己的机器上运行得好好的,但是到了别人的机器上就崩溃了;有时自己在编写和测试的过程中就莫名其妙地遇到了非法操作,但是却无法确定到底是源代码中的哪行引起的……是不是很痛苦呢?不要紧,本文可以帮助你走出这种困境,甚至你从此之后可以自豪地要求用户把崩溃地址告诉你,然后你就可以精确地定位到源代码中出错的那行了。(很神奇吧?呵呵。)

首先我必须强调的是,本方法可以在目前市面上任意一款编译器上面使用。但是我只熟悉 M$ 的 VC 和 MASM ,因此后面的部分只介绍如何在这两个编译器中实现,请读者自行融会贯通,掌握在别的编译器上使用的方法。

Well,废话说完了,让我们开始! :)

首先必须生成程序的 MAP 文件。什么是 MAP 文件?简单地讲, MAP 文件是程序的全局符号、源文件和代码行号信息的唯一的文本表示方法,它可以在任何地方、任何时候使用,不需要有额外的程序进行支持。而且,这是唯一能找出程序崩溃的地方的救星。

好吧,既然 MAP 文件如此神奇,那么我们应该如何生成它呢?在 VC 中,我们可以按下 Alt+F7 ,打开“Project Settings”选项页,选择 C/C++ 选项卡,并在最下面的 Project Options 里面输入:/Zd ,然后要选择 Link 选项卡,在最下面的 Project Options 里面输入: /mapinfo:lines 和 /map:PROJECT_NAME.map 。最后按下 F7 来编译生成 EXE 可执行文件和 MAP 文件。

在 MASM 中,我们要设置编译和连接参数,我通常是这样做的:

rc %1.rc
ml /c /coff /Zd %1.asm
link /subsystem:windows /mapinfo:exports /mapinfo:lines /map:%1.map %1.obj %1.res

把它保存成 makem.bat ,就可以在命令行输入 makem filename 来编译生成 EXE 可执行文件和 MAP 文件了。

在此我先解释一下加入的参数的含义:

/Zd 表示在编译的时候生成行信息
/map[:filename] 表示生成 MAP 文件的路径和文件名
/mapinfo:lines 表示生成 MAP 文件时,加入行信息
/mapinfo:exports 表示生成 MAP 文件时,加入 exported functions (如果生成的是 DLL 文件,这个选项就要加上)

OK,通过上面的步骤,我们已经得到了 MAP 文件,那么我们该如何利用它呢?

让我们从简单的实例入手,请打开你的 VC ,新建这样一个文件:

01 //****************************************************************
02 //程序名称:演示如何通过崩溃地址找出源代码的出错行
03 //作者:罗聪
04 //日期:2003-2-7
05 //出处:http://www.luocong.com(老罗的缤纷天地)
06 //本程序会产生“除0错误”,以至于会弹出“非法操作”对话框。
07 //“除0错误”只会在 Debug 版本下产生,本程序为了演示而尽量简化。
08 //注意事项:如欲转载,请保持本程序的完整,并注明:
09 //转载自“老罗的缤纷天地”(http://www.luocong.com)
10 //****************************************************************
11 
12 void Crash(void)
13 {
14 int i = 1;
15 int j = 0;
16 i /= j;
17 }
18 
19 void main(void)
20 {
21 Crash();
22 }

很显然本程序有“除0错误”,在 Debug 方式下编译的话,运行时肯定会产生“非法操作”。好,让我们运行它,果然,“非法操作”对话框出现了,这时我们点击“详细信息”按钮,记录下产生崩溃的地址——在我的机器上是 0x0040104a 。

再看看它的 MAP 文件:(由于文件内容太长,中间没用的部分我进行了省略)

CrashDemo

Timestamp is 3e430a76 (Fri Feb 07 09:23:02 2003)

Preferred load address is 00400000

Start Length Name Class
0001:00000000 0000de04H .text CODE
0001:0000de04 0001000cH .textbss CODE
0002:00000000 00001346H .rdata DATA
0002:00001346 00000000H .edata DATA
0003:00000000 00000104H .CRT$XCA DATA
0003:00000104 00000104H .CRT$XCZ DATA
0003:00000208 00000104H .CRT$XIA DATA
0003:0000030c 00000109H .CRT$XIC DATA
0003:00000418 00000104H .CRT$XIZ DATA
0003:0000051c 00000104H .CRT$XPA DATA
0003:00000620 00000104H .CRT$XPX DATA
0003:00000724 00000104H .CRT$XPZ DATA
0003:00000828 00000104H .CRT$XTA DATA
0003:0000092c 00000104H .CRT$XTZ DATA
0003:00000a30 00000b93H .data DATA
0003:000015c4 00001974H .bss DATA
0004:00000000 00000014H .idata$2 DATA
0004:00000014 00000014H .idata$3 DATA
0004:00000028 00000110H .idata$4 DATA
0004:00000138 00000110H .idata$5 DATA
0004:00000248 000004afH .idata$6 DATA

Address Publics by Value Rva+Base Lib:Object

0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo.obj
0001:00000070 _main 00401070 f CrashDemo.obj
0004:00000000 __IMPORT_DESCRIPTOR_KERNEL32 00424000 kernel32:KERNEL32.dll
0004:00000014 __NULL_IMPORT_DESCRIPTOR 00424014 kernel32:KERNEL32.dll
0004:00000138 __imp__GetCommandLineA@0 00424138 kernel32:KERNEL32.dll
0004:0000013c __imp__GetVersion@0 0042413c kernel32:KERNEL32.dll
0004:00000140 __imp__ExitProcess@4 00424140 kernel32:KERNEL32.dll
0004:00000144 __imp__DebugBreak@0 00424144 kernel32:KERNEL32.dll
0004:00000148 __imp__GetStdHandle@4 00424148 kernel32:KERNEL32.dll
0004:0000014c __imp__WriteFile@20 0042414c kernel32:KERNEL32.dll
0004:00000150 __imp__InterlockedDecrement@4 00424150 kernel32:KERNEL32.dll
0004:00000154 __imp__OutputDebugStringA@4 00424154 kernel32:KERNEL32.dll
0004:00000158 __imp__GetProcAddress@8 00424158 kernel32:KERNEL32.dll
0004:0000015c __imp__LoadLibraryA@4 0042415c kernel32:KERNEL32.dll
0004:00000160 __imp__InterlockedIncrement@4 00424160 kernel32:KERNEL32.dll
0004:00000164 __imp__GetModuleFileNameA@12 00424164 kernel32:KERNEL32.dll
0004:00000168 __imp__TerminateProcess@8 00424168 kernel32:KERNEL32.dll
0004:0000016c __imp__GetCurrentProcess@0 0042416c kernel32:KERNEL32.dll
0004:00000170 __imp__UnhandledExceptionFilter@4 00424170 kernel32:KERNEL32.dll
0004:00000174 __imp__FreeEnvironmentStringsA@4 00424174 kernel32:KERNEL32.dll
0004:00000178 __imp__FreeEnvironmentStringsW@4 00424178 kernel32:KERNEL32.dll
0004:0000017c __imp__WideCharToMultiByte@32 0042417c kernel32:KERNEL32.dll
0004:00000180 __imp__GetEnvironmentStrings@0 00424180 kernel32:KERNEL32.dll
0004:00000184 __imp__GetEnvironmentStringsW@0 00424184 kernel32:KERNEL32.dll
0004:00000188 __imp__SetHandleCount@4 00424188 kernel32:KERNEL32.dll
0004:0000018c __imp__GetFileType@4 0042418c kernel32:KERNEL32.dll
0004:00000190 __imp__GetStartupInfoA@4 00424190 kernel32:KERNEL32.dll
0004:00000194 __imp__HeapDestroy@4 00424194 kernel32:KERNEL32.dll
0004:00000198 __imp__HeapCreate@12 00424198 kernel32:KERNEL32.dll
0004:0000019c __imp__HeapFree@12 0042419c kernel32:KERNEL32.dll
0004:000001a0 __imp__VirtualFree@12 004241a0 kernel32:KERNEL32.dll
0004:000001a4 __imp__RtlUnwind@16 004241a4 kernel32:KERNEL32.dll
0004:000001a8 __imp__GetLastError@0 004241a8 kernel32:KERNEL32.dll
0004:000001ac __imp__SetConsoleCtrlHandler@8 004241ac kernel32:KERNEL32.dll
0004:000001b0 __imp__IsBadWritePtr@8 004241b0 kernel32:KERNEL32.dll
0004:000001b4 __imp__IsBadReadPtr@8 004241b4 kernel32:KERNEL32.dll
0004:000001b8 __imp__HeapValidate@12 004241b8 kernel32:KERNEL32.dll
0004:000001bc __imp__GetCPInfo@8 004241bc kernel32:KERNEL32.dll
0004:000001c0 __imp__GetACP@0 004241c0 kernel32:KERNEL32.dll
0004:000001c4 __imp__GetOEMCP@0 004241c4 kernel32:KERNEL32.dll
0004:000001c8 __imp__HeapAlloc@12 004241c8 kernel32:KERNEL32.dll
0004:000001cc __imp__VirtualAlloc@16 004241cc kernel32:KERNEL32.dll
0004:000001d0 __imp__HeapReAlloc@16 004241d0 kernel32:KERNEL32.dll
0004:000001d4 __imp__MultiByteToWideChar@24 004241d4 kernel32:KERNEL32.dll
0004:000001d8 __imp__LCMapStringA@24 004241d8 kernel32:KERNEL32.dll
0004:000001dc __imp__LCMapStringW@24 004241dc kernel32:KERNEL32.dll
0004:000001e0 __imp__GetStringTypeA@20 004241e0 kernel32:KERNEL32.dll
0004:000001e4 __imp__GetStringTypeW@16 004241e4 kernel32:KERNEL32.dll
0004:000001e8 __imp__SetFilePointer@16 004241e8 kernel32:KERNEL32.dll
0004:000001ec __imp__SetStdHandle@8 004241ec kernel32:KERNEL32.dll
0004:000001f0 __imp__FlushFileBuffers@4 004241f0 kernel32:KERNEL32.dll
0004:000001f4 __imp__CloseHandle@4 004241f4 kernel32:KERNEL32.dll
0004:000001f8 /177KERNEL32_NULL_THUNK_DATA 004241f8 kernel32:KERNEL32.dll

entry point at 0001:000000f0


Line numbers for ./Debug/CrashDemo.obj(d:/msdev/myprojects/crashdemo/crashdemo.cpp) segment .text

13 0001:00000020 14 0001:00000038 15 0001:0000003f 16 0001:00000046
17 0001:00000050 20 0001:00000070 21 0001:00000088 22 0001:0000008d

如果仔细浏览 Rva+Base 这栏,你会发现第一个比崩溃地址 0x0040104a 大的函数地址是 0x00401070 ,所以在 0x00401070 这个地址之前的那个入口就是产生崩溃的函数,也就是这行:

0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo.obj

因此,发生崩溃的函数就是 ?Crash@@YAXXZ ,所有以问号开头的函数名称都是 C++ 修饰的名称。在我们的源程序中,也就是 Crash() 这个子函数。

OK,现在我们轻而易举地便知道了发生崩溃的函数名称,你是不是很兴奋呢?呵呵,先别忙,接下来,更厉害的招数要出场了。

请注意 MAP 文件的最后部分——代码行信息(Line numbers information),它是以这样的形式显示的:

13 0001:00000020

第一个数字代表在源代码中的代码行号,第二个数是该代码行在所属的代码段中的偏移量。

如果要查找代码行号,需要使用下面的公式做一些十六进制的减法运算:

崩溃行偏移 = 崩溃地址(Crash Address) - 基地址(ImageBase Address) - 0x1000

为什么要这样做呢?细心的朋友可能会留意到 Rva+Base 这栏了,我们得到的崩溃地址都是由 偏移地址(Rva)+ 基地址(Base) 得来的,所以在计算行号的时候要把基地址减去,一般情况下,基地址的值是 0x00400000 。另外,由于一般的 PE 文件的代码段都是从 0x1000 偏移开始的,所以也必须减去 0x1000 。

好了,明白了这点,我们就可以来进行小学减法计算了:

崩溃行偏移 = 0x0040104a - 0x00400000 - 0x1000 = 0x4a

如果浏览 MAP 文件的代码行信息,会看到不超过计算结果,但却最接近的数是 CrashDemo.cpp 文件中的:

16 0001:00000046

也就是在源代码中的第 16 行,让我们来看看源代码:

16 i /= j;

哈!!!果然就是第 16 行啊!

兴奋吗?我也一样! :)

方法已经介绍完了,从今以后,我们就可以精确地定位到源代码中的崩溃行,而且只要编译器可以生成 MAP 文件(包括 VC、MASM、VB、BCB、Delphi……),本方法都是适用的。我们时常抱怨 M$ 的产品如何如何差,但其实 M$ 还是有意无意间提供了很多有价值的信息给我们的,只是我们往往不懂得怎么利用而已……相信这样一来,你就可以更为从容地面对“非法操作”提示了。你甚至可以要求用户提供崩溃的地址,然后就可以坐在家中舒舒服服地找到出错的那行,并进行修正。

是不是很爽呢? :) 

 

对“仅通过崩溃地址找出源代码的出错行”一文的补充与改进

作者:
上海伟功通信 roc

下载源代码

  读了老罗的“仅通过崩溃地址找出源代码的出错行”(下称"罗文")一文后,感觉该文还是可以学到不少东西的。不过文中尚存在有些说法不妥,以及有些操作太繁琐的地方 。为此,本人在学习了此文后,在多次实验实践基础上,把该文中的一些内容进行补充与改进,希望对大家调试程序,尤其是release版本的程序有帮助 。欢迎各位朋友批评指正。


一、该方法适用的范围
在windows程序中造成程序崩溃的原因很多,而文中所述的方法仅适用与:由一条语句当即引起的程序崩溃。如原文中举的除数为零的崩溃例子。而笔者在实际工作中碰到更多的情况是:指针指向一非法地址 ,然后对指针的内容进行了,读或写的操作。例如:

void Crash1() {  char * p =(char*)100;  *p=100; }

  这些原因造成的崩溃,无论是debug版本,还是release版本的程序,使用该方法都可找到造成崩溃的函数或子程序中的语句行,具体方法的下面还会补充说明。 另外,实践中另一种常见的造成程序崩溃的原因:函数或子程序中局部变量数组越界付值,造成函数或子程序返回地址遭覆盖,从而造成函数或子程序返回时崩溃。例如:

#include  void Crash2(); int main(int argc,char* argv[]) { 	Crash2(); 	return 0; }  void Crash2() { 	char p[1]; 	strcpy(p,"0123456789"); }

在vc中编译运行此程序的release版本,会跳出如下的出错提示框。 


图一 上面例子运行结果

这里显示的崩溃地址为:0x34333231。这种由前面语句造成的崩溃根源,在后续程序中方才显现出来的情况,显然用该文所述的方法就无能为力了。不过在此例中多少还有些蛛丝马迹可寻找到崩溃的原因:函数Crash2中的局部数组p只有一个字节大小 ,显然拷贝"0123456789"这个字符串会把超出长度的字符串拷贝到数组p的后面,即*(p+1)=''1'',*(p+2)=''2'',*(p+3)=''3'',*(p+4)=4。。。。。。而字符''1''的ASC码的值为0x31,''2''为0x32,''3''为0x33,''4''为0x34。。。。。,由于intel的cpu中int型数据是低字节保存在低地址中 ,所以保存字符串''1234''的内存,显示为一个4字节的int型数时就是0x34333231。显然拷贝"0123456789"这个字符串时,"1234"这几个字符把函数Crash2的返回地址给覆盖 ,从而造成程序崩溃。对于类似的这种造成程序崩溃的错误朋友们还有其他方法排错的话,欢迎一起交流讨论。


二、设置编译产生map文件的方法
该文中产生map文件的方法是手工添加编译参数来产生map文件。其实在vc6的IDE中有产生map文件的配置选项的。操作如下:先点击菜单"Project"->"Settings。。。",弹出的属性页中选中"Link"页 ,确保在"category"中选中"General",最后选中"Generate mapfile"的可选项。若要在在map文件中显示Line numbers的信息的话 ,还需在project options 中加入/mapinfo:lines 。Line numbers信息对于"罗文"所用的方法来定位出错源代码行很重要 ,但笔者后面会介绍更加好的方法来定位出错代码行,那种方法不需要Line numbers信息。 


图二 设置产生MAP文件 


三、定位崩溃语句位置的方法
"罗文"所述的定位方法中,找到产生崩溃的函数位置的方法是正确的,即在map文件列出的每个函数的起始地址中,最近的且不大于崩溃地址的地址即为包含崩溃语句的函数的地址 。但之后的再进一步的定位出错语句行的方法不是最妥当,因为那种方法前提是,假设基地址的值是 0x00400000 ,以及一般的 PE 文件的代码段都是从 0x1000偏移开始的 。虽然这种情况很普遍,但在vc中还是可以基地址设置为其他数,比如设置为0x00500000,这时仍旧套用

 崩溃行偏移 = 崩溃地址 - 0x00400000 - 0x1000 

的公式显然无法找到崩溃行偏移。 其实上述公式若改为

崩溃行偏移 = 崩溃地址 - 崩溃函数绝对地址 + 函数相对偏移

即可通用了。仍以"罗文"中的例子为例:"罗文"中提到的在其崩溃程序的对应map文件中,崩溃函数的编译结果为

0001:00000020 ?Crash@@YAXXZ 00401020 f CrashDemo。obj 

对与上述结果,在使用我的公式时 ,"崩溃函数绝对地址"指00401020, 函数相对偏移指 00000020, 当崩溃地址= 0x0040104a时, 则 崩溃行偏移 = 崩溃地址 - 崩溃函数起始地址+ 函数相对偏移 = 0x0040104a - 0x00401020 + 0x00000020= 0x4a,结果与"罗文"计算结果相同 。但这个公式更通用。


四、更好的定位崩溃语句位置的方法。
其实除了依靠map文件中的Line numbers信息最终定位出错语句行外,在vc6中我们还可以通过编译程序产生的对应的汇编语句,二进制码,以及对应c/c++语句为一体的"cod"文件来定位出错语句行 。先介绍一下产生这种包含了三种信息的"cod"文件的设置方法:先点击菜单"Project"->"Settings。。。",弹出的属性页中选中"C/C++"页 ,然后在"Category"中选则"Listing Files",再在"Listing file type"的组合框中选择"Assembly,Machine code, and source"。接下去再通过一个具体的例子来说明这种方法的具体操作。 


图三 设置产生"cod"文件 

准备步骤1)产生崩溃的程序如下:

01 //**************************************************************** 02 //文件名称:crash。cpp 03 //作用:    演示通过崩溃地址找出源代码的出错行新方法 04 //作者:   伟功通信 roc 05 //日期:   2005-5-16 06//**************************************************************** 07 void Crash1(); 08 int main(int argc,char* argv[]) 09 { 10	Crash1(); 11	return 0; 12 } 13 14 void Crash1() 15 { 16  char * p =(char*)100; 17  *p=100; 18 } 

准备步骤2)按本文所述设置产生map文件(不需要产生Line numbers信息)。
准备步骤3)按本文所述设置产生cod文件。
准备步骤4)编译。这里以debug版本为例(若是release版本需要将编译选项改为不进行任何优化的选项,否则上述代码会因为优化时看作废代码而不被编译,从而看不到崩溃的结果),编译后产生一个"exe"文件 ,一个"map"文件,一个"cod"文件。 
运行此程序,产生如下如下崩溃提示: 


图四 上面例子运行结果 

排错步骤1)定位崩溃函数。可以查询map文件获得。我的机器编译产生的map文件的部分如下:

 Crash   Timestamp is 42881a01 (Mon May 16 11:56:49 2005)   Preferred load address is 00400000   Start Length Name Class 0001:00000000 0000ddf1H .text CODE 0001:0000ddf1 0001000fH .textbss CODE 0002:00000000 00001346H .rdata DATA 0002:00001346 00000000H .edata DATA 0003:00000000 00000104H .CRT$XCA DATA 0003:00000104 00000104H .CRT$XCZ DATA 0003:00000208 00000104H .CRT$XIA DATA 0003:0000030c 00000109H .CRT$XIC DATA 0003:00000418 00000104H .CRT$XIZ DATA 0003:0000051c 00000104H .CRT$XPA DATA 0003:00000620 00000104H .CRT$XPX DATA 0003:00000724 00000104H .CRT$XPZ DATA 0003:00000828 00000104H .CRT$XTA DATA 0003:0000092c 00000104H .CRT$XTZ DATA 0003:00000a30 00000b93H .data DATA 0003:000015c4 00001974H .bss DATA 0004:00000000 00000014H .idata$2 DATA 0004:00000014 00000014H .idata$3 DATA 0004:00000028 00000110H .idata$4 DATA 0004:00000138 00000110H .idata$5 DATA 0004:00000248 000004afH .idata$6 DATA  Address Publics by Value Rva+Base Lib:Object  0001:00000020 _main 00401020 f Crash.obj 0001:00000060 ?Crash1@@YAXXZ 00401060 f Crash.obj 0001:000000a0 __chkesp 004010a0 f LIBCD:chkesp.obj 0001:000000e0 _mainCRTStartup 004010e0 f LIBCD:crt0.obj 0001:00000210 __amsg_exit 00401210 f LIBCD:crt0.obj 0001:00000270 __CrtDbgBreak 00401270 f LIBCD:dbgrpt.obj ... 

对于崩溃地址0x00401082而言,小于此地址中最接近的地址(Rva+Base中的地址)为00401060,其对应的函数名为?Crash1@@YAXXZ,由于所有以问号开头的函数名称都是 C++ 修饰的名称 ,"@@YAXXZ"则为区别重载函数而加的后缀,所以?Crash1@@YAXXZ就是我们的源程序中,Crash1() 这个函数。
排错步骤2)定位出错行。打开编译生成的"cod"文件,我机器上生成的文件内容如下:

	TITLE	E:/Crash/Crash。cpp 	.386P include listing.inc if @Version gt 510 .model FLAT else _TEXT	SEGMENT PARA USE32 PUBLIC ''CODE'' _TEXT	ENDS _DATA	SEGMENT DWORD USE32 PUBLIC ''DATA'' _DATA	ENDS CONST	SEGMENT DWORD USE32 PUBLIC ''CONST'' CONST	ENDS _BSS	SEGMENT DWORD USE32 PUBLIC ''BSS'' _BSS	ENDS $SYMBOLS	SEGMENT BYTE USE32 ''DEBSYM'' $SYMBOLS	ENDS $TYPES	SEGMENT BYTE USE32 ''DEBTYP'' $TYPES	ENDS _TLS	SEGMENT DWORD USE32 PUBLIC ''TLS'' _TLS	ENDS ;	COMDAT _main _TEXT	SEGMENT PARA USE32 PUBLIC ''CODE'' _TEXT	ENDS ;	COMDAT ?Crash1@@YAXXZ _TEXT	SEGMENT PARA USE32 PUBLIC ''CODE'' _TEXT	ENDS FLAT	GROUP _DATA, CONST, _BSS 	ASSUME	CS: FLAT, DS: FLAT, SS: FLAT endif PUBLIC	?Crash1@@YAXXZ					; Crash1 PUBLIC	_main EXTRN	__chkesp:NEAR ;	COMDAT _main _TEXT	SEGMENT _main	PROC NEAR					; COMDAT  ; 9    : {    00000	55		 push	 ebp   00001	8b ec		 mov	 ebp, esp   00003	83 ec 40	 sub	 esp, 64			; 00000040H   00006	53		 push	 ebx   00007	56		 push	 esi   00008	57		 push	 edi   00009	8d 7d c0	 lea	 edi, DWORD PTR [ebp-64]   0000c	b9 10 00 00 00	 mov	 ecx, 16			; 00000010H   00011	b8 cc cc cc cc	 mov	 eax, -858993460		; ccccccccH   00016	f3 ab		 rep stosd  ; 10   : 	Crash1();    00018	e8 00 00 00 00	 call	 ?Crash1@@YAXXZ		; Crash1  ; 11   : 	return 0;    0001d	33 c0		 xor	 eax, eax  ; 12   : }    0001f	5f		 pop	 edi   00020	5e		 pop	 esi   00021	5b		 pop	 ebx   00022	83 c4 40	 add	 esp, 64			; 00000040H   00025	3b ec		 cmp	 ebp, esp   00027	e8 00 00 00 00	 call	 __chkesp   0002c	8b e5		 mov	 esp, ebp   0002e	5d		 pop	 ebp   0002f	c3		 ret	 0 _main	ENDP _TEXT	ENDS ;	COMDAT ?Crash1@@YAXXZ _TEXT	SEGMENT _p$ = -4 ?Crash1@@YAXXZ PROC NEAR				; Crash1, COMDAT  ; 15   : {    00000	55		 push	 ebp   00001	8b ec		 mov	 ebp, esp   00003	83 ec 44	 sub	 esp, 68			; 00000044H   00006	53		 push	 ebx   00007	56		 push	 esi   00008	57		 push	 edi   00009	8d 7d bc	 lea	 edi, DWORD PTR [ebp-68]   0000c	b9 11 00 00 00	 mov	 ecx, 17			; 00000011H   00011	b8 cc cc cc cc	 mov	 eax, -858993460		; ccccccccH   00016	f3 ab		 rep stosd  ; 16   :  char * p =(char*)100;    00018	c7 45 fc 64 00 	00 00		 mov	 DWORD PTR _p$[ebp], 100	; 00000064H  ; 17   :  *p=100;    0001f	8b 45 fc	 mov	 eax, DWORD PTR _p$[ebp]   00022	c6 00 64	 mov	 BYTE PTR [eax], 100	; 00000064H  ; 18   : }    00025	5f		 pop	 edi   00026	5e		 pop	 esi   00027	5b		 pop	 ebx   00028	8b e5		 mov	 esp, ebp   0002a	5d		 pop	 ebp   0002b	c3		 ret	 0 ?Crash1@@YAXXZ ENDP					; Crash1 _TEXT	ENDS END 

其中

?Crash1@@YAXXZ PROC NEAR				; Crash1, COMDAT

为Crash1汇编代码的起始行。产生崩溃的代码便在其后的某个位置。接下去的一行为: 

; 15   : {

冒号后的"{"表示源文件中的语句,冒号前的"15"表示该语句在源文件中的行数。 这之后显示该语句汇编后的偏移地址,二进制码,汇编代码。如 

00000	55		 push	 ebp

其中"0000"表示相对于函数开始地址后的偏移,"55"为编译后的机器代码," push ebp"为汇编代码。从"cod"文件中我们可以看出,一条(c/c++)语句通常需要编译成数条汇编语句 。此外有些汇编语句太长则会分两行显示如: 

00018	c7 45 fc 64 00 	00 00		 mov	 DWORD PTR _p$[ebp], 100	; 00000064H

其中"0018"表示相对偏移,在debug版本中,这个数据为相对于函数起始地址的偏移(此时每个函数第一条语句相对偏移为0000);release版本中为相对于代码段第一条语句的偏移(即代码段第一条语句相对偏移为0000,而以后的每个函数第一条语句相对偏移就不为0000了)。"c7 45 fc 64 00 00 00 "为编译后的机器代码 ,"mov DWORD PTR _p$[ebp], 100"为汇编代码, 汇编语言中";"后的内容为注释,所以";00000064H",是个注释这里用来说明100转换成16进制时为"00000064H"。
接下去,我们开始来定位产生崩溃的语句。
第一步,计算崩溃地址相对于崩溃函数的偏移,在本例中已经知道了崩溃语句的地址(0x00401082),和对应函数的起始地址(0x00401060),所以崩溃地址相对函数起始地址的偏移就很容易计算了: 

  崩溃偏移地址 = 崩溃语句地址 - 崩溃函数的起始地址 = 0x00401082 - 0x00401060 = 0x22。

第二步,计算出错的汇编语句在cod文件中的相对偏移。我们可以看到函数Crash1()在cod文件中的相对偏移地址为0000,则 

崩溃语句在cod文件中的相对偏移 =  崩溃函数在cod文件中相对偏移 + 崩溃偏移地址 = 0x0000 + 0x22 = 0x22

第三步,我们看Crash1函数偏移0x22除的代码是什么?结果如下 

 00022	c6 00 64	 mov	 BYTE PTR [eax], 100	; 00000064H

这句汇编语句表示将100这个数保存到寄存器eax所指的内存单元中去,保存空间大小为1个字节(byte)。程序正是执行这条命令时产生了崩溃,显然这里eax中的为一个非法地址 ,所以程序崩溃了!
第四步,再查看该汇编语句在其前面几行的其对应的源代码,结果如下: 

; 17   :  *p=100;

其中17表示该语句位于源文件中第17行,而“*p=100;”这正是源文件中产生崩溃的语句。
至此我们仅从崩溃地址就查找出了造成崩溃的源代码语句和该语句所在源文件中的确切位置,甚至查找到了造成崩溃的编译后的确切汇编代码!
怎么样,是不是感觉更爽啊?


五、小节

1、新方法同样要注意可以适用的范围,即程序由一条语句当即引起的崩溃。另外我不知道除了VC6外,是否还有其他的编译器能够产生类似的"cod"文件。
2、我们可以通过比较 新方法产生的debug和releae版本的"cod"文件,查找那些仅release版本(或debug版本)有另一个版本没有的bug(或其他性状)。例如"罗文"中所举的那个用例 ,只要打开release版本的"cod"文件,就明白了为啥debug版本会产生崩溃而release版本却没有:原来release版本中产生崩溃的语句其实根本都没有编译 。同样本例中的release版本要看到崩溃的效果,需要将编译选项改为为不优化的配置。

关于MFC下检查和消除内存泄露的技巧

作者:
freepublic

摘要
本文分析了Windows环境使用MFC调试内存泄露的技术,介绍了在Windows环境下用VC++查找,定位和消除内存泄露的方法技巧。

关键词:VC++;CRT 调试堆函数;试探法。

编译环境
VC++6.0
技术原理
检测内存泄漏的主要工具是调试器和 CRT 调试堆函数。若要启用调试堆函数,请在程序中包括以下语句:

#define CRTDBG_MAP_ALLOC #include <stdlib.h> #include <crtdbg.h>

注意 #include 语句必须采用上文所示顺序。如果更改了顺序,所使用的函数可能无法正确工作。 

通过包括 crtdbg.h,将 malloc 和 free 函数映射到其“Debug”版本_malloc_dbg 和_free_dbg,这些函数将跟踪内存分配和释放。此映射只在调试版本(在其中定义了 _DEBUG)中发生。发布版本使用普通的 malloc 和 free 函数。

#define 语句将 CRT 堆函数的基版本映射到对应的“Debug”版本。并非绝对需要该语句,但如果没有该语句,内存泄漏转储包含的有用信息将较少。

在添加了上面所示语句之后,可以通过在程序中包括以下语句来转储内存泄漏信息:

_CrtDumpMemoryLeaks();

当在调试器下运行程序时,_CrtDumpMemoryLeaks 将在“输出”窗口中显示内存泄漏信息。内存泄漏信息如下所示:

Detected memory leaks!  Dumping objects ->  C:PROGRAM FILESVISUAL STUDIOMyProjectsleaktestleaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.  Data: <        > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete. 

如果不使用 #define _CRTDBG_MAP_ALLOC 语句,内存泄漏转储如下所示:

Detected memory leaks!  Dumping objects ->  {18} normal block at 0x00780E80, 64 bytes long.  Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD  Object dump complete. 

未定义 _CRTDBG_MAP_ALLOC 时,所显示的会是: 

内存分配编号(在大括号内)。 
块类型(普通、客户端或 CRT)。 
十六进制形式的内存位置。 
以字节为单位的块大小。 
前 16 字节的内容(亦为十六进制)。 
定义了 _CRTDBG_MAP_ALLOC 时,还会显示在其中分配泄漏的内存的文件。文件名后括号中的数字(本示例中为 20)是该文件内的行号。 

转到源文件中分配内存的行 

在"输出"窗口中双击包含文件名和行号的行。 
-或- 

在"输出"窗口中选择包含文件名和行号的行,然后按 F4 键。

_CrtSetDbgFlag 

如果程序总在同一位置退出,则调用 _CrtDumpMemoryLeaks 足够方便,但如果程序可以从多个位置退出该怎么办呢?不要在每个可能的出口放置一个对 _CrtDumpMemoryLeaks 的调用,可以在程序开始包括以下调用:

_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF ); 

该语句在程序退出时自动调用 _CrtDumpMemoryLeaks。必须同时设置 _CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF 两个位域,如上所示。 

说明 
在VC++6.0的环境下,不再需要额外的添加

#define CRTDBG_MAP_ALLOC  #include <stdlib.h>  #include <crtdbg.h> 

只需要按F5,在调试状态下运行,程序退出后在"输出窗口"可以看到有无内存泄露。如果出现

Detected memory leaks!  Dumping objects -> 

就有内存泄露。 

确定内存泄露的地方 
根据内存泄露的报告,有两种消除的方法: 

第一种比较简单,就是已经把内存泄露映射到源文件的,可以直接在"输出"窗口中双击包含文件名和行号的行。例如

Detected memory leaks!  Dumping objects ->  C:PROGRAM FILESVISUAL STUDIOMyProjectsleaktestleaktest.cpp(20) : {18} normal block at 0x00780E80, 64 bytes long.  Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD  Object dump complete. C:PROGRAM FILESVISUAL STUDIOMyProjectsleaktestleaktest.cpp(20)

就是源文件名称和行号。 

第二种比较麻烦,就是不能映射到源文件的,只有内存分配块号。

Detected memory leaks!  Dumping objects ->  {18} normal block at 0x00780E80, 64 bytes long.  Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD  Object dump complete. 

  这种情况我采用一种"试探法"。由于内存分配的块号不是固定不变的,而是每次运行都是变化的,所以跟踪起来很麻烦。但是我发现虽然内存分配的块号是变化的,但是变化的块号却总是那几个,也就是说多运行几次,内存分配的块号很可能会重复。因此这就是"试探法"的基础。

  1. 先在调试状态下运行几次程序,观察内存分配的块号是哪几个值;
  2. 选择出现次数最多的块号来设断点,在代码中设置内存分配断点: 添加如下一行(对于第 18 个内存分配):
    _crtBreakAlloc = 18; 
    或者,可以使用具有同样效果的 _CrtSetBreakAlloc 函数:
    _CrtSetBreakAlloc(18); 

     

  3. 在调试状态下运行序,在断点停下时,打开"调用堆栈"窗口,找到对应的源代码处; 
  4. 退出程序,观察"输出窗口"的内存泄露报告,看实际内存分配的块号是不是和预设值相同,如果相同,就找到了;如果不同,就重复步骤3,直到相同。 
  5. 最后就是根据具体情况,在适当的位置释放所分配的内存。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值