Windows下检测内存泄露的方法

        工作也有一些年头了,一直在使用C++做程序,程序做了不少,BUG也改了不少。这里面,内存泄露的跟踪,调试和修改是最耗时间的工作之一。这些年,还是积累了一些经验,在这里记录一下,也算做个总结。

        现在的高级语言,大多都有垃圾回收机制,除非语言本身的缺陷,一般不会遇到内存泄露的问题。但C/C++不一样,自己申请的资源必须自己释放,否则,就像慢性病一样慢慢侵蚀你的程序,在不经意间给你的程序致命一击,然后悄无声息的消失,让你不知所措。如果这个程序是你从头到尾负责的,解决起来还好;如果是从别人那里接手的程序出了问题,那解决起来才叫身心俱疲,头发不知道要掉多少。

        但是问题总得解决才行,解决了问题,才能睡好觉,吃好饭。那么遇到像内存泄露这样的问题,可以怎样解决呢?

第一种方法:代码回溯

        这是最简单,最节省时间的一种做法。有时候,前几天的代码还没有什么问题,今天突然出问题了,调试起来工作量比较大,就可以使用这种方式。在版本控制系统中以未出问题版本时间开始到今天为止,采用二分法方式取历史代码来调试,找到出问题的时间点,再进一步调试。其实不光内存泄露,比较麻烦的崩溃等问题,都可以采取这些方式来解决。采用这种方式,我还是解决了好几个看起来很麻烦的问题,尤其是接手别人的代码。

第二种方法:打日志

        这是程序员最常用的方法,不管什么问题,都可以用打日志的方法来定位问题,尤其是服务器程序和需要长时间运行的程序,虽然不能准确定位问题,但可以缩小分析问题的范围。在日志中,可以定时把内存占用情况打印出来,万一出现内存泄露,还有些痕迹可以查询。

第三种方法:代码调试

        VC中,在Debug模式下,可以在程序入口加入以下代码

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF)

这样在程序退出的时候,就可以打印出泄露的代码码,看如下代码

int main()
{
	#ifdef _DEBUG
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
	#endif
	char* p = new char[1000];
	char* q = new char[500];
	return 0;
}

 调试完成后,Output会输出

 一个1000, 一个500。但这只知道有泄露,不知道哪里泄露啊。如果程序比较复杂,还得慢慢分析。

我们可以将new改写一下

#ifdef _DEBUG
#define MY_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__) 
#else
#define MY_NEW new
#endif


int main()
{
	#ifdef _DEBUG
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
	#endif
	char* p = MY_NEW char[1000];
	char* q = MY_NEW char[500];
	return 0;
}

这样就知道了,哪里分配没有释放了

        但这只适合自己从头到尾参与的项目,而且自己是主要开发人员。如果是中途接手的,那调整的工作量也是挺大的。但是项目有迭代,项目组也不只你一个人,如果有人没有按你的要求来做,还是有泄露风险的。

最后,放大招吧。使用WinDbg来调试

        接下来,我重点讲讲怎么通过WinDbg来定位内存泄露。

        WinDbg是Windows平台下一个非常强大的调试工具,不仅可以调试用户态的程序,还可以进行内核态的调试。这里不对该工具做过多的介绍,网上使用教程也多,现在我们只讨论下怎么使用它去检测内存泄露(用我习惯的做法)。

        安装完WinDbg后,还需要做一些简单的设置,才能开始我们后续的工作。

        1、由于我们要使用到WinDbg的扩展命令!heap,而这个命令需要下载ntdll.dll和kernel32.dll的符号,所以我们必须配置Windows的符号服务器。我们得添加一个环境变量

变量名必须是_NT_SYMBOL_PATH_,

变量值是 SRV*f:\symbols* http://msdl.microsoft.com/download/symbols,这里面,f:\symbols是我放符号的路径。当然,也可以在WinDbg里面使用命令加载符号服务器,我觉得不方便,这里就不做介绍了。不过我使用新版WinDbg后好像不用设置也行,但为了尊重以前的习惯,还是加上吧。

        2、接下来,我们需要监视我们的程序内存分配情况。在WinDbg目录下,有一个gflags.exe程序,我们使用命令行对程序进行设置

Gflags.exe /i F:\Test\Debug\Test.exe +ust

其中,/i 表示指定哪个文件,比如我的程序放在F:\Test\Debug目录下,就指定为F:\Test\Debug\Test.exe。+ust表示创建用户模式堆栈跟踪数据库。执行后,命令行提示

 表示成功。gflags更多的设置,可以在命令行下输入gflags.exe /?进行查阅。

(特别提醒,打开了内存监控以后,程序运行会特别占内存,所以调试完成以后一定要关闭监控,命令也很简单,Gflags.exe /i F:\Test\Debug\Test.exe -ust,+ust改为-ust即可)

        好了,现在可以调程序了。

        首先,我们打开WinDbg,然后通过File->Open Executable菜单打开我们的程序。如果我们的程序正在运行,那使用File->Attach to a process挂载上我们的程序。

         接下来,使用菜单File->Symbol file path加载Test.exe(我们自己的程序)的符号。我们的程序在Debug模式编译好后都会生成一个PDB文件,这个文件里面包含我们程序的调试符号。加载上程序的符号,我们才好对程序下断点,单步执行等操作,否则,只能去汇编代码里面找位置下断点或者在程序里面加中断了。

 符号路径之间使用分号“;”隔开,这里我的路径为(srv*;C:\Users\zy099\source\repos\Test\x64\Debug)点击OK。然后在WinDbg命令行中输入

.reload /f

加载符号,等待符号加载完成。在WinDbg命令行里也可以操作,但我这个记性不太好,又懒得去查,所以就用最简单的方式吧。

由于符号服务器在国外,在国内下载会比较慢,所以需要耐心等待一段时间....

当WinDbg命令行可输入的时候,说明符号已下载成功,这时使用“lm”命令查看符号加载情况

 这里请一定注意ntdll、KERNEL32和我们的Test程序符号加载上没有,如果后面是如上提示,说明符号加载成功。ntdll和KERNEL32主要是需要使用扩展命令,Test用于调试程序。

        接下来,可以使用菜单File->Open Source file打开要调试的源文件。一般如果符号加载正确,代码中有中断的话,就自动跳到源代码处。这里我们就手动打开。

如果Test.exe的符号加载成功,就可以在指定位置按F9下断点了。这里我在 main 的 return 处下了一个断点

        之后就是检查程序泄露点了,在这里,我们要用到WinDbg的扩展命令!heap,关于!heap命令的详细使用方式,这里不多做介绍,有兴趣的朋友可以去查看WinDbg的帮助文档,以后有时间,我也可以另起一篇介绍介绍。

代码如下

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)
    400 2 - 800  (0.36)
    100 8 - 800  (0.36)
    7c4 1 - 7c4  (0.35)
    7a2 1 - 7a2  (0.35)
    138 6 - 750  (0.33)
    390 2 - 720  (0.33)
    695 1 - 695  (0.30)
    628 1 - 628  (0.28)
    1d8 3 - 588  (0.25)
    25c 2 - 4b8  (0.22)
    470 1 - 470  (0.20)
    168 2 - 2d0  (0.13)
    50 8 - 280  (0.11)
    238 1 - 238  (0.10)

我们看到,大小为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)
        000001e13454d090 013f 013f  [00]   000001e13454d0c0    013bc - (busy)
        000001e13454e480 013f 013f  [00]   000001e13454e4b0    013bc - (busy)
        000001e13454f870 013f 013f  [00]   000001e13454f8a0    013bc - (busy)
        000001e134550c60 013f 013f  [00]   000001e134550c90    013bc - (busy)
        000001e134552050 013f 013f  [00]   000001e134552080    013bc - (busy)
          unknown!noop
        000001e134553440 013f 013f  [00]   000001e134553470    013bc - (busy)
        000001e134554830 013f 013f  [00]   000001e134554860    013bc - (busy)
          unknown!printable
        000001e134555c20 013f 013f  [00]   000001e134555c50    013bc - (busy)
          unknown!printable

这里我只截取了部分数据,其实这儿比较长。这里我们会看到很多状态为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,确实是我们分配没有释放的内存空间。这就是这个堆块分配的堆栈信息,通过这个信息,我们就可以定位到这块内存是哪里分配的,然后再到相应的函数里面去分析。

        真正在项目中,情况远没有这种简单,有时候,打印出来的堆信息就有很长一串,这就需要在这些信息里面去找有用的信息的。调试是很让人头痛的一件事,但一旦解决了这些难嗗的骨头,成就感还是有的。

写在最后

        C++程序员要想做到内存使用不出什么意外,好像还真不容易做到。但我们使用一些方法减少内存泄露的风险,比如,使用C++的智能指针,使用内存池技术来统一管理内存等。

  • 0
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Windows操作系统提供了一些内存泄漏检测工具,可以帮助开发人员识别和解决内存泄漏问题。 1. Windows Performance Toolkit:Windows性能工具包(WPT)包括各种工具,其中包含了用于分析和检测内存泄漏的工具。WPT中的堆分析器(Heap Profiler)可以帮助检测应用程序中的内存泄漏,并生成详细的报告。它可以跟踪程序中的内存分配和释放,并标记出没有释放的内存块。 2. Visual Studio Diagnostic Tools:微软的Visual Studio集成开发环境(IDE)提供了一些强大的诊断工具。Visual Studio的诊断工具可以通过内存监视器(Memory Profiler)检测和分析内存泄漏问题。开发人员可以使用这些工具来监视和跟踪应用程序中的内存使用情况,并识别出内存泄漏的原因。 3. 第三方工具:除了Windows官方提供的工具之外,还有一些第三方工具可用于检测和解决内存泄漏问题。例如,MemProfiler是一款用于.NET应用程序的高级内存分析工具,可以帮助开发人员找出内存泄漏的源头。另外,Valgrind是一款开源的内存调试和性能分析工具,可以在Linux和Mac OS X上进行内存泄漏检测。 总之,针对Windows操作系统,开发人员可以使用官方提供的Windows Performance Toolkit和Visual Studio Diagnostic Tools,或者选择一些第三方工具来检测和解决内存泄漏问题。这些工具可以帮助开发人员识别内存泄漏的原因,提高应用程序的性能和稳定性。 ### 回答2: Windows操作系统中的内存泄漏是指程序在运行过程中分配的内存未能释放,导致系统内存资源的浪费和性能下降。为了解决和检测这种问题,Windows提供了一些工具。 1. Windows性能监视器(Performance Monitor):可以用来监测和分析系统的性能指标,包括内存使用情况。通过查看内存使用率的波动,可以推测是否存在内存泄漏的情况。 2. 内存诊断工具(Memory Diagnostics Tool):Windows自带的内存诊断工具可以检测和诊断系统中的硬件问题,包括内存故障和内存泄漏。可以通过启动该工具来进行运行时内存检测。 3. 调试工具(Debugger):Windows提供了强大的调试工具集,如WinDBG和Visual Studio。这些工具可以通过附加到运行中的程序来实时追踪和分析系统中的内存泄漏。 4. 第三方工具:除了Windows自带的工具,还有一些第三方工具可用于检测和分析Windows内存泄漏问题。例如,MemProfiler和LeakDiag等工具提供了更高级的功能,可以帮助开发人员更容易地定位和解决内存泄漏问题。 总结起来,Windows提供了多种工具来帮助检测和解决内存泄漏问题,开发人员可以根据具体情况选择合适的工具来进行内存泄漏的定位和修复。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cantaloupe77

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值