C++内存调试技术

     说到C++调试想必大家会想到一堆调试中遇到的问题,而在我看来C++中最难也是最普遍的调试问题就出在内存上。为什么要这么说呢?可以想想你曾经碰到过的问题,内存泄露应该是最普遍的,其次是内存越界,野指针,这些碰到哪一个都是硬点子。特别是项目规模越来越大的时候,这些问题就成为骨中钉,肉中刺,膈应的开发人员什么想法都没有了。

    问题既然产生,那必然会有方法解决。我们从现在开始一点一点的剖析这些问题的产生原因再对症下药,保管药到病除啊。

 

1. 内存泄露

 

    内存泄露是个老掉牙的问题,从写程序的第一天就没离开过我的视野范围。有点程序基础的人都知道它是怎么产生的,我这里就不罗嗦。我只介绍几种内存泄露的检查方法。

 

1.1 如何检测内存泄露

    正常情况下,我们通过Virtual Studio 生成的程序,除MFC以外是不会报告内存泄露的,即使你确实泄露了。那么为什么是除MFC应用程序以外呢?这个问题就说到了MFC应用程序向导都为我们生成了些什么。

如果你细心的话,应该会在你的项目里找到这么一段话。这段话的意思是,如果是DEBUG版本,则将 new 替换成 DEBUG_NEW。

那么DEBUG_NEW又是什么?

我们跟过去看一下它的定义

它只是在new后面加了两个参数 THIS_FILE, __LINE__

这两个参数都是编译器的预定义宏(THIS_FILE其实是重新定义的__FILE__),分别表示,当前文件的文件名和行号,如果你的MFC程序发生了泄露,又正好被捕获到了,那么output窗口中显示的文件名和行号就是从这里来的。

那么new 怎么会有参数的呢?

秘密在于MFC重载了 operator new。当然所有的内存分配最后都会调用crt的malloc进行内存分配。

所以我们不一定要用MFC,CRT本身就自带了内存泄露的检测功能。我们需要做的,只是做一些小设置。

在CRT中我们可通过如下的函数调用来打开内存泄露报告。

如果发生了内存泄露则会有如下的输出

 

报告是有了,但是报告中没有指明产生泄露的文件和行号,那么如何通过报告来找出内存泄露的地方呢?

可以看到,每个泄露的报告项中的最前面有一个{91},这其实是一个分配的序号。无论你用new或者malloc都会导致这个序号增长。

当我们需要定位某个序号的内存泄露的时候可以通过如下代码

当程序分配到91块内存的时候就会出现ASSERT断言,暂停程序的执行。这样做有一个好处,就是可以通过函数调用堆栈还原泄露时的现场,从而更具体的分析泄露出现的原因。

 

那么CRT真的无法像MFC一样打印出泄露的文件名和行号吗?其实这是一个很简单的事情,我们只要通过

来分配内存就可以得到像MFC的内存泄露检测报告。其实MFC也是通过CRT做的。现在我们看到的内存泄露检测报告应该是这样的

 如此我们已经可以让内存泄露检测报告像MFC一样打印出文件名和行号了。

3. 更深入的讨论

CRT是如何将文件名和行号保存下来的?又是如何打印到我们的Output窗口的呢?

我们先看一下反汇编以后的new 代码

从反汇编出来的代码中我们可以看到,文件名和行号都是存储在程序数据段中的,传入operator new 的只是文件名的地址而已。跟踪进入call operator new[] 我们可以看到如下代码

 operator new 的代码如下

  

可以看到,最终还是调用了_nh_malloc_dbg.如果我们继续跟踪下去的话会找到真正分配内存的地方,这里我着重讲一下CRT的内存结构。和很多内存池一样,CRT也在分配内存的前后加入了一些标记和内容。其中最重要的就是一个_CrtMemBlockHeader 的结构。

这个结构是一个双向链表,分别记录了前一个和后一个分配的内存块的首地址。同时,该结构也记录了产生这块内存的具体文件和行号。其次是内存的类型,以及请求的大小,以及分配内存时的分配序号。最后是一个4字节的上溢保护,通过这四个字节可以检测出大部分由负序数导致的数组上溢问题。

从内存实际分配字节数的计算中我们还可以看出,除请求的内存量和_CrtMemBlockHeader所占用的内存量以外,还有一个4字节的下溢字节保护。上溢和下溢的保护字节一般被初始化为0xFD。

初始化内存信息的代码如下

 如此,在最终生成内存泄露检测报告的时候就可以根据内存块信息来得到文件名和行号了。

4. CRT的缺陷

CRT检查内存泄露的方法是比较直观的,但是也相应的存在缺陷。

首先,是你new的时候或者malloc的时候不可能都是用特殊版本的函数调用,只能通过定义宏来实现。宏这个东西我比较讨厌,因为在来来回回的包含中你不能确定哪些new 被替换了。所以一旦有没有被宏替换过的new那么你的报告就会出现一些没有地址和行号的内存泄露报告。

其次,CRT的内存泄露报告没有调用堆栈,有时候泄露很可能是一些和调用顺序相关的临界条件引起的,至少碰到这种情况就比较难查。

最后,假如你的程序中存在静态对象,恰好你的静态对象被析构的时候是在内存检测报告完成之后,那么内存检测报告就会发生误报。

以上种种,都促使我寻找一种更先进的检查内存泄露的方法。

 

5. Virutal Leak Detected

初见VLD的时候我认为没有比他更好的内存泄露检测方法了。从技术的角度讲,它所做的工作有点像黑客干的事情,因为它用到了一种技术——DLL补丁。

说起DLL补丁我们还要先讲一下EXE程序和DLL之间的关系,以及EXE如何调用DLL

当主程序加载DLL到自己的进程地址空间之前,操作系统首先要将整个DLL文件载入到内存中,然后根据需要重新映射动态链接库的地址,之后调整导出符号表中入口函数的地址,使之指向正确的函数入口。如果你跟踪汇编代码的话,会发现你Call 一个DLL的导出函数或者类函数的时候其实是先到了一个全是jmp指令的地方,然后才到达正确的代码地址。这个全是jmp指令的地方就是导出函数表。如果我们修改了表中的跳转地址的话,当你去call 的时候就会跳转到一个你指定的地方,比如说一个钩子函数。但是,这种修改跳转表的方法只对当前程序实例有效。

而VLD的核心思想是,拦截所有的内存分配函数的入口地址,使之转移到我们的入口函数中记录一些内容,比如调用时的指令指针EIP的值等。 

虽然看起来很复杂,但是vld的使用时非常简单的我们只需要在工程中设置vld.lib导入库的地址,在某一个头文件中包含vld.h就可以了。之后我们将vld.ini放在运行目录下

vld.ini有如下配置

 

我们先看一下VLD的初始化过程,这有助于我们加深对VLD工作机制的理解。我这里使用的是vld 1.9h的版本,支持VS2008及以下的编译器。最新的2.0a版本已经可以支持vs2010

 

6. 非泄露内存增长

什么是非泄露内存增长?举例来说,你的程序有一个列表,所有已分配的内存都被记录在这个列表中,但是列表中的内存在某些情况下没有被删除。所以,未释放的内存越来越多,直到最后内存分配失败。但是,你使用之前的方法检查内存泄露却发现,并没有任何日志。原来,在你正常退出程序的时候列表中未被释放的内存已经被你挨个释放了。

实际情况要比这个复杂的多,也隐晦的多。这种情况也是内存泄露的一种,属于运行时泄露,它的危害更为严重。对于这种问题的追查也是一件很恼人的事情。那么从现在开始,让这么麻烦的问题见鬼去吧。

6.1 UMDH简介

 UMDH 是windows debug tools 下的一款命令行工具,它的全名是User-Mode Dump Heap 这个工具会分析当前进程在堆上分配的内存,并有两种模式

1. 进程分析模式,这个模式会对进程分配的每一块内存做记录,其中包含分配的内存大小;内存分配地址;内存分配时的函数调用堆栈等。

2. 日志分析模式,该模式会比较几个不同的日志,找出内存增长的地方。

在使用UMDH做分析之前我们要先做一些准备工作。

首先,打开进程的栈捕捉标志。这个步骤通过一行命令来完成

gflags /i ImageName +ust

这个命令只影响新启动的进程,对已经在运行状态的进程不起作用。

其次,安装windows 的 symbol文件。如果不需要的话可以不用安装。

最后,设置环境变量

set _NT_SYMBOL_PATH=Path

通过这几个步骤后我们才可以使用UMDH来对进程内存做分析。

 

首先,我们先启动目标进程,稍等一会儿,让进程进入稳定的运行状态。

之后我们通过命令行 umdh -p:2230  -f:dump_allocations.txt 对进程进行分析。

其中-p后面的数字是进程号, -f 后面跟的是日志文件的文件名。

等待一段时间后我们就会收集到一个文件,里面记录了一些内存的分配信息。

之后我们重复以上步骤几次,最好给文件名编个号。比如 dump1.txt dump2.txt

 

最后,我们通过命令行对刚才的文件进行分析

umdh -v dump1.txt dump2.txt > memleak.txt

这样我们得到了一个描述内存增长的日志文件memleak.txt类似如下的格式

 

+ 5320 (f110 - 9df0) 3a allocs BackTrace00053 
Total increase == 5320

ntdll!RtlDebugAllocateHeap+0x000000FD
ntdll!RtlAllocateHeapSlowly+0x0000005A
ntdll!RtlAllocateHeap+0x00000808
MyApp!_heap_alloc_base+0x00000069
MyApp!_heap_alloc_dbg+0x000001A2
MyApp!_nh_malloc_dbg+0x00000023
MyApp!_nh_malloc+0x00000016
MyApp!operator new+0x0000000E
MyApp!LeakyFunc+0x0000001E
MyApp!main+0x0000002C
MyApp!mainCRTStartup+0x000000FC
KERNEL32!BaseProcessStart+0x0000003D

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值