使用C运行时库查找内存泄漏

@Visual Studio 2010

内存泄漏定义为没有正确释放先前分配的内存,是C/C++程序中是最难检测的Bug。一个小的内存泄漏开始时没有被察觉,随着时间推移,累积的内存泄漏可能造成性能降低,进而程序超出内存导致崩溃。更糟的是,一个泄漏的程序吃尽所有可以获得的内存后可能造成其他程序崩溃,从而给查找原因造成迷惑。甚至,看起来无害的内存泄漏可能是应该被修正的其他问题的一种症状。

Visual Studio 的调试器(debugger)和 C 运行时(CRT)库提供了一些侦测和鉴别内存泄漏的方法。

打开内存泄漏检测

检验内存泄漏的主要工具就是调试器和C运行时库的调试堆函数(debug heap functions)。

为了打开调试堆函数,需要在程序中包含如下语句

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

为了CRT函数正确运行, #include 语句顺序必须如上所示。

crtdbg.h 会将 mallocfree 函数映射到他们的调试版本 _malloc_dbgfree 。它们会追踪内存的分配和释放。这种映射只在调试版(debug)本的构建(build)时作用,发布版(release)依然使用正常的 mallocfree 函数。调试版构建时, _DEBUG 宏会被定义。

#define _CRTDBG_MAP_ALLOC语句将基本版本的CRT堆函数映射到相应的调试版。如果没有定义这个语句,内存泄漏的信息会不详细。

使用上述语句打开调试堆函数之后,在程序的出口处调用 _CrtDumpMemoryLeaks ,以便在程序退出时生成内存泄漏报告

_CrtDumpMemoryLeaks();

如果程序有多个出口,你不需要手工为每个出口添加 _CrtDumpMemoryLeaks 调用。只要在程序的开始处调用 _CrtSetDbgFlag ,将会在每个出口处自动的调用 _CrtDumpMemoryLeaks 。只需要设置两个位域标识:

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

默认情况, _CrtDumpMemoryLeaks 将内存泄漏报告输出到VS的输出窗口(output window)的调试框(debug pane)内。你也可以使用 _CrtSetReportMode 将报告重定向到其他的位置。

如果你使用了一个库,此库可能重置输出到其他位置,这时,你可以将输出位置设回输出窗口,如下所示:

_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG);

内存泄漏报告解释

如果程序中没有定义 _CRTDBG_MAP_ALLOC 宏, _CrtDumpMemoryLeaks 显示的内存泄漏的报告如下所示:

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 宏,内存报告如下所示:

Detected memory leaks!
Dumping objects ->
C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.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.

两份报告的区别在于泄漏内存初始分配时所在的文件名和行号。

无论是否定义了 _CRTDBG_MAP_ALLOC ,内存泄漏报告中都将显示如下的信息:

内存泄漏报告将内存块识别为normal,client或者CRT。normal块是你的程序中分配的普通块。client块是一种特殊的内存块,在MFC程序中,它被拥有析构函数的对象使用。MFC中的 new 操作符根据被创建对象的性质可能创建normal块或者client块。CRT块由CRT库分配,为己所用。CRT库处理这些块的析构。因此你不可能看着这种类型的内存泄漏报告,除非非常严重的错误,比如CRT库损坏。

有其他两种类型的内存块也从来不会出现在内存泄漏报告中。free块是已经被释放的内存。那意味着它不是定义中的泄漏。ignore块是你显式标记为从内存泄漏报告中剔除的内存。

这些技术对用标准CRT malloc 函数分配的内存有效。然而,如果你的程序中使用C++的 new 操作符来分配内存,你需要重新定义 new 才能在内存泄漏报告中看到文件名和行号。你可以按如下方式操作:

#ifdef _DEBUG
#  ifndef DBG_NEW
#    define DBG_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__)
#    define new DBG_NEW
#  endif
#endif       // _DEBUG

在内存分配号处设置断点

内存分配号表示了泄漏的内存何时被分配。例如内存分配号为18的块是程序运行期间第18次分配的内存块。CRT报告统计了所有运行期间的内存块的分配。这包括CRT库和其他库(比如MFC)分配的内存。因此,内存分配号为18的一块内存可能不是你的代码第18次分配的,通常情况下都不是。

你可以使用内存分配号在内存分配时设置一个断点。

使用观察窗口(watch window)设置内存分配断点。

  1. 在靠近程序起始位置设置一个断点,然后运行程序。
  2. 当程序在断点处中断,切换到观察窗口。
  3. 在观察窗口的名称列键入 _crtBreakAlloc 。如果你使用的是多线程dll版本的CRT库(/MD选项),应包括上下文操作符{,,msvcr100d.dll}_crtBreakAlloc
  4. 回车。调试器会计算这个调用,将结果置于值列中。如果没有在内存分配中设置任何断点,值为-1
  5. 在值列中,将值替换为你想要中断的内存分配号。

在内存分配号处设置完断点后,继续调试。仔细的运行程序以便同先前的运行条件相同,从而保证内存分配顺序也不会改变。当你的程序在指定内存分配位置处中断时,你可以使用调用栈(call stack)窗口和其他的调试窗口去确定内存分配的条件。然后,你可以继续执行来观察这个对象做了什么和确定它为什么没有被释放。

设置位于对象上的数据断点可能会很有用。

你也可以在代码中设置内存分配断点。有两种方式使用:

_crtBreakAlloc = 18;

或者

_CrtSetBreakAlloc(18);

比较内存状态

定位内存泄漏的另外一种技术是在关键点处对程序的内存做一次快照。为了在程序的给定点处的内存状态快照,要创建一个 _CrtMemState 结构体然后将其传递给 _CrtMemCheckpoint 函数。此函数会用当前内存状态的快照填充这个结构体:

_CrtMemState s1;
_CrtMemCheckpoint(&s1);

_CrtMemCheckpoint 用当前内存状态的快照填充这个结构体。

为了输出 _CrtMemState 结构体的内容,将结构体传递给 _CrtMemDumpStatistics 函数。

_CrtMemDumpStatics(&s1);

_CrtMemDumpStatics 输出内存状态如下所示:

0 bytes in 0 Free Blocks.
0 bytes in 0 Normal Blocks.
3071 bytes in 16 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 3071 bytes.
Total allocations: 3764 bytes.

为了确定内存泄漏是否位于一段代码中,你可以在这段代码的前后对内存做快照,然后使用 _CrtMemDifference 去比较两个状态:

_CrtMemCheckpoint(&s1);
// memory allocations take place here
_CrtMemCheckpoint(&s2);
 
if (_CrtMemDifference(&s3, &s1, &s2))
     _CrtMemDumpStatistics(&s3);

_CrtMemDifference 比较内存状态 s1s2 然后将两者的差别置于 s3 中返回。

查找内存泄漏的一项技术是在程序开始和结束处放置 _CrtMemCheckpoint ,然后用 _CrtMemDifference 比较结果。如果 _CrtMemDifference 显示了内存泄漏,你可以使用二分搜索的策略增加更多的 _CrtMemCheckpoint 来分割你的程序直到你定位到内存泄漏的出处。

误报

某些情况下, _CrtDumpMemoryLeaks 可能给出内存泄漏的错误指示。如果你使用了一个库,它标记内部分配为 _NORMAL_BLOCK s而不是 _CRT_BLOCK s或者 _CLIENT_BLOCK s,这时就可能发生。在这种情况下, _CrtDumpMemoryLeaks 是不能区分用户分配和内部库分配的。如果库分配的全局析构在你调用 _CrtDumpMemoryLeaks 之后运行,每一个内部库分配都会被报告为内存泄漏。比Visual Studio .NET更早的标准模板库的老版本,会使 _CrtDumpMemoryLeaks 报告这些误报,但是这在最近的版本中已经修正。

原文链接 中文链接 MFC相关

12 June 2014

blog comments powered by Disqus