1、mtrace
mtrace(memory trace)
,是 GNU Glibc 自带的内存问题检测工具,追踪内存分配相关函数的调用,检测内存是否泄漏,定位内存泄漏的位置。
1.1、mtrace 函数
通过 hook 机制实现。
#include <mcheck.h> // 开启内存分配跟踪 void mtrace(void); // 取消内存分配跟踪 void muntrace(void);
1.2、设置日志生成路径
外部(线上系统):export`命令导出环境变量,需要重启
改进:使用进程热更新,则无需重启。
export MALLOC_TRACE=./mem.log
内部:setenv 函数增加或修改环境变量(只在本进程本次执行中生效)
setenv("MALLOC_TRACE", "./mem.log");
1.3、编译源码
调试程序,加上-g调试选项,创建符号表,关闭优化机制,这样可以定位到源码的具体位置,而不是可执行文件的地址信息。
gcc -o memleak memleak.c -g
1.4、运行分析
例:源码第 16 行 malloc 发生了内存泄漏。
运行结果如下:+ 表示申请内存, - 表示释放内存。对比后发现机器码 [0x400671] 处调用 malloc 申请的内存未释放。
1.5、定位源码位置addr2line
通过使用 addr2line 命令工具,得到源文件的行数(根据机器码地址定位到源码所在行数)
参数:
-f:显示函数名信息。
-e filename:指定需要转换地址的可执行文件名。
-a address:显示指定地址(十六进制)。
使用例子如下:
addr2line -f -e memleak -a 0x4011ab
2、宏定义
2.1、检测位置
使用宏定义,替换系统的内存分配函数,并在内部添加位置信息,实现监控
#define malloc(size) _malloc(size, __FILE__, __LINE__) #define free(p) _free(p, __FILE__, __LINE__)
这里,使用了系统宏定义,来追踪位置信息。
__FILE__,正在编译文件的文件名
__LINE__,正在编译文件的行号
2.2、改进方案
每次观察内存分配与释放是否一一对应的时候,令人恼火,有没有什么更好的方法?
我们可以在内存分配的同时,创建一个文件,文件名为指向新分配内存的指针,文件里记录源码调用内存分配函数时对应的位置;该内存释放时,删除该指针对应的文件。这样一来,程序结束后,如果没有文件则说明内存没有泄露;若存在文件,则说明内存发生泄漏。
2.3、运行分析
例:源码第 36 行 malloc 发生了内存泄漏。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> void *_malloc(size_t size, const char *filename, int line) { void *p = malloc(size); 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); // printf("[+]%s:%d, %p\n", filename, line, p); fflush(fp); fclose(fp); return p; } void _free(void *p, const 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(p) _free(p, __FILE__, __LINE__) int main() { void *p1 = malloc(10); void *p2 = malloc(15); void *p3 = malloc(20); free(p2); free(p3); return 0; }
运行结束后,可以看到,存在一个文件,说明发生了内存泄漏。查看文件信息后,结果显示在源码的第 36 行,正好对应指针 p1 的 malloc。
[+]memleak.c:36, addr: 0x1a88010, size: 10
3、hook
利用 hook 机制改写系统的内存分配函数,同样关键在于如何检测并确定内存泄漏的位置。
3.1、检测位置
系统宏定义__LINE__在函数内部调用,无法确定在主函数中的调用位置。
gcc 提供了 __builtin_return_address ,该函数返回当前函数或其调用者之一的返回地址。 参数level 表示向上扫描调用堆栈的帧数
void * __builtin_return_address(unsigned int level)
这样,我们可以获得主函数中调用系统内存分配函数的所在位置。
3.2、递归调用的问题
由于我们要改写系统内存的分配函数,不可避免的,对于某些系统函数,若其内部也调用了内存分配函数,则同样被改写了,会产生功能问题。
例:我们改写了系统内存分配函数 malloc 函数,调用 malloc 函数时执行printf 函数,而 printf 函数内部会调用系统的 malloc 函数。
void *malloc(size_t size) { printf("%s:%d\n", __FILE__, __LINE__); }
从 gdb 调试结果可以看出,调用 malloc 执行 printf 函数,调用 printf 执行 malloc 函数,因此陷入无尽的递归调用中,直至栈溢出。
Breakpoint 1, malloc (size=1024) at memleak.c:48 48 printf("%s:%d\n", __FILE__, __LINE__); (gdb) c Continuing
为了解决这一问题,我们设置变量 enable_malloc_hook
,当进入 malloc 函数内部后,根据自己的需要,设置 hook 的开关。在关闭的区域内调用 malloc 后进入到 else 部分执行原来的 hook 函数,避免了无限递归的发生。
int enable_malloc_hook = 1; void *malloc(size_t size) { // 执行改写的 malloc 函数 if (enable_malloc_hook) { enable_malloc_hook = 0; // 关闭 hook, printf 内部的 malloc 执行 else 的部分 printf("%s:%d\n", __FILE__, __LINE__); enable_malloc_hook = 1; } // 执行原来的 malloc 函数 else { p = malloc_f(size); } }
3.3、运行分析
例:源码第 82 行 malloc 发生了内存泄漏。
#define _GNU_SOURCE #include <dlfcn.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> 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 *malloc(size_t size) { void *p = NULL; if (enable_malloc_hook) { enable_malloc_hook = 0; enable_free_hook = 0; 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, "%s:[%p] + addr: %p, size: %ld\n", __FILE__, caller, p, size); fflush(fp); fclose(fp); enable_free_hook = 1; enable_malloc_hook = 1; } else { p = malloc_f(size); } return p; } void free(void *ptr) { if (enable_free_hook) { enable_free_hook = 0; enable_malloc_hook = 0; char buff[128] = {0}; sprintf(buff, "./mem/%p.mem", ptr); if (unlink(buff) < 0) { printf("double free: %p\n", ptr); return; } free_f(ptr); enable_malloc_hook = 1; enable_free_hook = 1; } else { free_f(ptr); } } 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"); } } int main() { init_hook(); void *p1 = malloc(10); void *p2 = malloc(15); void *p3 = malloc(20); free(p2); free(p3); return 0; }
运行结束后,可以看到,存在一个文件,说明发生了内存泄漏。查看文件信息如下:
memleak.c:[0x400b62] + addr: 0x117d010, size: 10
使用 addr2line 定位源码位置
addr2line -f -e memleak -a 0x400b62
内存泄漏的位置在源码的第 82 行,正好对应指针 p1 的 malloc。
0x0000000000400b62
main
/home/jtz/code/memleak.c:82
4、__libc_malloc
gcc 提供的__libc_malloc
, libc_free
接口用来代替系统的 malloc
, free
,实现 hook 机制。
#define _GNU_SOURCE #include <dlfcn.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> extern void *__libc_malloc(size_t size); extern void __libc_free(void *p); int enable_malloc_hook = 1; int enable_free_hook = 1; void *malloc(size_t size) { void *p = NULL; if (enable_malloc_hook) { enable_malloc_hook = 0; enable_free_hook = 0; p = __libc_malloc(size); // 返回函数堆栈 void *caller = __builtin_return_address(0); char buff[128] = {0}; sprintf(buff, "./mem/%p.mem", p); FILE *fp = fopen(buff, "w"); fprintf(fp, "%s:[%p] + addr: %p, size: %ld\n", __FILE__, caller, p, size); fflush(fp); fclose(fp); enable_free_hook = 1; enable_malloc_hook = 1; } else { p = __libc_malloc(size); } return p; } void free(void *ptr) { if (enable_free_hook) { enable_free_hook = 0; enable_malloc_hook = 0; char buff[128] = {0}; sprintf(buff, "./mem/%p.mem", ptr); if (unlink(buff) < 0) { printf("double free: %p\n", ptr); return; } __libc_free(ptr); enable_malloc_hook = 1; enable_free_hook = 1; } else { __libc_free(ptr); } } int main() { void *p1 = malloc(10); void *p2 = malloc(15); void *p3 = malloc(20); free(p2); free(p3); return 0; }
5、addr2line 的乱码问题
在高版本 gcc 下使用 addr2line 命令会出现乱码问题,以 hook 方法实现的内存泄漏检测代码为例,换用 gcc 11.2.0 执行相同的代码,查看内存泄漏文件的信息。
memleak.c:[0x5649280a4625] + addr: 0x564929ae02a0, size: 10
使用 addr2line 定位所在的源码位置
addr2line -f -e memleak -a 0x5649280a4625
此时出现了乱码
0x00005649280a4625
??
??:0
这是由于 addr2line 作用于 ELF 可执行文件,而高版本的 gcc 调用 __builtin_return_address返回的地址 caller 位于内存映像上,所以会产生乱码。
动态链接库的dladdr
函数 ,作用于共享目标,可以获取某个地址的符号信息,函数原型如下:
#define _GNU_SOURCE #include <dlfcn.h> int dladdr(void *addr, Dl_info *info); int dladdr1(void *addr, Dl_info *info, void **extra_info, int flags); Link with -ldl.
使用该函数可以解析符号地址
void* converToELF(void *addr) { Dl_info info; struct link_map *link; dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP); // elf地址 = 用户程序的地址 - 基地址 return (void *)((size_t)addr - link->l_addr); }
我们对 hook 方法实现的内存检测方法的代码进行修改,源码第 93 行 malloc 发生了内存泄漏。
#define _GNU_SOURCE #include <dlfcn.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <dlfcn.h> #include <link.h> 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* converToELF(void *addr) { Dl_info info; struct link_map *link; dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP); // printf("%p\n", (void *)(size_t)addr - link->l_addr); return (void *)((size_t)addr - link->l_addr); } void *malloc(size_t size) { void *p = NULL; if (enable_malloc_hook) { enable_malloc_hook = 0; enable_free_hook = 0; 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"); // 对 converToELF(caller) 返回的地址进行转换 fprintf(fp, "%s:[%p] + addr: %p, size: %ld\n", __FILE__, converToELF(caller), p, size); fflush(fp); fclose(fp); enable_free_hook = 1; enable_malloc_hook = 1; } else { p = malloc_f(size); } return p; } void free(void *ptr) { if (enable_free_hook) { enable_free_hook = 0; enable_malloc_hook = 0; char buff[128] = {0}; sprintf(buff, "./mem/%p.mem", ptr); if (unlink(buff) < 0) { printf("double free: %p\n", ptr); return; } free_f(ptr); enable_malloc_hook = 1; enable_free_hook = 1; } else { free_f(ptr); } } 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"); } } int main() { init_hook(); void *p1 = malloc(10); void *p2 = malloc(15); void *p3 = malloc(20); free(p2); free(p3); return 0; }
修改后执行程序,内存泄漏文件信息后,可以看到,ELF 地址 0x16af明显变小了。
memleak.c:[0x16af] + addr: 0x55a0d06b82a0, size: 10
使用 addr2line 定位所在的源码位置,验证成功。
0x00000000000016af
main
/home/jtz/memleak.c:93