内存泄漏检测 linux,如何在linux下检测内存泄漏 (1)

1.开发背景

在 windows 下使用 VC 编程时,我们通常需要 DEBUG 模式下运行程序,而后调试器将在退出程序时,打印出程序运行过程中在堆上分配而没有释放的内存信息,其中包括代码文件名、行号以及内存大小。该功能是 MFC Framework 提供的内置机制,封装在其类结构体系内部。

在 linux 或者 unix 下,我们的 C++ 程序缺乏相应的手段来检测内存信息,而只能使用 top 指令观察进程的动态内存总额。而且程序退出时,我们无法获知任何内存泄漏信息。为了更好的辅助在 linux 下程序开发,我们在我们的类库项目中设计并实现了一个内存检测子系统。下文将简述 C++ 中的 new 和 delete 的基本原理,并讲述了内存检测子系统的实现原理、实现中的技巧,并对内存泄漏检测的高级话题进行了讨论。

回页首

2.New和delete的原理

当我们在程序中写下 new 和 delete 时,我们实际上调用的是 C++ 语言内置的 new operator 和 delete operator。所谓语言内置就是说我们不能更改其含义,它的功能总是一致的。以 new operator 为例,它总是先分配足够的内存,而后再调用相应的类型的构造函数初始化该内存。而 delete operator 总是先调用该类型的析构函数,而后释放内存(图1)。我们能够施加影响力的事实上就是 new operator 和 delete operator 执行过程中分配和释放内存的方法。

new operator 为分配内存所调用的函数名字是 operator new,其通常的形式是 void * operator new(size_t size); 其返回值类型是 void*,因为这个函数返回一个未经处理(raw)的指针,未初始化的内存。参数 size 确定分配多少内存,你能增加额外的参数重载函数 operator new,但是第一个参数类型必须是 size_t。

delete operator 为释放内存所调用的函数名字是 operator delete,其通常的形式是 void operator delete(void *memoryToBeDeallocated);它释放传入的参数所指向的一片内存区。

这里有一个问题,就是当我们调用 new operator 分配内存时,有一个 size 参数表明需要分配多大的内存。但是当调用 delete operator 时,却没有类似的参数,那么 delete operator 如何能够知道需要释放该指针指向的内存块的大小呢?答案是:对于系统自有的数据类型,语言本身就能区分内存块的大小,而对于自定义数据类型(如我们自定义的类),则 operator new 和 operator delete 之间需要互相传递信息。

当我们使用 operator new 为一个自定义类型对象分配内存时,实际上我们得到的内存要比实际对象的内存大一些,这些内存除了要存储对象数据外,还需要记录这片内存的大小,此方法称为 cookie。这一点上的实现依据不同的编译器不同。(例如 MFC 选择在所分配内存的头部存储对象实际数据,而后面的部分存储边界标志和内存大小信息。g++ 则采用在所分配内存的头 4 个自己存储相关信息,而后面的内存存储对象实际数据。)当我们使用 delete operator 进行内存释放操作时,delete operator 就可以根据这些信息正确的释放指针所指向的内存块。

以上论述的是对于单个对象的内存分配/释放,当我们为数组分配/释放内存时,虽然我们仍然使用 new operator 和 delete operator,但是其内部行为却有不同:new operator 调用了operator new 的数组版的兄弟- operator new[],而后针对每一个数组成员调用构造函数。而 delete operator 先对每一个数组成员调用析构函数,而后调用 operator delete[] 来释放内存。需要注意的是,当我们创建或释放由自定义数据类型所构成的数组时,编译器为了能够标识出在 operator delete[] 中所需释放的内存块的大小,也使用了编译器相关的 cookie 技术。

综上所述,如果我们想检测内存泄漏,就必须对程序中的内存分配和释放情况进行记录和分析,也就是说我们需要重载 operator new/operator new[];operator delete/operator delete[] 四个全局函数,以截获我们所需检验的内存操作信息。

回页首

3.内存检测的基本实现原理

上文提到要想检测内存泄漏,就必须对程序中的内存分配和释放情况进行记录,所能够采取的办法就是重载所有形式的operator new 和 operator delete,截获 new operator 和 delete operator 执行过程中的内存操作信息。下面列出的就是重载形式

void* operator new( size_t nSize, char* pszFileName, int nLineNum )

void* operator new[]( size_t nSize, char* pszFileName, int nLineNum )

void operator delete( void *ptr )

void operator delete[]( void *ptr )

我们为 operator new 定义了一个新的版本,除了必须的 size_t nSize 参数外,还增加了文件名和行号,这里的文件名和行号就是这次 new operator 操作符被调用时所在的文件名和行号,这个信息将在发现内存泄漏时输出,以帮助用户定位泄漏具体位置。对于 operator delete,因为无法为之定义新的版本,我们直接覆盖了全局的 operator delete 的两个版本。

在重载的 operator new 函数版本中,我们将调用全局的 operator new 的相应的版本并将相应的 size_t 参数传入,而后,我们将全局 operator new 返回的指针值以及该次分配所在的文件名和行号信息记录下来,这里所采用的数据结构是一个 STL 的 map,以指针值为 key 值。当 operator delete 被调用时,如果调用方式正确的话(调用方式不正确的情况将在后面详细描述),我们就能以传入的指针值在 map 中找到相应的数据项并将之删除,而后调用 free 将指针所指向的内存块释放。当程序退出的时候,map 中的剩余的数据项就是我们企图检测的内存泄漏信息--已经在堆上分配但是尚未释放的分配信息。

以上就是内存检测实现的基本原理,现在还有两个基本问题没有解决:

1) 如何取得内存分配代码所在的文件名和行号,并让 new operator 将之传递给我们重载的 operator new。

2) 我们何时创建用于存储内存数据的 map 数据结构,如何管理,何时打印内存泄漏信息。

先解决问题1。首先我们可以利用 C 的预编译宏 __FILE__ 和 __LINE__,这两个宏将在编译时在指定位置展开为该文件的文件名和该行的行号。而后我们需要将缺省的全局 new operator 替换为我们自定义的能够传入文件名和行号的版本,我们在子系统头文件 MemRecord.h 中定义:

#define DEBUG_NEW new(__FILE__, __LINE__ )

而后在所有需要使用内存检测的客户程序的所有的 cpp 文件的开头加入

#include "MemRecord.h"

#define new DEBUG_NEW

就可以将客户源文件中的对于全局缺省的 new operator 的调用替换为 new (__FILE__,__LINE__) 调用,而该形式的new operator将调用我们的operator new (size_t nSize, char* pszFileName, int nLineNum),其中 nSize 是由 new operator 计算并传入的,而 new 调用点的文件名和行号是由我们自定义版本的 new operator 传入的。我们建议在所有用户自己的源代码文件中都加入上述宏,如果有的文件中使用内存检测子系统而有的没有,则子系统将可能因无法监控整个系统而输出一些泄漏警告。

再说第二个问题。我们用于管理客户信息的这个 map 必须在客户程序第一次调用 new operator 或者 delete operator 之前被创建,而且在最后一个 new operator 和 delete operator 调用之后进行泄漏信息的打印,也就是说它需要先于客户程序而出生,而在客户程序退出之后进行分析。能够包容客户程序生命周期的确有一人--全局对象(appMemory)。我们可以设计一个类来封装这个 map 以及这对它的插入删除操作,然后构造这个类的一个全局对象(appMemory),在全局对象(appMemory)的构造函数中创建并初始化这个数据结构,而在其析构函数中对数据结构中剩余数据进行分析和输出。Operator new 中将调用这个全局对象(appMemory)的 insert 接口将指针、文件名、行号、内存块大小等信息以指针值为 key 记录到 map 中,在 operator delete 中调用 erase 接口将对应指针值的 map 中的数据项删除,注意不要忘了对 map 的访问需要进行互斥同步,因为同一时间可能会有多个线程进行堆上的内存操作。

好啦,内存检测的基本功能已经具备了。但是不要忘了,我们为了检测内存泄漏,在全局的 operator new 增加了一层间接性,同时为了保证对数据结构的安全访问增加了互斥,这些都会降低程序运行的效率。因此我们需要让用户能够方便的 enable 和 disable 这个内存检测功能,毕竟内存泄漏的检测应该在程序的调试和测试阶段完成。我们可以使用条件编译的特性,在用户被检测文件中使用如下宏定义:

#include "MemRecord.h"

#if defined( MEM_DEBUG )

#define new DEBUG_NEW

#endif

当用户需要使用内存检测时,可以使用如下命令对被检测文件进行编译

g++ -c -DMEM_DEBUG xxxxxx.cpp

就可以 enable 内存检测功能,而用户程序正式发布时,可以去掉 -DMEM_DEBUG 编译开关来 disable 内存检测功能,消除内存检测带来的效率影响。

4.错误方式删除带来的问题

以上我们已经构建了一个具备基本内存泄漏检测功能的子系统,下面让我们来看一下关于内存泄漏方面的一些稍微高级一点的话题。

首先,在我们编制 c++ 应用时,有时需要在堆上创建单个对象,有时则需要创建对象的数组。关于 new 和 delete 原理的叙述我们可以知道,对于单个对象和对象数组来说,内存分配和删除的动作是大不相同的,我们应该总是正确的使用彼此搭配的 new 和 delete 形式。但是在某些情况下,我们很容易犯错误,比如如下代码:

class Test {};

……

Test* pAry = new Test[10];//创建了一个拥有 10 个 Test 对象的数组

Test* pObj = new Test;//创建了一个单对象

……

delete []pObj;//本应使用单对象形式 delete pObj 进行内存释放,却错误的使用了数

//组形式

delete pAry;//本应使用数组形式 delete []pAry 进行内存释放,却错误的使用了单对

//象的形式

不匹配的 new 和 delete 会导致什么问题呢?C++ 标准对此的解答是"未定义",就是说没有人向你保证会发生什么,但是有一点可以肯定:大多不是好事情--在某些编译器形成的代码中,程序可能会崩溃,而另外一些编译器形成的代码中,程序运行可能毫无问题,但是可能导致内存泄漏。

既然知道形式不匹配的 new 和 delete 会带来的问题,我们就需要对这种现象进行毫不留情的揭露,毕竟我们重载了所有形式的内存操作 operator new,operator new[],operator delete,operator delete[]。

我们首先想到的是,当用户调用特定方式(单对象或者数组方式)的 operator new 来分配内存时,我们可以在指向该内存的指针相关的数据结构中,增加一项用于描述其分配方式。当用户调用不同形式的 operator delete 的时候,我们在 map 中找到与该指针相对应的数据结构,然后比较分配方式和释放方式是否匹配,匹配则在 map 中正常删除该数据结构,不匹配则将该数据结构转移到一个所谓 "ErrorDelete" 的 list 中,在程序最终退出的时候和内存泄漏信息一起打印。

上面这种方法是最顺理成章的,但是在实际应用中效果却不好。原因有两个,第一个原因我们上面已经提到了:当 new 和 delete 形式不匹配时,其结果"未定义"。如果我们运气实在太差--程序在执行不匹配的 delete 时崩溃了,我们的全局对象(appMemory)中存储的数据也将不复存在,不会打印出任何信息。第二个原因与编译器相关,前面提到过,当编译器处理自定义数据类型或者自定义数据类型数组的 new 和 delete 操作符的时候,通常使用编译器相关的 cookie 技术。这种 cookie 技术在编译器中可能的实现方式是:new operator 先计算容纳所有对象所需的内存大小,而后再加上它为记录 cookie 所需要的内存量,再将总容量传给operator new 进行内存分配。当 operator new 返回所需的内存块后,new operator 将在调用相应次数的构造函数初始化有效数据的同时,记录 cookie 信息。而后将指向有效数据的指针返回给用户。也就是说我们重载的 operator new 所申请到并记录下来的指针与 new operator 返回给调用者的指针不一定一致(图3)。当调用者将 new operator 返回的指针传给 delete operator 进行内存释放时,如果其调用形式相匹

亿恩科技地址(ADD):郑州市黄河路129号天一大厦608室 邮编(ZIP):450008 传真(FAX):0371-60123888

联系:亿恩小凡

QQ:89317007

电话:0371-63322206

本文出自:亿恩科技【www.enkj.com】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值