手写内存泄漏检测组件

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

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

常用内存泄漏检测组件: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
相关文章
|
2月前
|
监控 JavaScript Java
Node.js中内存泄漏的检测方法
检测内存泄漏需要综合运用多种方法,并结合实际的应用场景和代码特点进行分析。及时发现和解决内存泄漏问题,可以提高应用的稳定性和性能,避免潜在的风险和故障。同时,不断学习和掌握内存管理的知识,也是有效预防内存泄漏的重要途径。
182 52
|
2月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
58 6
|
2月前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
405 9
|
2月前
|
监控 JavaScript 前端开发
如何检测和解决 JavaScript 中内存泄漏问题
【10月更文挑战第25天】解决内存泄漏问题需要对代码有深入的理解和细致的排查。同时,不断优化和改进代码的结构和逻辑也是预防内存泄漏的重要措施。
83 6
|
2月前
|
Web App开发 缓存 JavaScript
如何检测和解决闭包引起的内存泄露
闭包引起的内存泄露是JavaScript开发中常见的问题。本文介绍了闭包导致内存泄露的原因,以及如何通过工具检测和代码优化来解决这些问题。
|
3月前
|
Web App开发 开发者
|
3月前
|
设计模式 Java Android开发
安卓应用开发中的内存泄漏检测与修复
【9月更文挑战第30天】在安卓应用开发过程中,内存泄漏是一个常见而又棘手的问题。它不仅会导致应用运行缓慢,还可能引发应用崩溃,严重影响用户体验。本文将深入探讨如何检测和修复内存泄漏,以提升应用性能和稳定性。我们将通过一个具体的代码示例,展示如何使用Android Studio的Memory Profiler工具来定位内存泄漏,并介绍几种常见的内存泄漏场景及其解决方案。无论你是初学者还是有经验的开发者,这篇文章都将为你提供实用的技巧和方法,帮助你打造更优质的安卓应用。
|
3月前
|
缓存 监控 Java
内存泄漏:深入理解、检测与解决
【10月更文挑战第19天】内存泄漏:深入理解、检测与解决
145 0
|
3月前
|
数据处理 Python
Python读取大文件的“坑“与内存占用检测
Python读取大文件的“坑“与内存占用检测
94 0
|
3月前
|
存储 算法 C语言
MacOS环境-手写操作系统-15-内核管理 检测可用内存
MacOS环境-手写操作系统-15-内核管理 检测可用内存
61 0