内存泄漏是在没有自动GC的编程语言里面,经常发生的一些问题。要实现一个内存泄露的检测组件,有两个需求:
- 能够检测出来内存泄漏
- 能够判断出来哪一个地方的申请没有释放(哪一行引起的泄漏)
方案1:借助mtrace
mtrace
是一个Linux系统下的内存泄漏检测工具,它可以跟踪程序中的内存分配和释放操作,并记录每个内存块的分配和释放位置。通过分析mtrace
的输出,我们可以找到内存泄漏的地方。
下面是一个简单的示例程序,演示如何使用mtrace
来检测内存泄漏:
#include <stdio.h>
#include <stdlib.h>
#include <mcheck.h>
int main() {
setenv("MALLOC_TRACE", "trace.log", 1);
mtrace();
int* p = malloc(sizeof(int));
free(p);
muntrace();
return 0;
}
在这个程序中,我们使用了mcheck.h
头文件中的mtrace
和muntrace
函数来启用和禁用内存跟踪功能。在程序开始时,我们通过调用setenv
函数设置环境变量MALLOC_TRACE
,将内存跟踪日志输出到文件trace.log
中。然后,我们调用mtrace
函数启用内存跟踪。在程序结束时,我们调用muntrace
函数禁用内存跟踪。
运行程序后,mtrace
会记录程序中的内存分配和释放操作,并将跟踪日志输出到文件trace.log
中。我们可以使用mtrace
命令行工具来分析这个日志文件,找到内存泄漏的地方。例如,假设trace.log
文件的内容如下:
Memory not freed:
-----------------
Address Size Caller
0x0000000000602010 0x4 at /home/user/test.c:8
这个日志文件告诉我们,程序在文件test.c
的第8行分配了一个4字节的内存块,但是没有释放它。通过查看日志文件中的Caller
字段,我们可以找到这个内存块的分配位置。在这个例子中,我们可以看到,这个内存块是在test.c
文件的第8行分配的。
需要注意的是,mtrace
只能检测到使用malloc
和free
函数分配和释放的内存块,对于其他的内存分配和释放操作,mtrace
可能无法正确地跟踪和记录。此外,mtrace
会增加程序的运行时间和内存占用,因此在生产环境中应该谨慎使用。
方案2
实现自定义的内存分配和释放函数,它会在每次内存分配和释放时将分配和释放的地址、大小、文件名和行号等信息输出到一个以分配地址为名的文件中。这样可以方便地跟踪内存分配和释放的位置和大小,以便检测内存泄漏和其他内存相关的问题。
这个实现的原理是,在每次内存分配时,先调用系统的malloc
函数分配内存,然后根据分配的地址生成一个文件名,并将分配的地址、大小、文件名和行号等信息输出到文件中。在每次内存释放时,先根据释放的地址生成文件名,然后从文件系统中删除对应的文件,并调用系统的free
函数释放内存。
需要注意的是,这个实现并不是线程安全的,如果在多线程环境中使用,可能会出现竞争条件。此外,这个实现也会增加程序的运行时间和磁盘占用,因此做测试用就好。
void *_malloc(size_t size, char *filename, int line)
{
void *p = malloc(size);
// printf("[+]%s:%d, %p\n", filename, line, p);
char buff[128] = {0};
sprintf(buff,"./mem/%p.mem",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, char *filename, int line)
{
char buff[128] = {0};
sprintf(buff,"./mem/%p.mem",p);
if(unlink(buff) < 0)
{
printf("double free: %p\n",p);
return ;
}
return free(p);
}
#define malloc(size) _malloc(size, __FILE__, __LINE__)
#define free(size) _free(size, __FILE__, __LINE__)
方案3
这段代码在C语言中实现了对malloc
和free
函数的钩子(hook)。
malloc
函数用于分配指定大小的内存块,free
函数用于释放先前分配的内存块。
代码中定义了两个函数指针类型 malloc_t
和 free_t
,分别代表了指向 malloc
和 free
函数的指针类型。
然后定义了两个全局变量 malloc_f
和 free_f
,用于保存实际的 malloc
和 free
函数的地址。
接下来是两个整型变量 enable_malloc_hook
和 enable_free_hook
,用于控制是否启用钩子功能。
ConvertToELF
函数用于将传入的地址转换为 ELF(可执行和可链接格式)的地址。(这一步能让我们可以通过后面的addr2line找到泄露位置)
在 malloc
函数中,如果钩子功能被启用(enable_malloc_hook
为真),则会记录调用 malloc
的函数地址、分配的内存地址和大小,并将这些信息写入一个文件中。
在 free
函数中,如果钩子功能被启用(enable_free_hook
为真),则会删除之前记录的文件,并调用实际的 free
函数释放内存。
最后,init_hook
函数用于初始化钩子,通过 dlsym
函数获取实际的 malloc
和 free
函数的地址。
总的来说,这段代码通过钩子机制,在每次调用 malloc
和 free
函数时记录相关信息,并可以在分配和释放内存时执行额外的操作。
typedef void *(*malloc_t)(size_t size);
typedef void (*free_t)(void *ptr);
malloc_t malloc_f = NULL;
free_t free_f = NULL;
int enable_malloc_hook = 1;
int enable_free_hook = 1;
//
void *ConvertToELF(void *addr)
{
Dl_info info;
struct link_map *link;
dladdr1(addr,&info,(void**)&link,RTLD_DL_LINKMAP);
return (void *)((size_t)addr - link->l_addr);
}
void *malloc(size_t size)
{
void *p = NULL;
if (enable_malloc_hook)
{
enable_malloc_hook = 0;
// printf("malloc size: %ld\n", size);
p = malloc_f(size);
// 返回谁调用的这个谁所在地址
void *caller = __builtin_return_address(0);
char buff[128] = {0};
sprintf(buff, "./mem/%p.mem", p);
FILE *fp = fopen(buff, "w");
fprintf(fp, "[+]%p,addr:%p,size:%ld\n", ConvertToELF(caller), p, size);
fflush(fp);
enable_malloc_hook = 1;
}
else
{
p = malloc_f(size);
}
return p;
}
void free(void *p)
{
if (enable_free_hook)
{
enable_free_hook = 0;
char buff[128] = {0};
sprintf(buff, "./mem/%p.mem", p);
if (unlink(buff) < 0)
{
printf("double free:%p\n", p);
return;
}
free_f(p);
enable_free_hook = 1;
}
else
{
free_f(p);
}
// retu/rn;
}
static void init_hook(void)
{
if (malloc_f == NULL)
{
malloc_f = (malloc_t)dlsym(RTLD_NEXT, "malloc");
}
if (free_f == NULL)
{
free_f = (free_t)dlsym(RTLD_NEXT, "free");
}
}
对于上面的方案,内存分配释放的信息被写入文件后,我们以方案3举例,打开文件可以看到如下信息
然后我们借助addr2line工具,直接定位带泄漏位置,如下示例:
综上,我们即可找到内存泄露位置。