编写内存泄露检测器的方法选择以及实现方式 c++

本文探讨了如何实现C++内存泄露检测器,包括重载new/delete的优缺点,通过malloc/free进行内存监测,特别是使用dlsym重写系统内存管理函数的方法,以及解决循环调用和多线程优化的策略。通过全局和特定的内存泄漏检测,以及性能优化,提供了一种有效的内存监控方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


目的

目前线上代码有一定的内存泄漏问题,大多数情况下这种bug都难以追踪定位,因此想开发一个内存监测小工具。
需要两种监测方式。一种是全局监测,纪录每一次内存的分配和释放活动;另一种是较为轻量级的监测,只监测部分疑似存在泄漏的code。

内存监测需要hack进内存分配和释放相关的代码,监测其每次的活动。

方法选择

1.重载new/delete

首先想到的是对管理动态分配内存的函数进行改写和监测。而new 和 delete很容易重写,且据说也是线上用的最多的动态分配内存方式。

重载 ::operator new() 的理由Effective C++ 第三版第 50 条列举了定制 new/delete 的几点理由:

    • 检测代码中的内存错误 
    • 优化性能 
    • 获得内存使用的统计数据 

           然而,这么做的缺点也有很多,参见《不要重载全局new》http://www.360doc.com/content/12/1211/17/9200790_253442412.shtml  主要问题是与c库的兼容问题

2.从malloc,free入手

既然不选择重载new/delete,就只能从malloc free入手了。且new delete的底层也是由malloc free实现的。这里大约由如下几种

1.#define malloc(x )  debug_malloc(x,__file__,__line__)

优点:方便

缺点:不能替换系统函数使用到的malloc,如 sprintf  new 等使用的还是系统malloc

2.修改glibc或(libc.so)

缺点:有很多函数,影响整个系统,不只是当前代码使用

3.__malloc_hook

优点:只要你在程序中写上”__malloc_hook = my_malloc_hook;”,之后的malloc调用都会使用my_malloc_hook函数,方便易行。

缺点:但是这组调试变量不是线程安全的,当你想用系统malloc的时候不得不把他们改回来,多线程调用就得上锁了。因此方案不很适用于系统内存优化,勉强用来简单管理线程内存使用。

详细用法: http://linux.die.net/man/3/__malloc_hook

/* Prototypes for our hooks.  */
static void my_init_hook(void);
static void *my_malloc_hook(size_t, const void *);
/* Variables to save original hooks. */
static void *(*old_malloc_hook)(size_t, const void *);
/* Override initializing hook from the C library. */
void (*__malloc_initialize_hook) (void) = my_init_hook;
static void
my_init_hook(void)
{
    old_malloc_hook = __malloc_hook;
    __malloc_hook = my_malloc_hook;
}
static void *
my_malloc_hook(size_t size, const void *caller)
{
    void *result;
    /* Restore all old hooks */
    __malloc_hook = old_malloc_hook;
    /* Call recursively */
    result = malloc(size);
    /* Save underlying hooks */
    old_malloc_hook = __malloc_hook;
    /* printf() might call malloc(), so protect it too. */
    printf("malloc(%u) called from %p returns %p\n",
            (unsigned int) size, caller, result);
    /* Restore our own hooks */
    __malloc_hook = my_malloc_hook;
    return result;
}


4.dlysm

调用dlsym来对系统malloc函数进行存取,保存为sys_malloc。然后就可以重写malloc了,在重写的malloc里面调用sys_malloc来进行真正的内存分配。

详细用法及注意事项:http://www.slideshare.net/tetsu.koba/tips-of-malloc-free

功能:
根据动态链接库操作句柄与符号,返回符号对应的地址。

包含头文件:
#include<dlfcn.h>
函数定义:
void* dlsym(void* handle,const char*symbol)
函数描述:
根据 动态链接库 操作句柄(handle)与符号(symbol),返回符号对应的地址。使用这个函数不但可以获取函数地址,也可以获取变量地址。
handle:由dlopen打开动态链接库后返回的指针;
symbol:要求获取的函数或全局变量的名称。
返回值:
void* 指向函数的地址,供调用使用。


 

使用DLYSM重写系统的内存管理函数

1.函数原型设计

extern "C" void* malloc(size_t size)
{
	void * ptr=sys_malloc(size);
	handle extra behavior...
	return ptr;
}

 使用sys_malloc来进行真正的内存分配,然后做一些记录等额外的操作,最后返回指针。

2.存储系统真正内存分配释放函数

static bool performance_enabled_ = false;
static void* (*sys_malloc)(size_t) = 0;
static void* (*sys_realloc)(void*,size_t) = 0;
static void* (*sys_calloc)(size_t,size_t) = 0;
static void (*sys_free)(void*) = 0;
 
static void initialize_functions()
{	
	sys_malloc = reinterpret_cast<void*(*)(size_t)>(dlsym(RTLD_NEXT, "malloc"));
	sys_realloc = reinterpret_cast<void*(*)(void*,size_t)>(dlsym(RTLD_NEXT, "realloc"));
	sys_calloc = reinterpret_cast<void*(*)(size_t,size_t)>(dlsym(RTLD_NEXT, "calloc"));
	sys_free = reinterpret_cast<void(*)(void*)>(dlsym(RTLD_NEXT, "free"));
}

 

进程可以使用 dlsym(3C) 获取特定符号的地址。此函数采用句柄符号名称,并将符号地址返回给调用方。特殊句柄 RTLD_NEXT 允许从调用方链接映射列表中的下一个关联目标文件获取符号。由于我们重写了malloc,RTLD_NEXT就指向了系统的malloc。这样,sys_malloc,sys_realloc,sys_calloc,sys_free则存储了系统的内存管理函数。

3.初始化问题

上面两条描述了malloc重写的核心操作。我们可以在第一次使用malloc的时候对sys_malloc进行初始化。

extern "C" void* malloc(size_t size)
{
    if(sys_malloc==0)
		initialize_functions();
	void * ptr=sys_malloc(size);
	handle extra behavior...
	return ptr;
}

然而,调用dlsym来对系统malloc函数进行存取的时候,会调用calloc,如果calloc也用dlsym进行重载了,会造成循环调用。在你调用dlsym这个函数时,dlsym会调用dlerrordlerror会调用calloccalloc要调用malloc,而你的malloc正在初始化等待dlsym返回中,于是死循环了。

因此在第一次调用malloc或者calloc的时候可以分配一段静态的内存供dlsym使用以解决这个问题。

char tmpbuff[1024];
unsigned long tmppos = 0;
unsigned long tmpallocs = 0;
extern "C" void* malloc(size_t size)
{
	static bool is_initializing = false;
	if(sys_malloc == 0)
	{
		if(!is_initializing)
		{
			is_initializing = true;
			initialize_functions();
			is_initializing = false;
		}
		else
		{
			if(tmppos+size<sizeof(tmpbuff))
			{
				void *retptr = tmpbuff + tmppos;
				tmppos += size;
				++tmpallocs;
				return retptr;
			}
			else
				exit(1);
		}
	}
	void* ptr = sys_malloc(size);
	handle extra behaviors....
	return ptr;
}

4.循环调用的问题

当extra behavior中涉及到动态内存分配和释放的行为时,就会出现循环调用。 extra behavior->malloc->extra behavior->malloc->....因此我们需要使用一个flag来标志需要记录的外部变量。通过此flag来判断是否进行额外操作。这里使用了thread local storage的标志 __thread, 用于标识每个线程的is_external情况。

static __thread bool is_external = true;
extern "C" void* malloc(size_t size)
{
	static bool is_initializing = false;
	if(sys_malloc == 0)
	{
		if(!is_initializing)
		{
			is_initializing = true;
			initialize_functions();
			is_initializing = false;
		}
		else
		{
			if(tmppos+size<sizeof(tmpbuff))
			{
				void *retptr = tmpbuff + tmppos;
				tmppos += size;
				++tmpallocs;
				return retptr;
			}
			else
				exit(1);
		}
	}
	void* ptr = sys_malloc(size);
	if(is_external)
	{
		is_external=false;
		handle extra behaviors....
		is_external=true;
	}
	return ptr;
}

5.辅助函数

使用 int backtrace(void **buffer,int size)  函数得到当前线程的调用堆栈。

使用 size_t malloc_usable_size (void *ptr) 函数获得该地址指针指向的内存大小。

note: malloc(size) calloc(size,cnt) realloc(ptr,size)等函数分配的内存大小不一定是调用函数的时候赋予的size的大小,主要是由于地址对齐的考虑。所以需要用malloc_usable_size来获得真正的内存大小。



Monitor 设计


对malloc,calloc,realloc,free使用 dlsym来对系统函数进行存取的方式进行重写。使得内存分配函数除了分配内存还能做一些记录内存分配的操作。

monitor主要数据结构如下

	//record general performance info
 	struct General_Performance
    {
        int64_t used_memory_;
        std::vector<void *> stack_trace_;
    };
	//record general performance info by address
	struct General_Performance_Map: public std::unordered_map<uint64_t, General_Performance>
    {
    };
	//record specific performance info
 	struct Specific_Performance
    {
        size_t count_;
        int64_t duration_; //mili seconds
        int64_t hold_memory_; //the memory change size during a monitor life time
        int64_t allocated_memory_;//the memory allocation size during a monitor life time
        int64_t start_time_; //mili seconds
        int64_t end_time_;  //mili seconds
        General_Performance_Map detail_;
    };
	//record specific performance info by guard's tag
	struct Specific_Performance_Map: public std::unordered_map<Ads_String, Specific_Performance>
    {
    };
	//record the specific & general info for each thread
    struct Thread_Performance_Info
    {
        int guards_num_; //the number of guards in current thread
        General_Performance_Map general_performance_map_;//record the general performance by address
        std::list<void*> stack_guard_father_call_names_;//record the father function called Guard as a stack
        std::list<Specific_Performance> stack_specific_performance_;//record the specific performance as a stack, each only manipulate the back element,
        Specific_Performance_Map specific_performance_map_;//record the specific performance by guard's tag
        Thread_Performance_Info();
        ~Thread_Performance_Info();//do merge work when destruct a thread info 
    };
	//when a guard object exist, the corresponding specific monitor will be at working status
    struct Guard
    {
        Ads_String tag_;
        int64_t start_time_;
        int64_t end_time_;
        Guard(const Ads_String& tag);
        ~Guard();
    };


1.全局的memory leak detection

对每一次内存的分配和释放都进行记录,并使用backtrace进行调用跟踪,获得全局的内存分配信息,需要通过编译选项打开.

1.1在每个线程创建一个unordered_map<int64, general_info> ,这里key是用于内存分配的指针,value是这个指针的当前大小以及导致这次内存分配/释放的call stack

1.2每次内存操作的时候更新该线程的map信息

1.3线程退出时,把当前的map信息merge到全局的general info map中


2.specific的memory leak detection

使用宏定义,展开成构造一个Guard,在Guard的对象的作用域内统计分配内存大小,起止时间,进入次数等信息。利用函数的栈调用形式,Guard可以使用栈结构存储,只操作栈顶元素,在需要pop时把信息merge到新的栈顶

2.1通过PERFORMANCE_MONITOR的宏展开成为函数名的guard结构体,并保存当前的时间信息入栈stack_specific_performance

2.2每次内存操作查看stack_specific_performance是否为空,不为空则记录内存的变化信息以及call stack到栈顶

2.3guard结构体生命周期结束析构的时候,记录当前时间,计算duration,start time,end time等信息,并弹出栈顶。如果栈不为空,则把当前的specific info 合并到新的栈顶元素。


3.性能优化

3.1多线程优化

工具的使用环境是多线程的,如果每次内存信息的记录都merge到全局的变量,必然要给变量加锁以免冲突,这会极大的降低效率。 因此我们需要构造记录监测信息的线程内变量,在线程退出的时候,把这些变量merge到全局变量中。这样可以极大的减少锁的使用。

因此可以使用__thread_local Thread_Performance_Info>的形式构造线程变量,并且在Thread_Performance_Info的析构函数中加入线程变量的merge行为。因为TSS变量在析构的时候,会调用类型的析构函数。这样就满足了在线程析构的时候才merge到全局变量的要求。



评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值