c内存泄漏检测工具:用DEBUG_NEW定位泄漏点

检测维修 0 135

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

# 定义 DEBUG_NEW 为,new(THIS_FILE, __LINE__)。

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

char* p = new char;

经过宏替换就变成了:

将字符指针p,通过在当前文件位置THIS_FILE,以及当前行号__LINE__处进行动态内存分配,来创建字符类型的指针,使其指向新分配的内存地址。

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

无返回值指针类型的运算符新,其参数为大小尺寸类型,常量字符串指针类型,整型类型,用于分配内存空间。

在afxmem.cpp文件的63行处,我们寻得了一个呈现如此状态的operator new的达成方式。

产生一个指向空类型的指针,通过AFX_CDECL方式定义的运算符新,其参数为一个表示大小的无符号整数类型,一个指向常量字符串类型的指针,以及一个整数类型。

返回,运算符新,在大小为nSize的情况下,采用_NORMAL_BLOCK类型,关联lpszFileName文件名,处于nLine行号处。

void*,以__cdecl方式声明的,名为operator new的函数,其参数有,size_t类型的nSize,int类型的nType,LPCSTR类型的lpszFileName,int类型的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程序的环境里把内存泄漏的检测功能予以开启,这是极为简单的事情,你只需要在程序开始的入口之处添加如下的几行代码就可以了:

int型的tmpFlag被赋予了_CrtSetDbgFlag函数返回的值,该函数的参数是_CRTDBG_REPORT_FLAG。

tmpFlag进行或运算,其值被设置为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不用去修改被调试程序的源代码,也不必变换工程配置文件,如此一来用它就极为简便、直接了。

在此处,我们将malloc函数当作例子,去截获其他函数,其方法如同这般相似。

被需要截获的函数,其位置有可能处于DLL之中,也有可能处在程序的代码里面。举例来说,要是静态连结C - Runtime Library,那么malloc函数的代码将会被连结至程序里。为了能够截获住针对这类函数的调用,BoundsChecker会对这些函数的指令进行动态修改。

有两段汇编代码,一段是没有BoundsChecker 介入的,另一段是有BoundsChecker 介入的。

一百二十六比:_CRTIMP 无效指针 通过__cdecl方式调用 进行申请块内存 (这里的malloc是用于)

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 实现内存跟踪_C++ DEBUG_NEW 宏内存泄漏检测_c内存泄漏检测工具

00403C1F mov ecx,dword ptr

00403C22 push ecx

00403C23 call _nh_malloc_dbg (00403c80)

00403C28 add esp,14h

131: }

BoundsChecker介入之际,函数malloc的头三条汇编指令被置换成单独一条jmp指令,原本的三条指令被移至地址01F41EC8之处了。程序进入malloc后先跳转至01F41EC8,执行原先的三条指令,而后便是BoundsChecker的主场了。基本上它会先去记录函数的返回地址,函数的返回地址处于stack之上,因其处于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来检测内存泄漏,Virutal Bytes记录了该进程当前于虚地址空间上所使用的虚拟内存的大小,NT对内存的分配运用了分两步走的方式,首先,在虚地址空间上预留出一段空间,此时操作系统并未分配物理内存,仅仅是留存了一段地址,Handle Count记录的是进程当前打开的HANDLE的数量,监视这个Counter有助于察觉程序是否存在Handle泄漏。而后,再度递交这段空间,此时操作系统方才会分配物理内存。故而,Virtual Bytes通常总超出程序的 Working Set。监控 Virutal Bytes能够协助我们发觉某些系统底层的问题,Working Set记录了操作系统针对进程已提交的内存的总量,此值与程序申请的内存总量存有紧密的关系,要是程序存在内存的泄漏此值会持续增长,但 Virutal Bytes却是呈跳跃式增长的。

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

当Performance Monitor呈现出存在内存泄漏的状况,然而BoundsChecker却没办法检测出来时,存在着两种可能性:其一,出现了偶发性内存泄漏。在这种情形下,你务必要保证在运用。此时,你需再度审视程序的设计,而后认真钻研Performance Monitor所记录的Counter的值的变化图表,剖析其中的变化与程序运行逻辑之间的关联,寻觅一些潜在的缘由。这是一段令人煎熬的进程,充斥着假设、揣度、验证、受挫,然而这亦是一个积累经验的绝佳契机。

相关推荐: