Linux内存泄漏检测工具推荐,C++ new宏调试技巧

检测维修 0 54

具备了如此这般的定义, 于编译DEBUG版之际, 现身在这个cpp文件里的全部new均被替换成DEBUG_NEW了。那么DEBUG_NEW究竟是什么呢? DEBUG_NEW同样是一个宏, 以下内容摘自afx.h, 位于1632行。

预定义一个宏, 宏名为DEBUG_NEW, 其值为使用THIS_FILE和__LINE__作为参数调用new所得到的结果。

所以如果有这样一行代码:

char* p = new char;

经过宏替换就变成了:

char *p, 它是通过new(在这里是THIS_FILE, 处于某一行, 具体是__LINE__)来创建一个char类型对象的指针。

依照C++的标准, 针对以上new的运用方式, 编译器会去寻觅如此定义的operator new:

无返回值指针类型的运算符重载函数, 其参数为大小尺寸, 常量字符串指针, 整型数值, 此函数名为新运算符。

我们于afxmem.cpp的63行, 发现了一个如此这般的operator new的实现情况。

无结果返回值类型进行声明的运算符新建函数, 其参数为大小尺寸, 还有常量指针字符型文件名, 以及整型行号。

返回, 运算符新, 大小为nSize, 类型为_NORMAL_BLOCK, 给定文件名lpszFileName, 行号为nLine , 所对应的操作结果。

使指针为空类型, 以‘__cdecl’方式声明, 定义一个名为‘operator new’的运算符, 其参数包括一个表示大小的‘nSize’, 一个表示类型的‘nType’, 一个指向常量字符串的‘lpszFileName’, 以及一个表示行号的‘nLine’, 返回值类型为空指针。

pResult等于, 通过调用_malloc_dbg函数, 以nSize为大小, nType为类型, lpszFileName为文件名, nLine为行号, 所进行的内存分配操作的结果。

if (pResult != NULL)

return pResult;

第二个, operator new函数, 其长度较长, 鉴于简单起见, 我仅摘取了部分。很明显, 最后的内存分配, 是经由_malloc_dbg函数达成的, 此函数归属于MS C - Runtime Library的Debug Function。该函数不仅要求传入内存的大小, 此外, 还有文件名以及行号, 这般两个参数。文件名与行号旨在记录此次分配, 究竟是由哪一段代码所导致的。要是这块里面, 存在在程序结束之前都未曾被释放的情况, 那么这些信息就会被输出到Debug窗口当中。

这块顺带提及一下THIS_FILE, __FILE与 __LINE__。__FILE__以及__LINE__皆是编译器所定义的宏。在碰到__FILE__之际, 编译器会将__FILE__替换成一个字符串, 此字符串便是当下正在编译的文件的路径名。在碰到__LINE__之时, 编译器会把__LINE__替换成一个数字, 该数字即为当前这行代码的行号。那个在DEBUG_NEW的定义里头, 没直接去用__FILE__, 而是采用了THIS_FILE, 这么做的目的在于减小目标文件的大小。咱们假设一下, 在某一个cpp文件里居然出现了100处使用new这种情况的语句位置, 如果直接去使用__FILE__的话, 那么编译器就会生成100个常量字符串, 这100个字符串全部都是那个cpp文件的路径名, 很明显是非常冗余的。倘若运用 THIS_FILE, 编译器仅会生成一个常量字符串, 那么那 100 处所进行的 new 的调用, 所采用的无一不是指向常量字符串的指针。

再一次去观察经由MFC Application Wizard所生成的项目, 我们在cpp文件里会发现, 仅仅只是对new做了映射, 如若你于程序当中直接运用malloc函数去分配内存, 那么调用malloc的文件名以及行号是不会被记录下来的。要是这块内存出现了泄漏情况, MS C - Runtime Library依旧能够检测到, 然而当输出这块内存块的信息时, 是不会涵盖分配它的文件名与行号的。

假如你想要于并非MFC程序的环境里, 将内存泄漏的检测功能予以开启, 这其实是极为简单的事儿, 你只需在程序的入口的那个地方, 添加如下的这几行代码:

将_CrtSetDbgFlag函数以_CRTDBG_REPORT_FLAG作为参数调用后得到的值, 赋值给tmpFlag变量, tmpFlag变量的类型为int。

tmpFlag进行或运算, 使其值等于, _CRTDBG_LEAK_CHECK_DF 参与运算后的值。

_CrtSetDbgFlag( tmpFlag );

如此一来, 当程序终结之际, 即是在winmain、main或者dllmain函数返回之后, 倘若存有尚未释放的内存块, 那么它们的相关信息将会被打印至Debug窗口之中。

要是你尝试着构建生成了一个并非MFC的应用程序, 并且于程序的入口之处放进了以上所讲到的代码, 而且还刻意地在程序之内没有解除释放某些的内存块儿, 你就会在Debug窗口那里瞧见以下这般的信息:

在0x00C91C90处有一个47的正常块, 其长度为200字节。

数据: 小于号大于号, 零, 零一, 零二, 零三, 零四, 零五, 零六, 零七, 零八, 零九, 零A, 零B, 零C, 零D, 零E, 零F。

检测到了内存泄漏, 然而, 跟上面MFC程序的例子相比较, 缺少了文件名以及行号。对于一个相对比较大的程序而言, 没有这些信息, 去解决问题将会变得极其困难。

要弄清楚泄漏的内存块于何处被分配, 你得去实现跟MFC相仿的映射功能, 将诸如new、maolloc这般的函数映射至_malloc_dbg函数之上。在此我就不再详细讲述了, 你能够参考下MFC的源代码。

因Debug Function的实现在MS C - Runtime Library里边, 所以它仅能检测出堆内存的泄漏, 并且只限于是malloc、realloc或者strdup等所分配的内存, 而那些系统资源, 像HANDLE、GDI Object, 又或是不借助C - Runtime Library分配的内存, 诸如VARIANT、BSTR的泄漏, 它是没方法检测到的, 这是此种检测法的一个重大的局限性。另外, 为了能够记录内存块是于何处分配的, 源代码必须按照相应要求进行配合, 这在调试某些老程序时极为麻烦, 要知道修改源代码可不是一件让人省心的事情, 这正是这种检测方法的又一个局限性所在。

对于开发一个大型程序来说, MS C-Runtime Library所提供的检测功能, 那是远远不够的。接下来, 我们且瞧瞧外挂式的检测工具。我选用较多的是BoundsChecker, 一方面是由于它的功能较为全面, 更关键的是它具备稳定性。这类工具倘若不稳定, 那可就反倒会在忙乱之中徒增麻烦。毕竟它是出自大名鼎鼎的NuMega, 我使用下来基本上没什么大问题。

使用BoundsChecker检测内存泄漏:

BoundsChecker运用一种名为Code Injection的技术, 用以截获针对分配内存以及释放内存的函数的调用, 简单来讲, 当你的程序开始运行之际BoundsChecker的DLL会被自动载入进程的地址空间(此可借助system-level的Hook达成), 接着它会对进程中有关内存分配和释放的函数调用予以修改, 使得这些调用首先转向它的代码, 而后再去执行原本的代码。BoundsChecker在进行这些动作期间, 并不需要对被调试地程序的源代码或者工程配置文件作出修改, 如此情况便致使运用BoundsChecker它非常地简便、直接。

这里, 我们把malloc函数当作例子, 截获别的函数, 其方法跟这相似。

能够需要被截获的函数大概也许有可能在DLL当中, 或者说不定也可能在程序的代码之内, 比如说, 要假设倘若要是静态连结C - Runtime Library , 那么malloc函数的代码就会被连结到程序里面, 因为由于针对截获住对这类函数的调用这一情况来讲, 故而BoundsChecker会动态修改这些函数的指令。

在下述两段汇编代码之中, 一段不存在BoundsChecker的介入情况, 另一段却是有着BoundsChecker进行介入的状况。

127: size_t nSize

128: )

129: {

00403C10 push ebp

00403C11 mov ebp,esp

130, 返回, _nh_malloc_dbg, (nSize, _newmode, _NORMAL_BLOCK, NULL, 0)。

00403C13 push 0

00403C15 push 0

00403C17 push 1

00403C19 mov eax,

__newmode (0042376c)

00403C1E push eax

00403C1F mov ecx,dword ptr

00403C22 push ecx

00403C23调用_nh_malloc_dbg, 其地址为00403c80。

00403C28 add esp,14h

131: }

以下这一段代码有BoundsChecker介入:

126: _CRTIMP void * __cdecl malloc (

127: size_t nSize

128: )

129: {

00403C10 jmp 01F41EC8

00403C15 push 0

00403C17 push 1

00403C19 mov eax,

__newmode (0042376c)

00403C1E push eax

C++ operator new 内存分配机制_linux下内存泄露检测工具介绍_C++ DEBUG_NEW 宏内存泄漏检测

00403C1F mov ecx,dword ptr

00403C22 push ecx

00403C23 call _nh_malloc_dbg (00403c80)

00403C28 add esp,14h

131: }

当BoundsChecker这一程序参与进来之后, 函数malloc的前面部分的三条汇编指令被替换成为了一条jmp指令, 原本的那三条指令被转移到地址01F41EC8的地方了。当做程序进入到malloc这个函数之后, 先是jmp到01F41EC8这个地址位置, 去执行原来的那三条指令, 之后就变成是BoundsChecker发挥作用的范围了。大致而言, 它会先去记录函数的返回地址, 函数的返回地址处于stack之上, 因而极易被修改。随后, 它会将返回地址指向属于BoundsChecker的代码。接着, 它会跳到原本指向malloc函数的指令之处, 也就是在00403c15这个位置。当malloc这个函数执行完毕结束之时, 鉴于返回地址遭受到修改这种情况, 它会转而返回到BoundsChecker的代码区域之中, 此时, BoundsChecker会去记录经由malloc分配内存所产生的指针, 之后再跳跃到原本的返回地址那里去。

要是内存分配或者释放函数处于DLL当中, 那么BoundsChecker会运用另外一种方式去截获针对这些函数的调用, BoundsChecker借助修改程序的DLL Import Table, 使得table里的函数地址指向自身的地址, 以此来达成截获的目标。

成功截获住这些分配以及释放函数, BoundsChecker便可以记录下被分配的内存或者资源的生命周期, 接连而来的问题在于怎样与源代码产生关联, 换句话来讲就是当BoundsChecker对内存泄漏进行检测的时候, 它究竟该如何去报告出这块内存块是由哪一段代码所分配的, 答案便是调试信息, 也就是指Debug Information。当我们对一个Debug版的程序进行编译之时, 编译器会记录下源代码与二进制代码之间的对应关系, 将其放置于一个单独的文件里(.pdb)或是直接连结到目标程序之中, 借助直接读取调试信息, 便能够获取到分配某块内存的源代码所在的文件以及行数。通过运用Code Injection以及Debug Information, 使得BoundsChecker不仅能够记载调用分配函数时的源代码所在位置, 而且还能够记载分配时刻的Call Stack, 以及Call Stack上面函数的源代码所在位置, 就使用诸如MFC这般的类库时这相当有用, 下面我借助一个例子予以说明:

void ShowXItemMenu()

CMenu menu;

menu.CreatePopupMenu();

//add menu items.

menu.TrackPropupMenu();

void ShowYItemMenu( )

CMenu menu;

menu.CreatePopupMenu();

//add menu items.

menu.TrackPropupMenu();

将菜单分离, 这会导致菜单句柄泄漏, 菜单分离, 这会导致菜单句柄泄漏;句柄泄漏。

BOOL CMenu::CreatePopupMenu()

hMenu = CreatePopupMenu();

当去调用ShowYItemMenu()这个操作的时候, 我们是特意去形成HMENU的泄漏情形的。然而, 对于BoundsChecker而言, 那被泄漏的HMENU是于class CMenu::CreatePopupMenu()当中进行分配的。假设你的程序不少地方运用了CMenu的CreatePopupMenu()函数, 像是CMenu::CreatePopupMenu()导致的那般, 你依旧没法确定问题的根源究竟处于何处, 是在ShowXItemMenu()中间,还是在ShowYItemMenu()里面, 又或许有没有别的地方同样运用了CreatePopupMenu()呢? 获取了Call Stack的信息之后, 问题就变得简单起来了。BoundsChecker会报告泄漏的HMENU的信息, 情况是这样的:

Function

File

Line

CMenu::CreatePopupMenu

磁盘E分区下的8168文件夹里的vc98文件夹中的mfc文件夹中的include文件夹中的afxwin1.inl文件。

1009

ShowYItemMenu

E:\testmemleak\mytest.cpp

100

这里省略了其他的函数调用

如此一来, 我们能够轻易地找出那个出现问题的函数, 它是ShowYItemMenu()。在运用像MFC这类的类库进行编程之际, 大部分的API调用都被封装于类库的class之中, 鉴于有了Call Stack信息这下, 我们便能够极为轻松地追踪到那真正发生泄漏的代码。

Call Stack信息的记录, 会致使程序的运行极其变慢, 所以通常状况下BoundsChecker不会记录Call Stack信息能依照以下的步骤去开放记录Call Stack信息的选项开关:

1. 开启菜单, 其中选项为BoundsChecker, 还有Setting…。

2. 在Error Detection这个页面里, 于Error Detection Scheme的列表当中, 去挑选Custom。

3. 于Category的Combox之内, 挑选Pointer and leak error check。

4. 钩上Report Call Stack复选框

5. 点击Ok

依据Code Injection, BoundsChecker另外赋予了API Parameter的校验功能, memory over, run等功能, 这些功能之于程序的开发俱是颇为有益的, 鉴于这些方面不属于本文的主题范畴, 所以搁置此处不予详尽叙述了。

即便BoundsChecker具备这般强大功能, 然而在面对隐式内存泄漏状况时依旧显得极度乏力, 所以接下来我们瞧瞧怎样运用Performance Monitor检测内存泄漏。

使用Performance Monitor检测内存泄漏

在NT的内核开始其设计进程当中, 已然添加进了系统监视功能, 像CPU的使用率这个方面, 内存的使用状况这种情形, I/O操作的频繁程度这类情况, 都被当作一个个Counter, 应用程序能够经由读取这些Counter去知晓整个系统的或者某个进程的运行态势, Performance Monitor便是这样的一个应用程序。

用以检测内存泄漏, 通常而言, 我们能够监视Process对象的Handle Count, Virutal Bytes以及Working Set这三个Counter。进程当前所打开的 HANDLE 的个数由 Handle Count 进行记录, 对这个 Counter 予以监视, 其作用是有助于我们弄清楚程序有无 Handle 泄漏的状况, Virtual Bytes 记录的是该进程当前于虚地址空间时所使用的虚拟内存的大小, NT 的内存分配运用了分两步走的办法, 第一步, 在虚地址空间上留存一段空间, 此时操作系统没有分配物理内存, 仅仅是保留了一段地址, 第二步, 再将这段空间提交, 这个时候操作系统才会去分配物理内存。所以, Virtual Bytes通常总是大于程序的Working Set。对Virutal Bytes进行监视能够助力我们发觉一些系统底层的问题, Working Set记载了操作系统针对进程已提交的内存的总量, 这个数值与程序申请的内存总量有着紧密的关联, 要是程序存在内存的泄漏, 这个值会持续不断地增加, 然而Virtual Bytes却是呈跳跃式增加的。

对这些Counter予以监视, 能够使我们知晓进程运用内存的情形, 要是出现了泄漏, 哪怕是隐式内存泄漏, 这些Counter的值也会持续不断地增加。然而, 我们虽晓得存在问题, 却不清楚问题究竟出在哪里, 所以通常借助Performance Monitor来验证是否存在内存泄漏, 而借助BoundsChecker来找出并解决。

当Performance Monitor呈现出存在内存泄漏的状况, 然而BoundsChecker却没办法检测出来的时候, 存在着两种可能性: 其一, 出现了偶发性内存泄漏。在这种情形下, 你需要保证在运用Performance Monitor以及运用BoundsChecker之际, 程序的运行环境以及操作方法是相同的。其二, 出现了隐式的内存泄漏。这时, 你得重新去审查程序的设计, 接着要仔细去研究Performance Monitor记录的Counter的值的相关变化图, 对之间的变化以及和程序运行逻辑的关系展开分析, 从中找寻一些有可能的原因。 这是个痛苦的过程, 充斥着假设、 猜想、求证、 失败, 不过这同样是个积累经验的绝妙好机遇。

总结

即使在拥有Gabarge Collection机制的Java和.Net这样的环境中, 内存泄漏也是个大且复杂的问题, 存在泄漏可能性, 像隐式内存泄漏。因篇幅与能力受限, 本文仅能对该主题做粗浅研究。多模块下的泄漏检测、程序运行时对内存使用情况进行分析等其他问题都是可深入研究的题目。若您有想法、建议或发现错误, 欢迎与我交流。

相关推荐: