Windows平台下的内存泄漏检测
- 一、使用_CrtDumpMemoryLeaks定位内存泄露
- 添加对应的头文件
- 转储内存泄漏信息
- 程序任意点退出
- 指定调试信息输出
- 二、定位具体内存泄露位置
- 内存快照
- 转储内存快照
- 比较内存快照
- 完整例子
- 三、使用WinDbg定位
- 获取堆信息
- 查看指定堆的使用情况
- 获取地址信息
- 获取地址调用堆栈
更多资讯、知识,微信公众号搜索:“上官宏竹”。
Windows平台下面Visual Studio 调试器和 C 运行时 (CRT) 库为我们提供了检测和识别内存泄漏的有效方法,原理大致如下:内存分配要通过CRT在运行时实现,只要在分配内存和释放内存时分别做好记录,程序结束时对比分配内存和释放内存的记录就可以确定是不是有内存泄漏。
一、使用_CrtDumpMemoryLeaks定位内存泄露
添加对应的头文件
在程序中包括以下语句: (#include 语句必须采用上文所示顺序。 如果更改了顺序,所使用的函数可能无法正常工作。)
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
通过包括 crtdbg.h
,将 malloc
和 free
函数映射到它们的调试版本,即 _malloc_dbg
和 _free_dbg
,这两个函数将跟踪内存分配和释放。 此映射只在调试版本(在其中定义了_DEBUG)中发生。 发布版本使用普通的 malloc 和 free 函数。
_CRTDBG_MAP_ALLOC
在应用程序的调试版本中定义标志时,堆函数的基本版本将直接映射到其调试版本。 该标志在 Crtdbg.h
中用于执行映射。 此标志仅在应用程序中定义标志时才 _DEBUG
可用。并非绝对需要该语句;但如果没有该语句,内存泄漏转储包含的有用信息将较少。
转储内存泄漏信息
在程序退出前调用_CrtDumpMemoryLeaks()
函数来转储内存泄漏信息。
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include <iostream>
using namespace std;
void GetMemory(char *p, int num)
{
p = (char*)malloc(sizeof(char) * num);
}
int main(int argc,char** argv)
{
char *str = NULL;
GetMemory(str, 100);
cout<<"Memory leak test!"<<endl;
_CrtDumpMemoryLeaks();
return 0;
}
控制台上会输出检测到内存泄露的信息,如下:
Detected memory leaks!
Dumping objects ->
E:\code\ConsoleApplication1\ConsoleApplication1.cpp(10) : {151} normal block at 0x01604E58, 100 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
输出的信息包括下面几条:
- 内存分配编号
{151} - 块类型(普通、客户端或 CRT)
normal block
普通块”是由程序分配的普通内存。
“客户端块”是由 MFC 程序用于需要析构函数的对象的特殊类型内存块。 MFC new 操作根据正在创建的对象的需要创建普通块或客户端块。
“CRT 块”是由 CRT 库为自己使用而分配的内存块。 CRT 库处理这些块的释放,因此您不大可能在内存泄漏报告中看到这些块,除非出现严重错误(例如 CRT 库损坏)。
从不会在内存泄漏信息中看到下面两种块类型:
“可用块”是已释放的内存块。
“忽略块”是您已特别标记的块,因而不出现在内存泄漏报告中。 - 十六进制形式的内存位置。
0x01604E58 - 以字节为单位的块大小。
100 bytes long - 前 16 字节的内容(亦为十六进制)。
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
如果没有使用 #define _CRTDBG_MAP_ALLOC 语句,则不会显示在其中分配泄漏的内存的文件,如上述的E:\code\ConsoleApplication1\ConsoleApplication1.cpp(10)
信息。
程序任意点退出
如果程序总是在同一位置退出,调用 _CrtDumpMemoryLeaks
将非常容易。 如果程序从多个位置退出,则无需在每个可能退出的位置放置对 _CrtDumpMemoryLeaks
的调用,而可以在程序开始处包含以下调用:
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
该语句在程序退出时自动调用 _CrtDumpMemoryLeaks
。 必须同时设置 _CRTDBG_ALLOC_MEM_DF
和 _CRTDBG_LEAK_CHECK_DF
两个位域。
指定调试信息输出
默认情况下,_CrtDumpMemoryLeaks
将内存泄漏信息 dump 到 Output 窗口的 Debug 页, 如果你想将这个输出定向到别的地方,可以使用 _CrtSetReportMode
进行重置。
_CrtSetReportMode
指定的目标生成的特定报表类型,需要在debug环境下才会生效。
int _CrtSetReportMode(
int reportType,
int reportMode
);
参数
- reportType 报告类型
报告类型 | 描述 |
---|---|
_CRT_WARN | 不需要立即关注的警告、消息和信息。 |
_CRT_ERROR | 错误、不可恢复的问题和需要立即关注的问题。 |
_CRT_ASSERT | 断言失败 (断言表达式的计算结果为FALSE)。 |
- reportMode 新报告模式
报告模式 | _CrtDbgReport 行为 |
---|---|
_CRTDBG_MODE_DEBUG | 将消息写入调试器的输出窗口。 |
_CRTDBG_MODE_FILE | 将消息写入用户提供的文件句柄。 应调用 _CrtSetReportFile 来定义要用作目标的特定文件或流。 |
_CRTDBG_MODE_WNDW | 创建一个消息框显示消息以及中止,重试,并忽略按钮。 |
返回值
成功完成后, _CrtSetReportMode
返回上一个报告模式中指定的报告类型reportType
。 如果为传入的值无效reportType
或为指定无效模式reportMode
, _CrtSetReportMode
调用无效参数处理程序作为中所述参数验证。 如果允许执行继续,此函数可设置errno到EINVAL并返回-1。
二、定位具体内存泄露位置
通过上面的方法,我们几乎可以定位到是哪个地方调用内存分配函数malloc和new等,如上例中的GetMemory函数中,即第10行!但是不能定位到,在哪个地方调用GetMemory()导致的内存泄漏,而且在大型项目中可能有很多处调用GetMemory。如何要定位到在哪个地方调用GetMemory导致的内存泄漏?
定位内存泄漏的另一种技术涉及在关键点对应用程序的内存状态拍快照。 CRT 库提供一种结构类型 _CrtMemState
,您可用它存储内存状态的快照。
内存快照
_CrtMemCheckpoint
对给定点的内存状态拍快照,它会把当前内存的快照填充到 _CrtMemState
结构中。
void _CrtMemCheckpoint(
_CrtMemState *state
);
转储内存快照
通过向 _CrtMemDumpStatistics
函数传递 _CrtMemState
结构,可以在任意点转储该结构的内容。
比较内存快照
若要确定代码中某一部分是否发生了内存泄漏,可以在该部分之前和之后对内存状态拍快照,然后使用 _CrtMemDifference
比较这两个状态:
_CrtMemState s1, s2, s3;
_CrtMemCheckpoint( &s1 );
// memory allocations take place here
_CrtMemCheckpoint( &s2 );
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );
顾名思义,_CrtMemDifference
比较两个内存状态(s1 和 s2),生成这两个状态之间差异的结果(s3)。 在程序的开始和结尾放置 _CrtMemCheckpoint
调用,并使用_CrtMemDifference
比较结果,是检查内存泄漏的另一种方法。
如果检测到泄漏,则可以使用 _CrtMemCheckpoint
调用通过二进制搜索技术来划分程序和定位泄漏。
完整例子
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include <iostream>
using namespace std;
_CrtMemState s1, s2, s3;
void GetMemory(char *p, int num)
{
p = (char*)malloc(sizeof(char) * num);
}
int main(int argc,char** argv)
{
_CrtMemCheckpoint( &s1 );
char *str = NULL;
GetMemory(str, 100);
_CrtMemCheckpoint( &s2 );
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );
cout<<"Memory leak test!"<<endl;
_CrtDumpMemoryLeaks();
return 0;
}
程序输出如下,这说明在s1和s2之间存在内存泄漏!
0 bytes in 0 Free Blocks.
100 bytes in 1 Normal Blocks.
0 bytes in 0 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 0 bytes.
Total allocations: 100 bytes.
Detected memory leaks!
Dumping objects ->
E:\code\ConsoleApplication1\ConsoleApplication1.cpp(12) : {151} normal block at 0x008A4E58, 100 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
三、使用WinDbg定位
使用WinDbg定位内存泄露,主要是使用到它的扩展命令!heap
。
内存泄露代码如下:
class Bad
{
public:
void AllocMemory()
{
for (auto i = 0; i < 100; ++i)
{
char* p = new char[5000];
}
}
};
int main()
{
Bad b;
b.AllocMemory();
return 0;
}
很简单吧,可以一眼看出哪儿有内存泄露,现在我们就来看看WinDbg是怎么去发现的。
获取堆信息
在程序执行前,我们先看一下堆的情况。在WinDbg命令行中输入!heap -s显示所有堆的摘要信息:
0:000> !heap -s
Failed to read heap keySEGMENT HEAP ERROR: failed to initialize the extention
NtGlobalFlag enables following debugging aids for new heaps:
stack back traces
LFH Key : 0xe48d63c61a6de263
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-------------------------------------------------------------------------------------
000001e134530000 08000002 1220 60 1020 2 2 1 0 0 LFH
000001e134500000 08008000 64 4 64 2 1 1 0 0
-------------------------------------------------------------------------------------
然后按F5执行程序,命中断点后停下来。再来看一下堆信息:
0:000> !heap -s
Failed to read heap keySEGMENT HEAP ERROR: failed to initialize the extention
NtGlobalFlag enables following debugging aids for new heaps:
stack back traces
LFH Key : 0xe48d63c61a6de263
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-------------------------------------------------------------------------------------
000001e134530000 08000002 1220 652 1020 24 8 1 0 0 LFH
000001e134500000 08008000 64 4 64 2 1 1 0 0
-------------------------------------------------------------------------------------
这里我们看到,地址为0x000001e134530000的堆有明显增长,之前Commit是60K,现在是652K。
查看指定堆的使用情况
然后,我们使用命令!heap -stat -h 000001e134530000
进行查看,其中参数-stat
表示显示指定堆的使用情况统计信息,-h
指定要查看的堆地址,这里是0x000001e134530000
。
0:000> !heap -stat -h 000001e134530000
heap @ 000001e134530000
group-by: TOTSIZE max-display: 20
size #blocks total ( %) (percent of total busy bytes)
13bc 64 - 7b570 (90.02)
1cf0 1 - 1cf0 (1.32)
30 8d - 1a70 (1.21)
1234 1 - 1234 (0.83)
1034 1 - 1034 (0.74)
df4 1 - df4 (0.64)
...
我们看到,大小为0x13bc的块有0x64个,总大小0x7B570, 占整个正在使用块的90.02%。我们怀疑这些块就是泄露的块。
获取地址信息
接下来我们获取这些块的地址。使用命令!heap -flt s 13bc
。其中-flt
将显示范围限定为指定大小或大小范围的堆,参数s 13bc
就是指定大小为0x13bc的块。
0:000> !heap -flt s 13bc
_HEAP @ 1e134530000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
000001e134546ce0 013f 0000 [00] 000001e134546d10 013bc - (busy)
unknown!noop
000001e1345480d0 013f 013f [00] 000001e134548100 013bc - (busy)
000001e1345494c0 013f 013f [00] 000001e1345494f0 013bc - (busy)
000001e13454a8b0 013f 013f [00] 000001e13454a8e0 013bc - (busy)
000001e13454bca0 013f 013f [00] 000001e13454bcd0 013bc - (busy)
...
这里我只截取了部分数据,其实这儿比较长。这里我们会看到很多状态为busy的堆块,这些堆块应该就是没有释放的内存空间。
获取地址调用堆栈
我们使用!heap -p -a 000001e134546ce0
,来输出一下它的调用堆栈:
0:000> !heap -p -a 000001e134546ce0
address 000001e134546ce0 found in
_HEAP @ 1e134530000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
000001e134546ce0 013f 0000 [00] 000001e134546d10 013bc - (busy)
unknown!noop
7ff9c9d3d6c3 ntdll!RtlpAllocateHeapInternal+0x00000000000947d3
7ff9730dd480 ucrtbased!heap_alloc_dbg_internal+0x0000000000000210
7ff9730dd20d ucrtbased!heap_alloc_dbg+0x000000000000004d
7ff9730e037f ucrtbased!_malloc_dbg+0x000000000000002f
7ff9730e0dee ucrtbased!malloc+0x000000000000001e
7ff60b1c1f73 Test!operator new+0x0000000000000013
7ff60b1c19f3 Test!operator new[]+0x0000000000000013
7ff60b1c1e10 Test!Bad::AllocMemory+0x0000000000000040
7ff60b1c4746 Test!main+0x0000000000000046
7ff60b1c1eb9 Test!invoke_main+0x0000000000000039
7ff60b1c1d5e Test!__scrt_common_main_seh+0x000000000000012e
7ff60b1c1c1e Test!__scrt_common_main+0x000000000000000e
7ff60b1c1f4e Test!mainCRTStartup+0x000000000000000e
7ff9c83354e0 KERNEL32!BaseThreadInitThunk+0x0000000000000010
7ff9c9c8485b ntdll!RtlUserThreadStart+0x000000000000002b
在这里,我们看到了这个堆的调用堆栈,Test!Bad::AllocMemory,确实是我们分配没有释放的内存空间。这就是这个堆块分配的堆栈信息,通过这个信息,我们就可以定位到这块内存是哪里分配的,然后再到相应的函数里面去分析。
真正在项目中,情况远没有这种简单,有时候,打印出来的堆信息就有很长一串,这就需要在这些信息里面去找有用的信息的。
参考:Windows下检测内存泄露的方法