Windows平台下的内存泄漏检测
创始人
2024-05-04 08:26:15
0

Windows平台下的内存泄漏检测

  • 一、使用_CrtDumpMemoryLeaks定位内存泄露
    • 添加对应的头文件
    • 转储内存泄漏信息
    • 程序任意点退出
    • 指定调试信息输出
  • 二、定位具体内存泄露位置
    • 内存快照
    • 转储内存快照
    • 比较内存快照
    • 完整例子
  • 三、使用WinDbg定位
    • 获取堆信息
    • 查看指定堆的使用情况
    • 获取地址信息
    • 获取地址调用堆栈

请添加图片描述
更多资讯、知识,微信公众号搜索:“上官宏竹”。


Windows平台下面Visual Studio 调试器和 C 运行时 (CRT) 库为我们提供了检测和识别内存泄漏的有效方法,原理大致如下:内存分配要通过CRT在运行时实现,只要在分配内存和释放内存时分别做好记录,程序结束时对比分配内存和释放内存的记录就可以确定是不是有内存泄漏。

一、使用_CrtDumpMemoryLeaks定位内存泄露

添加对应的头文件

在程序中包括以下语句: (#include 语句必须采用上文所示顺序。 如果更改了顺序,所使用的函数可能无法正常工作。)

#define _CRTDBG_MAP_ALLOC
#include 
#include 

通过包括 crtdbg.h,将 mallocfree 函数映射到它们的调试版本,即 _malloc_dbg _free_dbg,这两个函数将跟踪内存分配和释放。 此映射只在调试版本(在其中定义了_DEBUG)中发生。 发布版本使用普通的 malloc 和 free 函数。
_CRTDBG_MAP_ALLOC在应用程序的调试版本中定义标志时,堆函数的基本版本将直接映射到其调试版本。 该标志在 Crtdbg.h 中用于执行映射。 此标志仅在应用程序中定义标志时才 _DEBUG 可用。并非绝对需要该语句;但如果没有该语句,内存泄漏转储包含的有用信息将较少。

转储内存泄漏信息

在程序退出前调用_CrtDumpMemoryLeaks()函数来转储内存泄漏信息。

#define _CRTDBG_MAP_ALLOC
#include 
#include #include 
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!"<

控制台上会输出检测到内存泄露的信息,如下:

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 
#include #include 
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!"<

程序输出如下,这说明在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 -sFailed to read heap keySEGMENT HEAP ERROR: failed to initialize the extentionNtGlobalFlag enables following debugging aids for new heaps:stack back tracesLFH Key                   : 0xe48d63c61a6de263Termination on corruption : ENABLEDHeap     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 -sFailed to read heap keySEGMENT HEAP ERROR: failed to initialize the extentionNtGlobalFlag enables following debugging aids for new heaps:stack back tracesLFH Key                   : 0xe48d63c61a6de263Termination on corruption : ENABLEDHeap     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 000001e134530000heap @ 000001e134530000group-by: TOTSIZE max-display: 20size  #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 @ 1e134530000HEAP_ENTRY Size Prev Flags            UserPtr UserSize - state000001e134546ce0 013f 0000  [00]   000001e134546d10    013bc - (busy)unknown!noop000001e1345480d0 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 @ 1e134530000HEAP_ENTRY Size Prev Flags            UserPtr UserSize - state000001e134546ce0 013f 0000  [00]   000001e134546d10    013bc - (busy)unknown!noop7ff9c9d3d6c3 ntdll!RtlpAllocateHeapInternal+0x00000000000947d37ff9730dd480 ucrtbased!heap_alloc_dbg_internal+0x00000000000002107ff9730dd20d ucrtbased!heap_alloc_dbg+0x000000000000004d7ff9730e037f ucrtbased!_malloc_dbg+0x000000000000002f7ff9730e0dee ucrtbased!malloc+0x000000000000001e7ff60b1c1f73 Test!operator new+0x00000000000000137ff60b1c19f3 Test!operator new[]+0x00000000000000137ff60b1c1e10 Test!Bad::AllocMemory+0x00000000000000407ff60b1c4746 Test!main+0x00000000000000467ff60b1c1eb9 Test!invoke_main+0x00000000000000397ff60b1c1d5e Test!__scrt_common_main_seh+0x000000000000012e7ff60b1c1c1e Test!__scrt_common_main+0x000000000000000e7ff60b1c1f4e Test!mainCRTStartup+0x000000000000000e7ff9c83354e0 KERNEL32!BaseThreadInitThunk+0x00000000000000107ff9c9c8485b ntdll!RtlUserThreadStart+0x000000000000002b

在这里,我们看到了这个堆的调用堆栈,Test!Bad::AllocMemory,确实是我们分配没有释放的内存空间。这就是这个堆块分配的堆栈信息,通过这个信息,我们就可以定位到这块内存是哪里分配的,然后再到相应的函数里面去分析。
真正在项目中,情况远没有这种简单,有时候,打印出来的堆信息就有很长一串,这就需要在这些信息里面去找有用的信息的。

参考:Windows下检测内存泄露的方法


请添加图片描述

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...