VC++内存泄漏检测工具浅究
前言
内存泄漏,即未能正确释放以前分配的内存,是 C/C++ 应用程序中最难以捉摸也最难以检测到的 Bug 之一。 最初少量内存泄漏可能不引人注目,但随着时间的推移,内存泄漏越来越多,就会出现一些征兆,包括性能下降,在应用程序内存不足时发生崩溃。 更严重的是,占用了所有可用内存的泄漏应用程序可能会导致其他应用程序崩溃,从而无法确定问题出在哪个应用程序。 即使看似无害的内存泄漏也可能说明存在其他问题应当纠正。
本文以VLD(visual leak detector)工具及C 运行库 (CRT) 调试堆函数作为研究对象。
正文
VLD(visual leak detector)
VLD简介
VLD是一款用于VisualC++的免费内存泄漏检查工具。可以在codeproject.com网站上找到,相比其它的内存泄漏检测工具,它在检查内存泄漏的同时,还具有如下特点:
可以得到内存泄漏点的调用堆栈,如果可以的话,还可以得到其所在的文件及行号;
可以得到泄漏内存的完整数据;
恶意设置内存泄漏报告的级别;
它以动态库的形式提供,无需编译源代码,只需要很小的改动程序;
*源代码使用GNU许可发布,并有详细的文档及其注释。
默认情况下,只有在Debug模式下才会启用VLD的功能。VLD只能在Windows下使用,它应用在C/C++语言中。
VLD下载安装
下载网址
http://vld.codeplex.com/
http://www.codeproject.com/Articles/9815/Visual-Leak-Detector-Enhanced-Memory-Leak-Detectio
安装会提示是否把DLL路径保存至系统环境变量,或者自己手动添加。如果没有添加则要拷贝相应DLL到工程目录下。
路径配置
路径一:在项目/属性/C/C++/附加包含目录,添加VLD的头文件路径”\include”路径;在链接器/常规/附加库目录,添加 VLD库文件的”\lib\Win32”路径,64位程序添加”\lib\Win64”路径。
路径二:Visual C++ IDE的”工具”→”选项”→”项目和解决方案”→”VC++ 目录”,在”包含文件”中增加VLD的头文件路径”\include”路径,在”库文件”增加VLD库文件的”\lib\Win32”路径,64位程序添加”\lib\Win64”路径。
VLD应用
代码修改
添加VLD方法很简单,只要在包含入口函数的.cpp文件中包含vld.h就可以。如果这个cpp文件中包含了stdafx.h,则将包含vld.h的语句放在stdafx.h的包含语句之后,否则放在最前面。
示例程序:
调试窗口输出
输出显示在内存块0x00AE77A0处有8 bytes的内存泄漏,位于d:\users\bqz88\documents\visual studio 2010\projects\vldtest\vldtest\vldtest.cpp文件的第13行,泄漏内存存储数据为CD CD CD CD CD CD CD CD,此为堆内存默认值。若有多处内存泄漏,则会有多个Block块信息。
泄漏信息写入日志文件
将配置文件vld.ini拷贝到当前工程目录下,修改其中ReportFile 参数值为 .\memory_leak_report.log。则vld将会把泄漏信息写入当前目录中的memory_leak_report.log文件中。
备注
VLD检测工具也能检测动态链接库中的堆内存泄漏,可在动态链接库中配置VLD,也可以在应用程序中配置VLD,配置及使用方法类似。
参考资料
http://blog.csdn.net/fan_hai_ping/article/details/8023433
http://blog.csdn.net/fengbingchun/article/details/44195959
https://msdn.microsoft.com/zh-cn/library/x98tx3cf.aspx
C 运行库 (CRT) 调试堆函数
调试器和 C 运行库 (CRT) 调试堆函数是检测内存泄漏的主要工具。
启用内存泄漏检测
若要启用调试堆函数,请在程序中包括以下语句:
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
为了 CRT 函数能够正常工作,#include 语句必须遵循此处所示的顺序。
_CRTDBG_MAP_ALLOC:宏定义将 CRT 堆函数的基础版本映射到对应的调试版本。
crtdbg.h头文件:将 malloc 和 free 函数映射到它们的调试版本,即 _malloc_dbg 和 free,它们将跟踪内存分配和释放。此映射只在包含 _DEBUG 的调试版本中发生。发布版本使用普通的 malloc 和 free 函数。
常用调试堆函数及参数
_CrtDumpMemoryLeaks(void):显示当前点之前的内存泄漏报告,在程序退出前调用此函数打印泄漏信息。
_CrtSetDbgFlag ():设置调试标识。其参数可为:
_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF宏:则会在程序的每个退出点自动调用 _CrtDumpMemoryLeaks()打印泄漏信息。
_CRTDBG_REPORT_FLAG宏:设置泄漏报告输出格式为日志文件,若没有设置此宏,默认在调试窗口打印泄漏信息。
_CrtSetReportMode(int,int):设置打印报告的错误级别及输出模式,基本包括警告、错误等四级,输出模式有调试窗口或者日志文件等四种。
_CrtSetReportFile(int,HFILE):设置输出信息级别及日志文件路径。
_CrtSetBreakAlloc(int):设置内存分配断点,也可以通过设置变量_crtBreakAlloc 的值来达到同样的效果。
示例程序
调试窗口输出信息:
输出信息解读:
内存分配编号,在本例中为 119
块类型,在本例中为 client。因在new的宏定义为_CLIENT_BLOCK
十六进制内存位置,在本例中为 0x0141178B0。
块的大小,在本例中为 8bytes。
块中前 8 个字节的数据(十六进制形式)。最多显示16字节数据,可设置。
对于有多个退出点的程序,可以参考代码来保证在程序退出前打印报告信息:
将泄漏信息输出至日志文件
因为要使用Create File函数,需包含Windows.h头文件。因为要在程序结束前输出泄漏信息,故_ CrtSetDbgFlag()参数设置为_CRTDBG_REPORT_FLAG,并要手动添加_CrtDumpMemoryLeaks()函数。
调试程序,在当前工程目录下生成memleak.log文件,其内容如下。
报告信息中大括号内数字为119,说明编号为119的内存块上有内存泄漏。可以通过设置_CrtSetBreakAlloc(119),或_crtBreakAlloc=119,在内存分配处中断,本例为MyClass *mc = new MyClass;语句。当然,CrtSetBreakAlloc函数要位于内存分配之前。
备注
C 运行库 (CRT) 调试堆函数也可在动态链接库中配置,其配置方法与应用程序类似。
在示例程序第9行,定义了new操作符的一个宏。这是因为我们在程序中分配的内存是由C++运算符new分配的,而不是标准CRT malloc函数分配的内存,所以要定义新的new宏定义来达到我们输出文件名和行号的目的。
参考资料
https://msdn.microsoft.com/zh-cn/library/x98tx3cf.aspx
http://www.cppblog.com/weiym/archive/2013/02/25/198072.html
在ObjectArx中的应用
应用成效
内存泄漏检测工具在arx中的应用效果不尽如人意。
添加VLD检测代码的arx在CAD加载过程中直接导致CAD崩溃退出。
添加CRT检测代码的arx虽能运行,但只能检测到C++内置类型及自定义类型对象的内存泄漏,对arx中内置的对象无能为力。而且,CRT是运行在Debug版本下,需要MSVCRTD.dll运行库的支持,但arx默认是在MSVCRT.dll中分配内存。在加载arx,CAD警告提示:xxx.arx从 MSVCR100D.DLL 中分配内存,但 AutoCAD 使用 MSVCR100.DLL 的内存。这可能会引起错误。这也许就是CRT无法检测arx中内置类型对象的内存泄漏。
对于VLD检测导致CAD崩溃的原因,我们先从VLD及CRT的原理分析开始。
VLD及CRT运行原理
对于CRT,其原理是在使用Debug版本分配内存时,它会在内存块中记录分配该内存的文件名和行号。当程序退出时CRT会在main函数返回时做一些清理工作,此时检查调试堆内存,如果仍然有内存没释放,则一定存在内存泄漏问题。从这些没有被释放的内存块的头中可以得到文件名和行号。这种静态的方法可以检查出内存泄漏,但是不知道泄漏究竟是怎么发生的,也不知道该内存分配语句是如何被执行到的,想要了解这些必须对内存分配过程进行动态跟踪。VLD就是这样做的,在每次内存分配的时候记录其上下文,当程序退出时对检测到的内存泄漏查找其上下文信息,并转换成报告输出到Output中。
VLD初始化
VLD要记录每次的内存分配,它通过Windows提供的分配钩子allocation hooks来监视调试堆内存的分配。它是一个用户自定义的回调函数,在每次从堆中分配内存之前被调用,在初始化是VLD使用_CrtSetAllocation注册这个钩子函数。
全局变量在程序初始化时就初始化,如果将VLD作为一个全局变量就可以与程序一起启动,但是C/C++并没有约定全局变量初始化的顺序,如果其它全局变量的构造函数中有内存分配则可能无法检测到。因此,VLD使用C/C++提供的#pragma init_seg来减少其它全局变量在它之前进行初始化。根据#pragma init_seg的定义,全局变量初始化分为3个阶段,首先是compiler阶段,一般进行C语言运行时库的初始化;然后是lib段,一般用于第三方类库的初始化扽;最后是user段,大部分的初始化都在这个阶段进行。
记录内存分配
一个内存分配钩子函数需要具有如下的定义:
int AllocHook(int allocType, voiduserData, size_t size,int blockType, long requestNumber, onst unsigned charfilename, int lineNumber);
该函数需要在VLD初始化时被注册,每次从堆中分配内存前被调用,它需要处理的事情就是记录下此时的调用堆栈和此时堆内存分配的唯一标识requestNumber。
得到当前堆栈的二进制表示并不是很复杂的事情,但是因为不同的体系结构、不同的编译器、不同的操作系统所产生的堆栈内容是不一样的,要解释堆栈并得到整个函数的调用过程比较复杂。不过Windows提供了一个StackWalk64函数可以获得堆栈的内容。
Arx程序的特殊性
Arx中new和delete的重载
Arx中定义了AcHeapOperators类对CAD中内置类,如AcDbPolyline、AcDbText等的内存分配进行了“重定义”,即对new及delete操作符进行了重载操作。下图为AcHeapOperators类的部分代码截图。
由上节的VLD及CRT运行原理可知,对于下面的代码,利用CRT只能检测到MyClass及int类型的内存泄漏,对于AcDbText及AcDbPolyline类型的内存泄漏却无能为力。
Arx与VLD可能存在的冲突
上节介绍了VLD及CRT运行原理,尤其是对VLD的检测原理进行了分析。由此可知,VLD在程序或动态库初始化时进行了一些操作,对于普通的运行程序或动态链接库,这些操作没有问题,但对于Arx动态库,其在加载时,CAD也会对arx动态库进行一些初始化操作。加载添加VLD检测代码的Arx动态库导致CAD崩溃可能源于Arx动态库加载时CAD对其操作与VLD对其操作冲突导致的。
小结
Arx程序虽然也是动态链接库,但这个链接库要无缝嵌入到AutoCAD中,服从AutoCAD的内存分配规则。这其中一个主要的体现方面是arx对CAD中内置类,比如AcDbPolyline、AcDbText等的内存分配进行了“重定义”,最直接的体现是对new操作符进行了重载操作。到VLD加载时,需要对内存分配做一些“辅助”工作,但arx作为CAD的动态链接库,需要在CAD框架、CAD定义的权限内运行,这就可能导致VLD初始化与上述原则冲突,最终导致CAD的崩溃。
参考资料
http://blog.csdn.net/fan_hai_ping/article/details/8023433
总结
本文以VC++内存泄漏检测工具VLD及CRT为研究对象,针对其在使用中的配置、代码修改及注意事项进行了简单说明。文中着重分析了以上两工具在ObjectArx中应用中出现的问题,并对产生相关问题的原因进行了引申分析。