手写内存泄漏检测组件

简介: 手写内存泄漏检测组件

  • 如何判断是否发生内存泄漏
  • 如何确定内存泄漏的位置

常用内存泄漏检测组件:valgrind

# 安装
 sudo apt install valgrind
 # 使用
 valgrind --tool=memcheck --leak-check=full ./memleak

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、运行分析

例:源码第 14 行 malloc 发生了内存泄漏。

#include <stdio.h>
 #include <stdlib.h>
 #include <mcheck.h>
 int main() {
     // 设置日志位置
     // 1、外部:export MALLOC_TRACE= ./mem.log 
     // 2、内部:setenv("MALLOC_TRACE", "./mem.log");
     setenv("MALLOC_TRACE", "./mem.log", 0);
     // 开启内存分配追踪
     mtrace();   
     void *p1 = malloc(10);
     void *p2 = malloc(15);
     void *p3 = malloc(20);
     free(p2);
     free(p3);
     // 取消内存分配跟踪
     muntrace();
     return 0;
 }

运行结果如下:+ 表示申请内存, - 表示释放内存。对比后发现机器码 [0x400671] 处调用 malloc 申请的内存未释放。

= Start
 // [机器码调用 malloc 的位置] + malloc 申请的地址 申请的内存大小
 @ ./memleak:[0x400671] + 0x6fa580 0xa
 @ ./memleak:[0x40067f] + 0x6fa5a0 0xf
 @ ./memleak:[0x40068d] + 0x6fa5c0 0x14
 // [机器码调用 free 的位置] - free 释放的地址 
 @ ./memleak:[0x40069d] - 0x6fa5a0
 @ ./memleak:[0x4006a9] - 0x6fa5c0
 = End

1.5、定位源码位置 addr2line

通过使用 addr2line 命令工具,得到源文件的行数(根据机器码地址定位到源码所在行数)

参数:

  • -f:显示函数名信息。
  • -e filename:指定需要转换地址的可执行文件名。
  • -a address:显示指定地址(十六进制)。

使用例子如下:

addr2line -f -e memleak -a 0x400671

结果显示在源码的第 14 行,正好对应指针 p1 的 malloc,这样就找到了内存泄漏的位置

0x0000000000400671
 main
 /home/jtz/code/memleak.c:14

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
相关文章
|
3月前
|
缓存 监控 Python
在Python中,如何检测和处理内存泄漏?
【2月更文挑战第7天】【2月更文挑战第18篇】在Python中,如何检测和处理内存泄漏?
|
4月前
|
安全 Linux 编译器
内存泄漏检测组件的分析与实现(linux c)-mtrace工具使用
内存泄漏产生原因 在堆上使用malloc/remalloc/calloc分配了内存空间,但是没有使用free释放对应的空间。
76 0
|
2月前
|
IDE Linux 开发工具
内存泄漏检测工具Valgrind:C++代码问题检测的利器(一)
内存泄漏检测工具Valgrind:C++代码问题检测的利器
96 0
|
2天前
|
Dart 前端开发 Java
【Flutter前端技术开发专栏】Flutter中的内存泄漏检测与解决
【4月更文挑战第30天】本文探讨了Flutter应用中的内存泄漏检测与解决方法。内存泄漏影响性能和用户体验,常见原因包括全局变量、不恰当的闭包使用等。开发者可借助`observatory`工具或`dart_inspector`插件监测内存使用。解决内存泄漏的策略包括避免长期持有的全局变量、正确管理闭包、及时清理资源、妥善处理Stream和RxDart订阅、正确 disposal 动画和控制器,以及管理原生插件资源。通过这些方法,开发者能有效防止内存泄漏,优化应用性能。
【Flutter前端技术开发专栏】Flutter中的内存泄漏检测与解决
|
2天前
|
数据可视化 Java 测试技术
【Go语言专栏】Go语言中的内存泄漏检测与修复
【4月更文挑战第30天】Go语言内存泄漏详解:概念、原因、检测与修复。内存泄漏由忘记释放内存、循环引用等引起,Go通过垃圾回收机制管理内存,但仍有泄漏风险。检测方法包括pprof、可视化工具、代码审查和单元测试。修复策略涉及优化代码、使用defer、减少全局变量、弱引用及及时释放资源。实践案例分析有助于理解和解决问题。了解内存管理,防止泄漏,提升Go应用性能和稳定性。
|
2天前
|
开发工具 Swift iOS开发
【Swift开发专栏】Swift中的内存泄漏检测与修复
【4月更文挑战第30天】本文探讨了Swift中的内存泄漏问题,尽管有ARC机制,但仍需关注内存管理。文章分为三部分:内存管理基础知识、检测方法和修复技巧。了解ARC原理和循环引用陷阱是防止内存泄漏的关键。检测方法包括使用Xcode内存调试器、LeakSanitizer和性能分析工具。修复技巧涉及打破循环引用、使用弱/无主引用及手动管理内存。理解这些对优化应用性能和稳定性至关重要。
|
2月前
|
缓存 Linux iOS开发
【C/C++ 集成内存调试、内存泄漏检测和性能分析的工具 Valgrind 】Linux 下 Valgrind 工具的全面使用指南
【C/C++ 集成内存调试、内存泄漏检测和性能分析的工具 Valgrind 】Linux 下 Valgrind 工具的全面使用指南
68 1
|
2月前
|
缓存 测试技术 开发工具
内存泄漏检测工具Valgrind:C++代码问题检测的利器(二)
内存泄漏检测工具Valgrind:C++代码问题检测的利器
36 0
|
2月前
|
Python
在Python中,如何检测和修复内存泄漏?
在Python中,如何检测和修复内存泄漏?
108 0
|
4月前
|
Web App开发 前端开发 JavaScript
JavaScript 内存泄漏的检测与防范:让你的程序更稳定
JavaScript 内存泄漏的检测与防范:让你的程序更稳定
JavaScript 内存泄漏的检测与防范:让你的程序更稳定