现在的位置: 首页 > 综合 > 正文

Windows程序调试—-第三部分 调试技术—-第9章 内存调试

2012年11月09日 ⁄ 综合 ⁄ 共 14413字 ⁄ 字号 评论关闭

9章内存调试

    能够方便高效地进行动态内存分配,是C++编程语言的重要优点之一;而调试时容易错误使用动态分配的内存也是其最大的缺点之一。Windows程序也可能同样存在与系统资源泄漏或者堆栈相关的内存问题。内存问题是Windows程序错误的常见来源之一、而且如果没有合适的工具进行调试:它们将是最难以追踪到的错误之一。

    动态内存分配错误有以下两种基本类型:内存错误和内存泄露。当一个指针或者该指针所指向的内存单元成为无效单元,或者内存中分配的数据结构被破坏时,就会造成内存错误。指针未被初始化、指针被初始化为一个无效地址、指针被不小心错误地修改、在与指针相关联的内存区域被释放以后使用该指针(这种指针被称为虚悬(dangling)指针),这些都会使指针变为无效指针。当通过一个错误指针或者虚悬指针对內存进行写入,或者是将指针强制转换为不匹配的数据结构,又或者是写数据越界的时候,内存本身也会遭到破坏。删除未被初始化的指针,删除非堆指针、多次删除同一指针或者覆盖一个指针的内部数据结构,都会造成内存分配系统错误。总之,C++中的内存错误有无数种可能发生的原因。

    内存泄漏在被动态分配的内存没有被释放的时候产生。有很多种情况会导致内存地漏,例如没有在全部的执行路径中释放内存(特别是在那些具有多个返回语句和具有异常抛出的函数中),没有在析构程序中释放所有的内存,或者是忘记将基类析构函数设记为虚函数,还有可能是很简单的情形:忘记释放内存。

    这里有一个不幸的消息,那就是在没有帮助的情况下,使用手工方式排除内存错误是极端困难的。内存错误很难被检测到,这是因为这些错误的症状往往十分细小,有时候甚至是不被人注意的。举个例子来说,一个内存泄漏错误可能没有任何症状——除了仅仅是在运行一段时间以后程序会崩溃。而一旦内存错误被找到,追踪得到产生这一错误的原因也十分困难,这是因为导致发生内存错误的原因和可观察到的内存错误产生的结果之间,可能已经相隔了很长的代码和很久的时间。虽然可以使用在第7章中所描述的内存断点的方法——使用Visual
C++
的调试器进行调试,能够在调试器的帮助下找到特定地址的内存错误,但是仅仅通过使用调试器跟踪检査的方法来确定是否发生了内存错误,可能会是一件困难的事情。使用手工方法将内存分配和回收的信息都记录下来,无疑是不现实的。可以找到这些错误的唯一现实的方法是协调好动态内存分配过程,使得程序能自动检测到自身的内存错误。

    这里还有一个好消息,就是Visual C++运行时刻函数库对动态内存分配的管理机制非常好,并且对检测和分析动态内存错误提供了广泛的支持。我将这个被管理的堆称为调试堆。Visual C++编译器自身也提供了相应选项来帮助发现未被初始化的指针变量和这些类型的堆桟破坏。你还可以使用C++的析构函数和智能指针来防止内存泄漏,当对象超出生存范围时会被自动释放。利用好了这些特性,你可以花最小的代价来发现、消除绝大多数的内存错误。

9.1 内存泄漏为什么不可接受

    内存错误显然是一个严重的问题,但是内存泄漏的重要性就远远没有那样明显。毕竟Windows会在程序结束的时候将泄漏的内存回收,因此内存泄漏仅仅是一个暂时性的问题。那么只要用户的程序的其他地方可以正确运行,为什么还要关心内存地漏的问题呢?

    不考虑回收内存的需要,下面有三个必须消除内存泄漏的原因。首先,内存泄漏往往会导致系统资源的泄露,这会对系统的性能产生直接影响。其次,虽然一旦程序结束,泄漏的内存就会被回收,但是通常的高质量程序和特定的服务器程序必须能够无限地运行。不要设想用户能够喜欢周期性重新启动你的程序。最后,内存泄漏往往是其他程序错误或者不良编程习惯的征兆。因此,对内存泄漏进行追踪,你往往会找到其他的一些文题。下面我们仔细看看前两种原因。

    内存泄露往往是其他程序错误或者不良编程习惯的征兆。

资源泄漏

    Windows程序中,动态分配内存往往不仅仅代表一块存储区域。很多时候,这些内存代表了某些类型的系统资源。例如文件、进程、线程、信号量、时钟、窗口、设备上下文、字体、画笔或者数据库连接,这些系统资源往往十分紧张,这就使得这种内存泄漏的后果变得比一般普通内存泄漏更加严重。对紧张的系统资源的泄漏会迅速导致系统性能下降,甚至会导致Windows在所有时用内存被耗尽之前彻底崩溃。为了强调这类问题的重要性,这种内存泄漏被称为资源泄漏。

    对紧张的系统资源的泄漏会迅速导致系统性能下降,甚至导致Windows崩溃。

    Windows 98中图形设备接口(GDI)的资源泄漏是这一类问题中最好的例子。Windows 98使用一个固定64K字节大小的堆来分配画刷、画笔以及其他的图形设备接口数据结构,这个堆被用于整个系统。如果一个程序不能将一个画刷或者画笔正确删除,这个64K字节的堆就会被很快耗尽,导致Widnows
98
和在其上运行的所有程序运行性能下降。即使用户拥有上百兆空闲内存,仍然会看到一个如图9.1所示的系统资源短缺对话框。

 

9.1 Windows 98系统资源短缺对话框

    看到这个消息对话框以后的最佳选择是保存你的工作,退出所有正在运行的程序并且重新启动Windows 98。这当然不太好了。当有了图形设备接口(GDI)以后,用户不再需要对这些资源直接进行动态分配。而是在调用例如CreatePen或者CreateSolidBrush这样的应用程序接口(
API)
函数时,让Windows在和用户程序行为相关的图形设备接口的堆中进行分配,Windows 2000中并末使用系统范围内的64K字节大小的堆,但是图形设备接口资源泄漏仍然会降低性能——不过不会如同在Windows
98
中那样快速地影响性能。

高质量程序应该无限地运行

    在某些时候,Windows程序中的资源泄漏是可以接受的。举个例子来说,一个用户往往仅仅运行几分钟就退出的应用工具程序所造成的资源泄露可能非常细小,不会造成任何可以观察到的影响。Windows自身也不是足够稳定,以致于每天不得不至少重新启动一次,因此有些Windows程序从来就没有机会能够将细小的资源泄漏积累成严重的大问题。此外,由于计算机的内存容量有限,用户不得不釆用经常性退出当前暂时不使用的程序的方法,以有效利用内存资源。这些程序的周期性重新启动给予了Windows充分机会来同收所有被泄漏的资源。

    但是现在时代已经不同了。Windows已经相当稳定,用户可以将Windows程序和Windows自身运行很长时间。现在的计算机拥有相当多的内存,这使得用户即使同时运行相当多的程序也毫无问题。这一切导致用户的期望发生了变化:他们现在期望Windows程序可以无限地运行。经常性地重新启动Windows或者Windows程序已经不再能够被用户接受。

    当然,导致程序崩溃的原因有很多种,资源泄漏仅仅是其中的一种。一个程序在崩溃之前可运行的时间越长,则导致崩溃的原因就越可能和资源泄漏有关,原因在于资源泄漏这种错误是和时间相关的(因为资源被耗尽需要花费一些时间),然而其他的错误一般不是时间相关的。举例来说,虽然一个很少被执行的代码中的逻辑错误可能会偶尔导致程序崩溃,但是这种崩溃在程序启动以后立即发生,或者在程序已经运行几天以后发生,其可能件是一样的。而与之产生对比的是,缓慢泄漏资源的程序可能需要几个星期才会逐渐到达崩溃的时刻。一个仅仅在程序已经运行了很长时间以后会发生的崩溃很有可能是资源泄漏的结果。去除程序中的内存泄漏对程序的长期稳定性来说十分重要。

9.2 内存调试的类型

    Windows通过它的VirtualAlloc/VirtualFreeHeapAlloc/HeapFree以及GlobalAlloc/GlobalFree应用程序接口函数提供了基本的动态内存分配函数。接着Visual
C++
运行时刻函数阵在HeapAllocHeapFree的上层实现了new/deletemalloc/freeMFCATL应用程序基本结构接着也向内存管理中添加了它们自己的部分,使得这问题变得更加复杂。对于大多数新的Windows程序来说,C的运行时刻函数库中已经拥有用户所需要用于内存管理和调试的所有内容,但是为了保持文章的完整性,我还是会讨论所有这些环境下面的内存调试。

Windows内存调试

    Windows中进行动态内存分配的基本方式是VirtualAlloc/VirtualFree应用程序接口函数。Windows为所有的进程提供了4G字节的虚拟地址空间(所有的Windows程序当然都是进程)。这样看起来就像是有很多地址空间,但是要记住这是虚拟内存,因此它仅仅是一段内存区域,除此之外什么都不是。在物理内存同虚拟内存的某一处对应起来以前,用户不可能使用虚拟内存完成任何有用的工作(需要注意的是X86处理器使用的是4K字节的页面大小)。所有被程序使用的内存(确切地说,是主程序可执行代码、动态链接库、拽、堆、内存映射文件和操作系统)都会被映射到虚拟内存中。但是这不一定意味着Windows程序需要直接对虚拟内存应用程序接口函数进行处理,除非该程序需要管理大规模数据(例如二维图形程序)或者完成系统功能(例如Windows自身)

    大多数C++程序使用C运行时刻函数库中提供的newdelete,而不是直接对虚拟内存进行处理:new函数和delete函数工作时,会分配一大块虚拟内存(Windows默认分配1M字节,但是可以通过修改链接器的/HEAP选项来修改分配字节数)并将这块虚拟内存按照程序需求再划分为小块(这被称为再分配)C运行时刻函数库是使用HeapAllocHeapFree实现这种再分配的。当一个进程被装载时,Windows会默认在该进程的虚拟地址空间中创建一个堆,这也就是我们所知道的默认堆空间。大多数程序有一个堆就足够了。但是有时候用户可以通过创建额外的堆来优化内存管理的性能。每一个进程中的C运行时刻函数库的实例都有其自己独立的堆。用户程序在一种情况下会具有多个堆(而用户也许毫不知情),那就是程序使用了动态链接库,而这个动态链接库具有其自己独立的运行时刻函数库的实例。我们常说的本地堆是指由运行时刻函数库的一个特殊实例来进行管理的堆。由此,所有C运行时刻函数库中内存调试函数的行为都将本地堆为基础来进行定义

    GlobalAllocGlobalFree函数是从16Windows中继承下来的。它们基本上是对HeapAllocHeapFree函数做了向后兼容的包装,因为它们除了从默认堆中进行再分配以外,几乎没有多做任何其它的事情。在新的程序中唯一会使用到这些函数的地方是将数据传输给Windows,例如将数据拷贝到剪切扳。只有当Windows应用程序接口文档特别推荐使用这些内存函数的时候,才有必要使用它们。

    Windows对内存调试提供的支持是十分简单的。Windows提供了IsBadCodePtrIsBadReadPtrIsBadStringPtrlsBadWritePtr应用程序接口函数来帮助用户判断各忡各样的类型指针是否合法。在用户觉得十分艰难的时候,可以使用HeapWalk应用程序接口函数来检查一个堆中的内容。除去这些函数以外,如果要调试从Windows那里直接获取的内存,用户在相当多的时候需要自己独立完成。

Windows保护内存

    虽然Windows没有提供很多应用程序接口函数来帮助用户进行内存调试,但是Windows和处理器一起提供了保护内存,这对跟踪非法指针和内存错误有很大帮助。简单了解一下保护内存在调试内存问题方面的有用之处和局限之处,还是有一些价值的。

    首先是保护内存的有用之处,由于32位的Windows提供了4GB的地址空间,对于普通程序来说,一个随机地址指向有效内存的可能性是很小的。因为Windows
2000
对最初的64KB进行保护,而Windows 98对最初的4KB进行保护,无论是使用空指针还是使用从空指针开始的即使很小的偏移量,都会导致非法内存访问。在Windows
2000
中,所有的操作系统地址空间(地址空间的高端2G字节)也同样被全部保护起来。在Windovvs所有的版本中,属于其他进程的内存是完全不能访问的。Windows还使用保护页对栈的上溢出和下溢出进行了保护,因此对堆栈写操作越界也会导致非法内存访问。另外,一个可被某一进程完全访问的内存页面可能具有只读属性,这样就可以对例如程序代码和只读数据这样的内存内容进行保护

    接下来是保护内存的局限之处,由于内存是按照4KB进行分配的,保护内存对一个可写页面没有提供任何保护。一种常见的内存错误问题是使用堆时写内存越界。由于进程对从内存中再分配出来的堆内存具有全部的访问权限,Windows对于那些仅仅在堆内部发生的内存错误并没有提供保护。但幸运的是,正如你所马上将要读到的Visual
C++
C运行时刻函数库在检测堆错误方面做得很好。

Visual C++C运行时刻函数库内存调试

    Visual C++C运行时刻函数库提供了广泛的功能,帮助用户检测动态内存分配的内存错误和内存泄漏。这些功能仅仅在调试版本中提供,因此它对发行版本没有影响,这样也就不会造成性能上的损失,这些调试中的大多数支持来源于MFC的一部分,但是从Visual
C++4.0
开始这些内容被移动到C运行时刻函数库中,这样所有使用Visual C++生成的程序都可以利用这些特性所带来的优点。

    当然,在C++中的内存分配是使用new函数,释放是使用delete函数的。在默认情况下。new函数在分配失败的情况下返回一个空指针,但是用户可以使用new而不是使用_set_new_handler加载一个处理程序来抛出一个异常,除非用户在维护一段等待new函数返回为空的代码,否则在使用new函数的时候,应该总是让其在失败时抛出异常,因为这样可以使用户更加容易书写可靠的代码。毕竟空指针很容易被忽视,而一个被抛出的异常确实不可能被忽略的(示例程序请查看第5章)。

    Visual C++C运行时刻函数库可以帮助用户使用许多种方法对内存错误进行调试。最主要的一种帮助就是对己经分配或者释放的的内存写入确定的字节作为标识,以帮助暴露程序中的错误。表9.1列出了所使用的标识。

9.1 Visual C++C运行时刻函数库标识模板

字节标识

含义

0xCD

已经分配的数据(助记词:alloCatedData)

0xDD

已经释放的数据(助记词:DeletedData)

0xFD

被保护的数据(助记词:FenceData)

    0xCD标识被用来填充那些最近分配的内存区域,而0xDD被用来填充已经释放的内存,保护字节被写入在被保护内存区域的开始和结束的四个字节,以帮助检测上溢出和下溢出。举例来说,在下面所列出的代码执行以后,pData被设为堆中的一个地址,*pData被设为0xCDCDCDCDfence1fence2被设为OxFDFDFDFD

    float* pData = new float;

    int fence1 = *((int*)pData) - 1);

    int fence2 = *(int*)(((char*)pData) + sizeof(float));

    而当下面的代码被执行以后,

    delete pData;

    *pData接着被设记为0xDDDDDDDD。如果你没有选中调试堆选项中的_CRTDBG_DELAY_FREE_MEM_DF(将会在本章稍后一些的地方讲到),而且打算重新计算fence1fence2的值,就会发现它们也被设成了0xDDDDDDDD,这是因为在内部数据结构中也使用了释放数据的字节标识。

    正如在运行时刻函数库的源代码(Crt\Src\Dbgheap.c)中所描述的,对这些标识的选择要十分仔细,这样才能暴露尽可能多的程序错误。这些标识被特意选择具有以下特征:非零(以和己经被初始化的数据产生对比)、常量(使程序错误可重现)、奇数(Macintosh上对奇地址的访问会导致自陷(trap),那么为什么不这样做呢)、大数(使其大于一个进程可能使用的地址空间,从而导致非法内存访问)而且不具规则性(因为这些标识不应该频繁位于真实的数据中)。而与之产生对比的是,将内存中的内容初始化为零会掩盖程序中的错误,而将其初始化为一个随机数则会产生随机的程序错误。

    这些字节标识还十分便于记忆,这一点可能是其最重要的属性了。如果你发现一个程序尝试对一个内容为0xCDCDCDCD或者0xDDDDDDDD的指针地址解除引用(或者是与之类似的标识,例如0xCDCDCDF0可能就是由某个错误指针加上偏移以后产生的),这时候一定是发现了一个程序错误。我还很喜欢的一点就是这些名字都有相当好的助记词。尽管微软提出的对0xCD的助记词“clear”和0xDD的助记词“dead”对笔者来说并不是很合适。其实微软可以选择更加显著的标识。举例来说,Brian
Kemighan
Rob Pike在《The Practice of Programmning》一书中推荐使用0xDEADBEEF作为标识。

    另一种帮助调试内存错误的方法就是C运行时刻函数库使用如表9.2所示的内存块类型标识符,将内存划分为五种块类型,这确定了内存信息是如何被跟踪和报告的。字节标识很有用,但是不能以规则的方式对它们进行检查;因此用户需要函数库提供如表9.3中所示的有用函数来帮助用户调试内存错误。

    9.2 Visual C++C运行时刻函数库内存块类型标识符

内存块类型

含义

_NORMAL_BLOCK

由程序直接分配的内存

_CLIENT_BLOCK

由程序直接分配的内存,可以通过内存调试函数对其拥有特殊控制权(用户还可以创建共享块的子类型,以实现高级控制)

_CRT_BLOCK

由运行时刻函数库内部分配的内存

_FREE_BLOCK

已经被释放,但是跟踪仍然被保留下来的内存、这在用户选择了调试堆的选项__CRTDBG_DELAY_FREE_MEM_DF以后会出现

_IGNORE_BLOCK

当使用_CrtDbgFlag关闭内存调试操作以后分配的内存(内存调试函数不会对这些内存块进行检查,认为它们没有错误)

    还需要注意的一点是,所有这些函数的使用范围仅仅是在调试版本中,因此不要期望一个像_CrtIsValidPointer这样的函数能够在发行版本中工作。对于发行版本,可以使用应用程序接口函数IsBadReadPtrIsBadWritePtr作为替代。与之类似,在发行版本中,使用_Heapchk函数代替了_CrtCheckMemory。表9.4列出了一些对调试內存泄漏有用的运行时刻函数库函数。最后,运行时刻函数库还提供了对一般内存调试有用的函数(见表9.5),大多数内存调试函数会在本章稍后一些的地方进行更详细的讨论。

    9.3 Visual C++C运行时刻函数库提供的帮助调试内存错误的函数

函数

用途

_CrtChcckMcmory

检査每一个内存块的内部数据结构和守护(guard)字节,以测试其完整性。对于内存错误来说十分有用,但是当调用次数过多时,可能会严重导致程序运行速度减慢

_CtiIsValidHeapPointer

检验指定指针是否存在于本地堆中

_CrtkValidPointer

检验给定内存范围对读写操作是否合法

_Cr!lsMemoryBlock

检验给定内存范围是否位于本地堆当中,是否拥有例如_NNORMAL_BLOCK这样的有效内存块类型标识符(该函数还可以被用以获得分配数目以及进行内存分配的源文件文件名和行)

    9.4
用于调试内存泄漏的Visual C++C运行时刻函数库中的函数

函数

用途

_CrtSetBreakAlloc

_crtBreakalloc=1

在给定的分配数目上分配断点,每一块被分配的内存都被指派一个连续的分配号。这对于查找特定的内存泄漏十分有用

_CrtDumpMemoryLeaks

判断一个内存泄漏是否发生。如果发生则将本地堆中所有当前分配的内存按照用户可以阅读的方式进行内存映象转储。这对于在程序结束的时候检测内存泄漏来说十分有用

_CrtMemCheckPoint

_CrtMemState结构中产生一个本地堆的当前状态的快照

_CrtMemDiffercnce

比较两个堆中的断点,并将不同之处保存在_CrtMemState结构中。如果两个断点不同,则返回真。这对于检测特殊区域代码的内存泄漏十分有用

_CrtMemDumpAllObjectsSince

将从给定堆断点或者从程序头开始分配的内存的所有信息按照用户可以阅读的方式进行内存映象转储

_CrtMeniDumpStatistics

将信息按照用户可以阅读的方式进行内存映象转储到一个_CrtMemState结构中。这一结构中可能包含着一个堆断点或者堆断点之间的差异。这对于得到被使用的动态内存的全面观察信息来说十分有用,而且对检测内存泄漏也是分有用

    9.5用于一般内存调试的Visual C++C运行时刻函数库中的函数

函数

用途

_CrtSetDbgFlag

控制内存调试函数的行为

_CrtSetAllocHook

加载一个内存分配过程中的钩子(hook)函数。对于监测内存使用状况或者模拟内存不足情况来说十分有用

_CrtSetReportHooK

加载一个进行定制报告处理的函数。对于过滤报告数据或者将报告数据发送到不同的目的地,例如将数据错误报告送发给一个消息框这样的情形很有帮助

_CrtSetDumpClient

加载一个对用户块进行内存映象转储的函数。对于将数据按照更易阅读的方式进行显示的情形很有帮助

_CrtDoForAllClientObject

对于所有作为用户块进行分配的数据,调用指定的函数

MFC内存调试

    正如我在前面所提到的,Visual C++C运行时刻函数库所支持的内存调试最开始是MFC的一部分。最初的MFC内存调试支持被保留下来,但是现在它大部分仅仅是对运行时刻函数库中函数的简单包装。

    MFC控制的内存有一个不同,那就是new函数被设定为默认在失败时抛出一个异常。在MFC中,用户必须通过使用AfxSetNewHandIer而不是_set_new_handler改变分配失败时候的行为。默认处理异常的函数是AfxNewHandler,它会抛出类型为CMemoryException的异常。在十分必要(我们希望它永远不会发生)的情况下,用户可以通过调用AfxSetNewHandler(0)来使new在失败的时候返回一个空指针,同样,在MFC中,用户可以直接调用AfxThrowMemoryException函数来抛出一个内存异常。读者可以阅读AfxMem.cpp的源代码以了解更多的细节。

    MFC控制的内存还有另外一个不同,那就是MFC重载了CObjett::operator newCObject::operator
delete
,这样就给所有由CObject派生的对象加上了一个_CLIENT_BLOCK的内存块类型标识符号。最主要的是,这就可以允许MFC调用_CrtSetDumpClient来如载_AfxCrtDumpClient函数(位于Dumpinit.cpp),这样使用CObject::Dump虚函数就可以对有效的从CObject派生的对象进行内存映象转储,用户由此可以得到更有用的内存错误分析信息(实现这一函数的技巧在第4章“使用调试语句”中已经介绍过)

    对于那些由CObject派生的类实现内存映象转储虚函数,以得到更加有用的内存借误分析信息。

    然而,在默认情况下,_AfxCrtDumpCIient函数并不会调用虚函数CObject::Dump,准确一些说,该函数将一个对象的类名、指针值和大小进行内存映象转储,我猜测之所以采用这样的默认设定,是因为一些例如数组、表和图这样的对象可能具有相当巨大的内存映象转储。为了使用内存映象转储虚函数,用户必须向自己的代码中添加下列代码,而且,最好是添加在初始化的地方。

    #ifdef _DEBUG

        afxDump.SetDepth(1);    // dump CObjects using a deep dump

    #endif

    为了在内存错误分析中使用内存映象转储虚函数,用户必须调用afxDump.SetDepth(1)函数。

    很多内存映象转储函数还使用了深度值来决定是使用浅度策略还是深度策略。举例来说,类C采用下面的方法实现其内存映象转储:

    void CObLisL::Dump(CDumpContext &dc) const {

        CObjecL::Dump(dc)

        dc << "with " << m_nCount << " elements";

        if(dc.GetDepth() > 0) {

            POSITION pos = GetHeadPosition();

            while(pos != NNLL)

                dc << "\n\t" << CetNext(pos);

        }

        dc << "\n";

    }

    我不知道为什么微软选择了这种极端的处理方法,看上去用户好像应该可以在使用内存映象转储虚函数的时候选择浅度策略进行内存映象转储。

    最后要说的是,_AfxCrtDumpClient函数会将所有内容内存映象转储到afxDump以及MFC预定义的全局变量CDumpContext,这表明MFC的调试堆输出由Visual
C++
跟踪工具进行控制。如果你没有得到预期的内存映象转储信息,那么运行跟踪程序确信打开了Enable tracing选项

    由于MFC对内存的支持大部分仅仅是对C运行时刻函数库中函数的简单包装,因此没有必要再重复我在前面己经介绍过的内容了。作为替代,表9.6给出了MFCC运行时刻函数库的内存调试函数转换于册:

    9.6 MFCC运行时刻函数库的内存调试函数转换手册

MFC函数

C运行时刻函数库

AfxCheckMemory

_CrtChcckMemory

AfxDoForAllObjects

_CrtDoForAllClientObjccts

AfxDumpMemoryLeaks

_CrtDumpMemoryLeaks

AfxIsMemoryBlock

_CrtIsMemoryBlock

CMemoryState::Checkpoint

_CrtMemCheckpoint

CMemoryState::Difference

_CrtMemDiffercnce

CMemoryState::DumpAllObjectsSince

_CrtMemDumpAllObjectsSince

CMemoryState::DumpStatistics

_CrtMemDumpStatistics

AfxSetAllocStop

_CrtSetBreakAlloc

AfxEnableMemoryTracking

_CrtSetDbgFlag

AfxMemDF

_CrtSetDbgFlag

AfxSetNewHandler

_set_new_handler

    那么,在MFC程序中,用户究竟应该选择那一组内存调试函数呢?如果用户在维护一个己有的MFC程序,并且这个程序己经使用了MFC函数,那么应该继续使用MFC函数。如果用户将要开发新的程序,那么我会推荐使用C运行时刻函数库的函数。虽然使用MFC函数可能会稍微方便一些,但是如果使用C运行时刻函数库的函数,你的程序会具有更好的可移植性(至少对于那些非MFC的程序来说是这样),而且使用库函数可以给予用户更多的控制能力

    在选择内存调试函数时,C运行时刻函数库要比MFC更好一些。

ATL内存调试

    ATL对内存调试的贡献在于,它维护了一张表,该表由所有用QueryInterface创建的COM接口指针构成,可以通过跟踪AddRefRelease函数调用对其生命周期进行监视。当COM服务器停止运行的时候,任何没有被释放的接口都是一个资源泄露。用户可以在StdAfx.h的头部(#include<atlcom.h>之前)定义控制预处理的常量_ATL_DEBUG_INTERFACES,这样就可以对接口资源泄漏进行跟踪。这样的设定会对所有AddRefRelease函数调用进行跟踪,将当前引用数目、类名以及被引用的接口名显示出来。

    这里介绍了怎样调试组件程序中的接口资源泄漏。在程序结束的时候,在调试语句输出窗口中检查那些标记着“INTERFACE LEAK”的文本,如下所示:

    INTERFACE LEAK: RefCount = 7, MaxRefCount = 10, {Allocation = 42}

        CMyComClass    Leak

    接下来在服务器初始化的时候对CComModule对象的m_nIndexBreakAt成员变量进行设置,就可以使用分配数目(allocation number)来帮助査找资源泄漏。例如:

    #define _ATL_DEBUG_INTERFACE // define in StdAfx.h

    BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID) {

        if(dwReason == DLL_PROCESS_ATTACH) {

            ...

            _Module.m_nIndexBreakAt = 42; // set breakpoint to find interface leak

        }

        return TRUE;

    }

    在用户下一次运行程序的时候,调试器会在分配数目到达设定值的时候中断程序运行。当然,这一技术要求分配数目在各个程序实例之间保持固定数值,而这样的情况并不是总能被保证。

9.3 使用调试堆

    到现在为止一切都很顺利,但是用户如何在自己的程序中使用调试堆呢?为了使用调试堆,用户必须确定自己使用的是程序的调试版本,并且链接的是C运行时刻函数库的调试版本,另外,也必须定义_DEBUG,这样调试堆版本的newdelete才会被调用。使用了这些设置以后,用户的程序就可以检测内存错误和内存泄漏。但不幸的是,到现在为止工作还没有完全结束:用户必须采用一些额外的步骤,选择所需要的额外测试选项,在程序结束的时候显示内存,正确报告源文件名和行号,并且使用便于阅读的方式显示数据信息。

调试堆选项

    在说明怎样让用户的程序显示内存泄露之前,我必须首先介绍一些可用的调试堆选项。用户使用_CrtSetDbgFlag函数对调试堆的检查工作进行控制。下面是是这些选项,它们可以一起进行或运算,从而实现多个选项的同时选取。

_CRTDBG_ALLOC_MEM_DF:启动堆分配检查。当这一选项关闭时,内存分配的处理大部分都相同,但要使用_IGNORE_BLOCK类型的内存块。这个选项最好用在将所有的堆检査都关闭,或者你希望忽略能确信是正确的特定内存分配,或者就是你所希望忽略的内存分配(默认情况下打开)时。

_CRTDBG_DELAY_FREE_MEM_DF:阻止内存被真正释放。它用来检査访问己被释放内存的错误或者用来模拟底层内存行为。注意,即使该选项没有打开,已经释放的内存也还是用0xDD的字节模式来填充的;但该选项没有打开时,被释放的内存会很快被重用,这使得访问己被释放内存的错误很难被査出。而且,_CrtCheckMemory在该选项关闭时是不检查己被释放内存的(默认情况下关闭)

_CRTDBG_CHECK_ALWAYS_DF:使得每次内存分配和内存释放时_CrtCheckMemory都会被调用。这对调试内存破坏很有帮助,但当程序分配了很多内存时,会导致运行速度下降很多(默认情况下关闭)

_CRTDBG_CHECK_CRT_DF:使得类型为_CRT_BLOCK的内存块在内存泄漏检查和状态差异检查时会被检查。通常我们不会打开这个选项,因为它会将运行时刻函数库里的某些内存误认成是被泄漏的内存,这些内存直到程序结束时才会被释放。只有当怀疑运行时刻函数库里有内存泄露时才会使用这个选项,通常情况下它不会被使用(默认关闭)

_CRTDBG_LEAK_CHECK_DF:使得在程序结束时_CrtDumpMemortLeaks自动被调用(默认情况下关闭)

    我推荐用户总是使用_CRTDBG_ALLOC_MEM_DF_CRTDBG_LEAK_CHECK_DF内存堆调试选项,而仅仅在帮助调试内存错误的时候才选择_CRTDBG_CHECK_ALWAYS_DF_CRTDBG_DELAY_FREE_MEM_DF选项。很有可能用户在认为自己的程序中存在内存泄漏的时候,仅仅使用_CRTDBG_CHECK_CRT_DF选项,但是这时候_CrtDumpMemoryLeaks不会报告任何信息。

显示内存泄漏

    使用程序的调试版本时,用户的程序能够对内存泄漏进行内存映象转储,但是这一功能并不是默认提供的。用户可以在程序结束的时候显式地调用_CrtDumpMemoryLeaks对内存泄漏进行内存映象转储,但是使用这种方法存在一个问题:用户的程序可能会存在多种不同的退出方式,这就需要用户在多个不同的地方都进行这个调用。因此,最简单的方法是使用_CRTDBG_LEAK_CHECK_DF这个调试堆选项。举例来说,如果用户向自己的程序中添加了下列代码:

    #include <crtdbg.h> // include to call _Crt functions

    int APIENTRT WinMain(...) {

        _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

        ...

    }

    那么内存泄漏会以下面所示的方式报告给用户

    Detected memory leaks!

    Dumping objects ->

    {21} normal block at 0x00780DA0, 8 bytes long.

    Data: <J M ! @ > 4A D8 12 4D FB 21 09 40

    ...

    Object dump complete.

    如果用户不能确信内存泄漏是否被报告,可以进行一个小的测试,故意地泄漏一些内存(举个例子来说,向自己的代码中添加int* pLeak = new int;而不加上对应的delete语句),然后看看在程序结束的时候是否报告内存泄漏。

    这是一个好的开始,但是如果将源代码文件名和行号信息都显示出来,这些内存泄漏的调试将会变得容易很多。用户可以通过向StdAfx.h的头部添加下面列出的语言以显示源代码信息:

    #define _CRTDBG_MAP_ALLOC // define to get line number

    #include <stdlib.h>       // define the memory allocation functions

抱歉!评论已关闭.