写点什么

全面解析内存泄漏检测与修复技术

  • 2023-10-18
    广东
  • 本文字数:9462 字

    阅读完需:约 31 分钟

全面解析内存泄漏检测与修复技术

本文分享自华为云社区《从源头解决内存泄漏问题:全面解析内存泄漏检测与修复技术》,作者 Lion Long 。

一、背景:什么是内存泄漏检测?

1.1、内存泄漏产生原因


内存泄漏是在没有自动 gc 的编程语言里面经常发生的问题;因为没有 gc,所以分配的内存需要程序自己调用释放。其核心是调用分配与释放没有符合开闭原则,没有配对,形成了有分配,没有释放的指针,从而产生了内存泄漏。例如:


void func(size_t s1){	void p1=malloc(s1);	void p2=malloc(s1);	// ...	free(p1);}
复制代码


以上代码段,分配了两个 s1 大小的内存块,由 p1 与 p2 指向。而代码块执行完以后,释放了 p1,而 p2 没有释放。形成了有分配没有释放的指针,产生了内存泄漏。

1.2、 内存泄漏导致的后果


随着工程代码量越来越多,内存泄漏的排查就变得极为头疼,一个程序,其虚拟内存一直在增长,无法准确判断是程序需要还是内存泄漏。有分配没释放,自然使进程堆的内存越来越少,直到耗尽。会造成后面的运行代码不能成功分配内存。甚至使程序奔溃。

1.3、内存泄漏如何解决?


内存泄漏是没有自动 gc 的编程语言所产生的,解决方案一,引入 gc。这是根治内存泄漏的最好的方案。但是这样的方案有失去了 c/c++语言的优势。方案二,当发生内存泄漏的时候,能够精准的定位代码哪一行所引起的。这也是实现内存泄漏检测的核心实现需求。


(1)能够检测出来内存泄漏。


(2)能够判断是由代码哪一行引起的内存泄漏。


一个程序,其虚拟内存一直在增长,无法准确判断是程序需要还是内存泄漏;如果是内存泄漏也不知道是发生在哪一行代码。

二、地址转换为符号信息

2.1、addr2line 工具


将地址转换为文件名和行号。


addr2line [-a|--addresses]          [-b bfdname|--target=bfdname]          [-C|--demangle[=style]]          [-e filename|--exe=filename]          [-f|--functions] [-s|--basename]          [-i|--inlines]          [-p|--pretty-print]          [-j|--section=name]          [-H|--help] [-V|--version]          [addr addr ...]

复制代码


描述:


addr2line 将地址转换为文件名和行号。给定可执行文件中的地址或可重定位对象部分中的偏移量,它使用调试信息确定与之关联的文件名和行号。


要使用的可执行或可重定位对象是用-e 选项指定的。默认为文件 a.out。要使用的可重定位对象中的节是用-j 选项指定的。


addr2line 有两种操作模式。


  • 在第一个命令行中,十六进制地址在命令行中指定,addr2line 显示每个地址的文件名和行号。

  • 在第二个命令中,addr2line 从标准输入中读取十六进制地址,并在标准输出中打印每个地址的文件名和行号。在这种模式下,addr2line 可以在管道中用于转换动态选择的地址。


注意:


addr2line 是将地址转换为文件号,而文件是保存在磁盘中的,程序运行的地址是在虚拟内存里面的(代码段),在高版本 Linux 中可能无法解析出地址在文件哪个位置。addr2line 只能看虚拟区域的地址。

2.2、dladdr1()函数


将地址转换为符号信息。函数原型:


#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.
复制代码


描述:


函数dladdr()确定 addr 中指定的地址是否位于调用应用程序加载的一个共享对象中。如果是,则dladdr()返回与 addr 重叠的共享对象和符号的信息。此信息以Dl_info结构返回:


typedef struct {    const char *dli_fname;  /* Pathname of shared object that contains address */    void       *dli_fbase;  /* Base address at which shared object is loaded */    const char *dli_sname;  /* Name of symbol whose definition overlaps addr */    void       *dli_saddr;  /* Exact address of symbol named in dli_sname */} Dl_info;

复制代码


函数dladdr1()类似于dladdr(),但通过参数extra_info返回附加信息。返回的信息取决于标志中指定的值,标志可以具有以下值之一:


(1)RTLD_DL_LINKMAP。获取指向匹配文件的链接映射的指针。extra_info 参数指向<link.h>中定义的 link_map 结构(即struct link_map**)的指针。


struct link_map {    ElfW(Addr) l_addr;  /* Difference between the address in the ELF file and the address in memory */    char      *l_name;  /* Absolute pathname where object was found */    ElfW(Dyn) *l_ld;    /* Dynamic section of the shared object */    struct link_map *l_next, *l_prev;                        /* Chain of loaded objects */
/* Plus additional fields private to the implementation */};
复制代码


(2)RTLD_DL_SYMENT。获取指向匹配符号的 ELF 符号表条目的指针。extra_info参数是指向符号指针的指针:const ElfW(Sym)**ElfW()宏定义将其参数转换为适合硬件体系结构的 ELF 数据类型的名称。例如,在 64 位平台上,ElfW(Sym)生成数据类型名称Elf64_Sym,该名称在<elf.h>中定义:


typedef struct  {    Elf64_Word    st_name;     /* Symbol name */    unsigned char st_info;     /* Symbol type and binding */    unsigned char st_other;    /* Symbol visibility */    Elf64_Section st_shndx;    /* Section index */    Elf64_Addr    st_value;    /* Symbol value */    Elf64_Xword   st_size;     /* Symbol size */} Elf64_Sym;

复制代码


封装:


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);}
复制代码

三、内存泄漏检测的实现


内 存 泄 漏 是由于 内 存 分 配 与 内 存 释放, 不 匹 配 所引起 的 。 对 内 存 分 配 函 数 malloc/calloc/realloc,以及内存释放 free 进行“劫持”hook。能够统计出内存分配的位置,内存释放的位置,从而判断是否匹配。

3.1、方式一:使用 mtrace

mtrace()和 muntrace()函数


mtrace 跟踪日志。函数原型:


#include <mcheck.h>
void mtrace(void);
void muntrace(void);

复制代码


描述:


mtrace()函数为内存分配函数安装 hook(钩子)函数【malloc()、realloc()、memalign(),free()】。这些 hook 函数记录有关内存分配和释放的跟踪信息。跟踪信息可用于发现内存泄漏,并尝试释放程序中未分配的内存。


muntrace()函数禁用mtrace()安装的 hook 函数,这样就不再为内存分配函数记录跟踪信息。如果mtrace()没有成功安装任何钩子函数,则muntrace()不执行任何操作。


调用mtrace()时,它会检查环境变量MALLOC_TRACE的值,该变量应包含要记录跟踪信息的文件的路径名。如果路径名成功打开,则其长度将被截断为零。


如果未设置MALLOC_TRACE,或者它指定的路径名无效或不可写,则不会安装 hook 函数,并且mtrace()无效。在set-user-ID 和 set-group-ID程序中,MALLOC_TRACE被忽略,mtrace()无效。

setenv()和 unsetenv()函数


改变或添加环境变量。函数原型:


#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
int unsetenv(const char *name);
/* Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
setenv(), unsetenv(): _BSD_SOURCE || _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600*/
复制代码


描述:


如果名称不存在,setenv()函数会将变量 name 添加到值为 value 的环境中。如果环境中确实存在 name,且 overwrite 非零,则其值将更改为 value;如果 overwrite 为零,则 name 的值不变(setenv()返回成功状态)。此函数复制 name 和 value 指向的字符串(与 putenv(3)相反)。


unsetenv()函数用于从环境中删除变量名。如果环境中不存在名称,则函数成功,环境不变。


返回值:


setenv()函数在成功时返回零,在错误时返回-1,并设置 errno 以指示错误的原因。


unsetenv()函数在成功时返回零,在错误时返回-1,并设置 errno 以指示错误的原因。


错误:


使用步骤


(1)在调用内存分配函数之前调用 mtrace();(2)在程序结束或者不需要追踪内存泄漏的地方调用 muntrace();(3)设置环境变量 MALLOC_TRACE 的值(setenv 函数或者 export 命令);(4)编译时要带上-g 参数。(5)当出现内存泄漏时,使用 addr2line 工具定位内存泄漏的位置。


$ addr2line -f -e memleak -a 0x4006b8
复制代码


示例中 memleak 是程序名,0x4006b8 是内存泄漏的地址。


例如:


$ cc -g t_mtrace.c -o t_mtrace$ export MALLOC_TRACE=/tmp/t$ ./t_mtrace$ mtrace ./t_mtrace $MALLOC_TRACE

复制代码

示例代码


#include <stdio.h>#include <stdlib.h>
#include <mcheck.h>
int main(int argc,char **argv){
setenv("MALLOC_TRACE", "./mem.txt", 1);
mtrace();
void *p1 = malloc(10); void *p2 = malloc(20); void *p3 = malloc(30);
free(p3); free(p2); muntrace();
unsetenv("MALLOC_TRACE");
return 0;}
复制代码


内存泄漏检测文件内容:


$ cat mem.txt = Start@ ./memleak:[0x4006b8] + 0x1886580 0xa@ ./memleak:[0x4006c6] + 0x18865a0 0x14@ ./memleak:[0x4006d4] + 0x18865c0 0x1e@ ./memleak:[0x4006e4] - 0x18865c0@ ./memleak:[0x4006f0] - 0x18865a0= End

复制代码


定位内存泄漏位置:


$ addr2line -f -e memleak -a 0x4006b80x00000000004006b8mainmemleak.c:13
复制代码

3.2、方式二:采用宏定义


Linux 中有两个宏__FILE____func____LINE__,分别指示当前的文件名、函数名和行号,利用宏定义封装内存分配函数和释放函数。


#define malloc(size)	_malloc(size,__FILE__,__LINE__)#define free(p)			_free(p,__FILE__,__LINE__)
复制代码


自己在_malloc 函数和_free 函数中调用 malloc 函数和 free 函数,并且做一些操作。


前提:宏一定要放在内存分配之前定义,这样预编译阶段才会替换 malloc 为我们自己实现的_malloc_free



示例代码:


#include <stdio.h>#include <stdlib.h>
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 <stdio.h>#include <stdlib.h>#include <unistd.h>
#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 <stdio.h>#include <stdlib.h>#include <unistd.h>
#include <dlfcn.h>
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 <stdio.h>#include <stdlib.h>#include <unistd.h>
#define LEAK_FILE_PATH "./mem/%p.mem"
#include <dlfcn.h>
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 <stdio.h>#include <stdlib.h>#include <unistd.h>
// 要记得手动创建一个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 <malloc.h>
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 <stdio.h>#include <stdlib.h>#include <unistd.h>
// 要记得手动创建一个mem文件夹#define LEAK_FILE_PATH "./mem/%p.mem"
#include <malloc.h>
/*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 voidmy_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 程序采用“热更新”的方式在需要的时候设置配置文件标志符为进行内存泄漏检测。


点击关注,第一时间了解华为云新鲜技术~

发布于: 2023-10-18阅读数: 2
用户头像

提供全面深入的云计算技术干货 2020-07-14 加入

生于云,长于云,让开发者成为决定性力量

评论

发布
暂无评论
全面解析内存泄漏检测与修复技术_程序员_华为云开发者联盟_InfoQ写作社区