从源头解决内存泄漏问题:全面解析内存泄漏检测与修复技术

自己在_malloc函数和_free函数中调用malloc函数和free函数,并且做一些操作。前提:宏一定要放在内存分配之前定义,这样预编译阶段才会替换malloc为我们自己实现的_malloc和_free。
示例代码:
#include
#include
void *_malloc(size_t size,const char*filename,int line)
{
void *p = malloc(size);
printf("[+] %s : %d, %p\n", filename, line,p);
return p;
}
void _free(void *p, const char*filename, int line)
{
printf("[-] %s : %d, %p\n", filename, line,p);
return free(p);
}
#define malloc(size) _malloc(size,__FILE__,__LINE__)
#define free(p) _free(p,__FILE__,__LINE__)
int main(int argc,char **argv)
{
void *p1 = malloc(10);
void *p2 = malloc(20);
void *p3 = malloc(30);
free(p3);
free(p2);
return 0;
}
采用宏定义方法的优缺点:(1)优点,实现简单。(2)缺点,只适合单文件,宏定义要放在文件的最前面。
使用文件替换打印:程序运行时总是打印不必要的信息即影响效率也不美观,可以在一个文件夹里创建、删除文件来统计内存泄漏。将指针值作为文件名,分配内存创建文件,释放内存删除文件,在文件里面记录分配内存的文件名和行号。如果文件夹里有文件则存在内存泄漏,没有文件就代表没有内存泄漏。
#include
#include
#include
#define LEAK_FILE_PATH "./mem/%p.mem"
void *_malloc(size_t size,const char*filename,int line)
{
void *p = malloc(size);
//printf("[+] %s : %d, %p\n", filename, line,p);
char buff[128] = { 0 };
sprintf(buff, LEAK_FILE_PATH, p);
FILE *fp = fopen(buff, "w");
fprintf(fp, "[+] %s : %d, addr: %p, size: %ld\n", filename, line, p, size);
fflush(fp);//刷新数据到文件中
fclose(fp);
return p;
}
void _free(void *p, const char*filename, int line)
{
//printf("[-] %s : %d, %p\n", filename, line,p);
char buff[128] = { 0 };
sprintf(buff, LEAK_FILE_PATH, p);
if (unlink(buff) < 0)
{
printf("double free %p\n", p);
return;
}
return free(p);
}
#define malloc(size) _malloc(size,__FILE__,__LINE__)
#define free(p) _free(p,__FILE__,__LINE__)
int main(int argc,char **argv)
{
void *p1 = malloc(10);
void *p2 = malloc(20);
void *p3 = malloc(30);
free(p3);
free(p2);
return 0;
}
注意,工具只能加快分析,不能100%确定内存泄漏,因为复杂的系统情况比较复杂。检测内存泄漏不是一开始就加上,一般通过”热更新“的方式在需要的时候打开,即在配置文件中有一个打开内存泄漏检测的标志位。只有需要的时候才开启,这样不影响程序效率。
3.3、方式三:hook(钩子)
hook使用步骤:(1)定义函数指针。
typedef void *(*malloc_t)(size_t size);
typedef void(*free_t)(void *p);
malloc_t malloc_f = NULL;
free_t free_f = NULL;
(2)函数实现,函数名与目标函数名一致。
void *malloc(size_t size)
{
// ...
}
void free(void *ptr)
{
// ...
}
(3)初始化hook,调用dlsym()。
static init_hook()
{
if (malloc_f == NULL)
malloc_f = (malloc_t)dlsym(RTLD_NEXT, "malloc");
if (free_f == NULL)
free_f = (malloc_t)dlsym(RTLD_NEXT, "free");
}
注意:hook的时候,要考虑其他函数也用到所hook住的函数,比如在printf()函数里面也调用了malloc,那么就需要防止内部递归进入死循环。例如:
#define _GNU_SOURCE
#include
#include
#include
#include
typedef void *(*malloc_t)(size_t size);
typedef void(*free_t)(void *p);
malloc_t malloc_f = NULL;
free_t free_f = NULL;
void *malloc(size_t size)
{
printf("malloc size: %ld", size);
return NULL;
}
void free(void *ptr)
{
printf("free: %p\n",ptr);
}
static int init_hook()
{
if (malloc_f == NULL)
malloc_f = (malloc_t)dlsym(RTLD_NEXT, "malloc");
if (free_f == NULL)
free_f = (free_t)dlsym(RTLD_NEXT, "free");
return 0;
}
int main(int argc,char **argv)
{
init_hook();
void *p1 = malloc(10);
void *p2 = malloc(20);
void *p3 = malloc(30);
free(p3);
free(p2);
return 0;
}
以上代码会出现段错误,使用gdb调试会发现在malloc函数的printf()调用进入了无限递归;栈溢出。
解决方案就是添加标志。gcc内置功能:void * __builtin_return_address(无符号整数级别)此函数返回当前函数或其调用方之一的返回地址。参数是向上扫描调用堆栈的帧数。值 产生当前函数的返回地址,值 生成当前函数调用方的返回地址,依此类推。内联预期行为时,函数返回返回的函数的地址。若要变通解决此问题,请使用函数属性。level:该参数必须是常量整数。在某些计算机上,可能无法确定除当前功能之外的任何函数的返回地址;在这种情况下,或者当到达堆栈的顶部时,此函数返回未指定的值。此外,可用于确定是否已到达堆栈的顶部。
示例代码:
#define _GNU_SOURCE
#include
#include
#include
#define LEAK_FILE_PATH "./mem/%p.mem"
#include
static int enable_malloc_hook = 1;
static int enable_free_hook = 1;
typedef void *(*malloc_t)(size_t size);
typedef void(*free_t)(void *p);
malloc_t malloc_f = NULL;
free_t free_f = NULL;
void *malloc(size_t size)
{
void *p;
if (enable_malloc_hook)
{
enable_malloc_hook = 0;
p = malloc_f(size);
printf("malloc size: %ld,p=%p\n", size,p);
// 获取上一层调用malloc地方的地址,这个地址用于addr2line工具将其转换为行号
void *caller = __builtin_return_address(0);
char buff[128] = { 0 };
sprintf(buff, LEAK_FILE_PATH, p);
FILE *fp = fopen(buff, "w");
fprintf(fp, "[+] %p , addr: %p, size: %ld\n", caller, p, size);
fflush(fp);//刷新数据到文件中
fclose(fp);
enable_malloc_hook = 1;
}
else
p = malloc_f(size);
return p;
}
void free(void *p)
{
if (enable_free_hook)
{
enable_free_hook = 0;
//printf("free: %p\n",p);
char buff[128] = { 0 };
sprintf(buff, LEAK_FILE_PATH, p);
if (unlink(buff) < 0)
{
printf("double free %p\n", p);
//enable_free_hook = 1;
free_f(p);
return;
}
free_f(p);
enable_free_hook = 1;
}
else
free_f(p);
}
static int init_hook()
{
if (malloc_f == NULL)
malloc_f = (malloc_t)dlsym(RTLD_NEXT, "malloc");
if (free_f == NULL)
free_f = (free_t)dlsym(RTLD_NEXT, "free");
return 0;
}
int main(int argc,char **argv)
{
init_hook();
void *p1 = malloc(10);
void *p2 = malloc(20);
void *p3 = malloc(30);
free(p3);
free(p2);
return 0;
}
通过__builtin_return_address(0)得到的地址需要addr2line工具将其转换为文件行号即可定位内存泄漏的位置。
3.4、方式四:使用__libc_malloc和__libc_free
思路和hook的一样,因为malloc和free底层调用的也是__libc_malloc和__libc_free。
示例代码:
#define _GNU_SOURCE
#include
#include
#include
// 要记得手动创建一个mem文件夹
#define LEAK_FILE_PATH "./mem/%p.mem"
extern void *__libc_malloc(size_t size);
extern void __libc_free(void *p);
static int enable_malloc_hook = 1;
void *malloc(size_t size)
{
void *p;
if (enable_malloc_hook)
{
enable_malloc_hook = 0;
p = __libc_malloc(size);
printf("malloc size: %ld,p=%p\n", size, p);
// 获取上一层调用malloc地方的地址,这个地址用于addr2line工具将其转换为行号
void *caller = __builtin_return_address(0);
char buff[128] = { 0 };
sprintf(buff, LEAK_FILE_PATH, p);
FILE *fp = fopen(buff, "w");
fprintf(fp, "[+] %p , addr: %p, size: %ld\n", caller, p, size);
fflush(fp);//刷新数据到文件中
fclose(fp);
enable_malloc_hook = 1;
}
else
p = __libc_malloc(size);
return p;
}
void free(void *p)
{
char buff[128] = { 0 };
sprintf(buff, LEAK_FILE_PATH, p);
if (unlink(buff) < 0)
{
printf("double free %p\n", p);
}
__libc_free(p);
}
int main(int argc,char **argv)
{
void *p1 = malloc(10);
void *p2 = malloc(20);
void *p3 = malloc(30);
free(p3);
free(p2);
return 0;
}
3.5、方式五:__malloc_hook(不推荐)
这种方式适用于比较老的Linux 版本,属于旧版本的API,__malloc_hook是指针的方式,是一个固定的值。本质上也是一种hook技术。函数原型:
#include
void *(*__malloc_hook)(size_t size, const void *caller);
void *(*__realloc_hook)(void *ptr, size_t size, const void *caller);
void *(*__memalign_hook)(size_t alignment, size_t size, const void *caller);
void (*__free_hook)(void *ptr, const void *caller);
void (*__malloc_initialize_hook)(void);
void (*__after_morecore_hook)(void);
描述:GNUC库允许您通过指定适当的钩子函数来修改malloc、realloc和free的行为。例如,您可以使用这些挂钩来帮助调试使用动态内存分配的程序。
变量__malloc_initialize_hook指向在初始化malloc实现时调用一次的函数。这是一个弱变量,因此可以在应用程序中使用如下定义覆盖它:
void(*__malloc_initialize_hook)(void)=my_init_hook();
现在函数my_init_hook()可以初始化所有钩子。
__malloc_hook、__realloc_hooks、__memalign_hooke、__free_hooky指向的四个函数的原型分别与函数malloc、realloc和memalign。
方案:交换法,自定义函数指针,实现具体函数,将自己实现的函数与系统提供的__malloc_hook交换。
示例代码:
#define _GNU_SOURCE
#include
#include
#include
// 要记得手动创建一个mem文件夹
#define LEAK_FILE_PATH "./mem/%p.mem"
#include
/*
typedef void *(*malloc_t)(size_t size);
typedef void(*free_t)(void *p);
malloc_t malloc_f = NULL;
free_t free_f = NULL;
*/
static int enable_malloc_hook = 1;
static void my_init_hook(void);
static void *my_malloc_hook(size_t, const void *);
static void my_free_hook(void *, const void *);
/* Variables to save original hooks. */
static void *(*old_malloc_hook)(size_t, const void *);
static void(*old_free_hook)(void *, 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;
old_free_hook = __free_hook;
__free_hook = my_free_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);
if (enable_malloc_hook)
{
enable_malloc_hook = 0;
result = malloc(size);
/* printf() might call malloc(), so protect it too. */
printf("malloc(%u) called from %p returns %p\n",
(unsigned int)size, caller, result);
char buff[128] = { 0 };
sprintf(buff, LEAK_FILE_PATH, result);
FILE *fp = fopen(buff, "w");
fprintf(fp, "[+] %p , addr: %p, size: %ld\n", caller, result, size);
fflush(fp);//刷新数据到文件中
fclose(fp);
enable_malloc_hook = 1;
}
else
result = malloc(size);
/* Save underlying hooks */
old_malloc_hook = __malloc_hook;
/* Restore our own hooks */
__malloc_hook = my_malloc_hook;
return result;
}
static void my_free_hook(void *ptr, const void *caller)
{
__free_hook = old_free_hook;
free(ptr);
old_free_hook = __free_hook;
/* printf() might call malloc(), so protect it too. */
printf("free(%p) called from %p\n",
ptr, caller);
char buff[128] = { 0 };
sprintf(buff, LEAK_FILE_PATH, ptr);
if (unlink(buff) < 0)
{
printf("double free %p\n", ptr);
}
/* Restore our own hooks */
__free_hook = my_free_hook;
}
int main(int argc,char **argv)
{
my_init_hook();
void *p1 = malloc(10);
void *p2 = malloc(20);
void *p3 = malloc(30);
free(p3);
free(p2);
return 0;
}
编译的时候会出现警告,系统不推荐使用这样的方法。
四、完整示例代码
代码比较长,为了避免篇幅较长,不利于阅读,这里没有贴上。如果需要,可以联系博主,或者关注微信公众号 《Lion 莱恩呀》 获取。
总结
内存泄漏检测的核心是要知道有没有内存泄漏已经在哪里出现的内存泄漏。
检测内存泄漏的方式有:mtrace、hook、宏定义、libc_malloc、__malloc_hook。其中mtrace需要设MALLOC_TRACE环境变量并且需要重启;宏定义适用于单文件;__malloc_hook已经淘汰。
在编译程序时加上-g可以使用addr2line工具定位内存泄漏在文件中的位置。
为了提高程序效率,release程序采用“热更新”的方式在需要的时候设置配置文件标志符为进行内存泄漏检测。
欢迎关注公众号《Lion 莱恩呀》学习技术,每日推送文章。