mtrace和memleak源码分析
文章目录
- 1. 内存泄漏
- 2. 定位工具
- 2.1 memleak 工具定位内存泄漏
- 2.1.1 源码解读
- 2.2 mtrace 工具定位内存泄漏
- 2.2.1 源码解读
1. 内存泄漏
内存泄漏(Memory Leak)指程序中已动态分配的堆内存因未正确释放或无法释放,导致系统内存资源被持续占用且无法回收的现象。其核心特征是隐蔽性和积累性,可能不会立即引发程序崩溃,但会随着时间推移逐渐消耗系统资源,最终引发性能下降或系统崩溃。
内存泄漏影响如下:
- 性能下降
- 内存资源耗尽:内存泄漏会持续占用可用内存,导致程序可用内存减少,频繁触发垃圾回收(GC),增加CPU负担;
- 内存碎片化:泄漏的内存块可能分散在堆中,导致可用内存被分割为碎片,降低内存分配效率;
- 系统稳定性受损
- 程序崩溃:当内存泄漏累积到系统可用内存极限时,可能引发内存溢出(OOM),导致程序强制终止;
- 资源竞争加剧:泄漏的内存可能影响其他进程或线程的正常运行,导致系统整体响应延迟;
2. 定位工具
内存泄漏,直接的表现就是 free
指令会显示剩余内存越来越少,少到一定程度先是会触发SWAP交换,如果SWAP交换后还是内存不足,就会触发OOM现象。
常见的定位工具有vargrind
、memleak
、 mtrace
,其中嵌入式设备因为内存限制,使用最普遍的是memleak
和 mtrace
工具。
2.1 memleak 工具定位内存泄漏
memleak 的原理是利用C语言的宏调用来替代原有的函数调用,如代码中调用malloc(s),实际是调用了dbg_malloc(s),其他的函数类似,如free,calloc都有一个对应的宏替代函数。memleak 工具的本质是内部维护了一个双向链表,用来存储每一次申请的内存地址、大小等信息,free会释放对应链表节点信息。
2.1.1 源码解读
-
源码下载
进入
sourceforge
官网,搜索即可,实际下载网址如下 memleak源码下载解压缩得到
memleak-0.3.1
文件夹,有如下文件:$ dir example.c LICENSE Makefile memleak.c memleak.h README
- example.c:meamleak工具的示例说明,演示内存泄漏跟踪的结果;
- LICENSE:软件的一些说明,如遵循的协议;
- Makefile:用于编译example.c;
- memleak.c:相关宏替换接口的实际定义,如
dbg_free
; - memleak.h:相关接口的宏替换声明;
- README:功能以及更新说明;
-
memleak.h 主要解读
#define FILE_LINE dbg_file_name = __FILE__, dbg_line_number = __LINE__ #define malloc(s) (FILE_LINE, dbg_malloc(s)) #define realloc(p, s) (FILE_LINE, dbg_realloc(p, s)) #define calloc(n, s) (FILE_LINE, dbg_calloc(n, s)) #define free(p) (FILE_LINE, dbg_free(p))
从上面可以看到,它将C库的标准函数
malloc
、realloc
和calloc
、free
都进行了宏替换,而且还使用了一个逗号表达式来将当前file
和line
进行赋值,便于后面对链表节点的赋值。 -
memleak.c主要解读
链表节点定义:struct head {void *addr;size_t size;char *file;unsigned long line;/* two addresses took the same space as an address and an integer on many archs => usable */union {struct { struct head *prev, *next; } list;struct { char *file; unsigned long line; } free;} in; };
上面定义了一个链表节点结构,可以用来存储本次申请内存的 地址->
addr
大小->size
文件->file
行数->line
,如果是申请内存,则会存在实用前向指针prev
指向前面一次的内存申请节点,后向指针next
指向NULL;如果是释放内存,这存储文件名file
和 当前行数line
。static struct head *first = NULL, *last = NULL;#define HLEN sizeof(struct head)static struct head *add(void *buf, size_t s) {struct head *p;p = malloc(HLEN);if(p){p->addr = buf;p->size = s;p->file = dbg_file_name;p->line = dbg_line_number;p->in.list.prev = last;p->in.list.next = NULL;if(last)last->in.list.next = p;elsefirst = p;last = p;memory_cnt += s;}return p; }void *dbg_malloc(size_t s) {void *buf;malloc_cnt++;buf = malloc(s);if(buf){if(add(buf, s))return buf;elsefree(buf);}fprintf(stderr, "%s:%lu: dbg_malloc: not enough memory\n", dbg_file_name, dbg_line_number);return NULL; }
上面是申请内存完成后,调用add
函数,内部申请了一个 head
节点保存了申请的地址,大小,申请内存的文件以及行数,然后前向节点指向上一次申请的内存节点last
, last
的后向节点只向本次申请的内存p
,然后p
变成了最后一次申请的内存节点last
。
static void del(struct head *p)
{struct head *prev, *next;prev = p->in.list.prev;next = p->in.list.next;if(prev)prev->in.list.next = next;elsefirst = next;if(next)next->in.list.prev = prev;elselast = prev;memory_cnt -= p->size;/* update history */if(history_length){p->in.free.file = dbg_file_name;p->in.free.line = dbg_line_number;memcpy(histp, p, HLEN);ADVANCE(histp);}free(p);
}static struct head *find_in_heap(void *addr)
{struct head *p;/* start search from lately allocated blocks */ for(p = last; p; p = p->in.list.prev)if(p->addr == addr) return p;return NULL;
}void dbg_free(void *buf)
{struct head *p;free_cnt++;if(buf)if((p = find_in_heap(buf))){del(p);free(buf);}elsedbg_check_addr("dbg_free", buf, CHK_FREED);elsefprintf(stderr, "%s:%lu: dbg_free: NULL\n", dbg_file_name, dbg_line_number);
}
上面在内存释放完成之后,从维护的双向链表中找到对应的free
节点信息,然后进行删除链表节点,并删除实际的内存。
void dbg_mem_stat(void)
{fprintf(stderr, "%s:%lu: m: %d, c: %d, r: %d, f: %d, mem: %ld\n",dbg_file_name, dbg_line_number,malloc_cnt, calloc_cnt, realloc_cnt, free_cnt, memory_cnt);
}
上面的函数会把内存申请以及释放的次数进行打印。
void dbg_heap_dump(char *key)
{char *buf;struct head *p;fprintf(stderr, "***** %s:%lu: heap dump start\n", dbg_file_name, dbg_line_number);p = first;while(p){buf = malloc(strlen(p->file) + 2*length(long) + 20);sprintf(buf, "(alloc: %s:%lu size: %lu)\n", p->file, p->line, (unsigned long)p->size);p = p->in.list.next;if(strstr(buf, key)) fputs(buf, stderr);free(buf);}fprintf(stderr, "***** %s:%lu: heap dump end\n", dbg_file_name, dbg_line_number);
}
上面的函数会从漏释放的内存的文件名,行数,以及大小进行打印。
通过上面的源码解读,其实只要在合适的地方使用 dbg_heap_dump
接口就可以统计没有释放的内存的情况了。示例我们在下一篇文章进行讲解。
2.2 mtrace 工具定位内存泄漏
2.2.1 源码解读
-
mtrace
并没有像memleak
一样有单独的源码,GLIBC 开源库本身就支持mtrace
功能,mtrace
本质是在调用void mtrace (void)
接口的时候,内部会为malloc
注册tr_mallochook
钩子hook
函数,钩子函数注册如下(以下全部以GLIBC2.25版本进行讲解, GLIBC源码下载):c函数 对应hook函数 free tr_freehook malloc tr_mallochook calloc tr_mallochook realloc tr_reallochook memalign tr_memalignhook -
主要源码解读
void
mtrace (void)
{
#ifdef _LIBCstatic int added_atexit_handler;
#endifchar *mallfile;/* Don't panic if we're called more than once. */if (mallstream != NULL)return;#ifdef _LIBC/* Make sure we close the file descriptor on exec. */int flags = __fcntl (fileno (mallstream), F_GETFD, 0);if (flags >= 0){flags |= FD_CLOEXEC;__fcntl (fileno (mallstream), F_SETFD, flags);}
#endif/* Be sure it doesn't malloc its buffer! */malloc_trace_buffer = mtb;setvbuf (mallstream, malloc_trace_buffer, _IOFBF, TRACE_BUFFER_SIZE);fprintf (mallstream, "= Start\n");tr_old_free_hook = __free_hook;__free_hook = tr_freehook;tr_old_malloc_hook = __malloc_hook;__malloc_hook = tr_mallochook;tr_old_realloc_hook = __realloc_hook;__realloc_hook = tr_reallochook;tr_old_memalign_hook = __memalign_hook;__memalign_hook = tr_memalignhook;
#ifdef _LIBCif (!added_atexit_handler){extern void *__dso_handle __attribute__ ((__weak__));added_atexit_handler = 1;__cxa_atexit ((void (*)(void *))release_libc_mem, NULL,&__dso_handle ? __dso_handle : NULL);}
#endif}elsefree (mtb);}
}
上面的old
系列函数,在初始化的时候都置为了NULL
,如下所示,然后使用一个全局的old
指针指向这些NULL
指针,新的钩子指针函数就指向了上面说的表格里面对应的函数。
static void *
malloc_hook_ini (size_t sz, const void *caller)
{__malloc_hook = NULL;ptmalloc_init ();return __libc_malloc (sz);
}static void *
realloc_hook_ini (void *ptr, size_t sz, const void *caller)
{__malloc_hook = NULL;__realloc_hook = NULL;ptmalloc_init ();return __libc_realloc (ptr, sz);
}static void *
memalign_hook_ini (size_t alignment, size_t sz, const void *caller)
{__memalign_hook = NULL;ptmalloc_init ();return __libc_memalign (alignment, sz);
}void weak_variable (*__free_hook) (void *__ptr,const void *) = NULL;
-
下面我们以
malloc
的钩子函数tr_mallochook
以及free
的钩子函数tr_freehook
为例,看一下是如何跟踪内存申请和释放的。static __ptr_t tr_mallochook (size_t size, const __ptr_t caller) {__ptr_t hdr;Dl_info mem;Dl_info *info = lock_and_info (caller, &mem);__malloc_hook = tr_old_malloc_hook;if (tr_old_malloc_hook != NULL)hdr = (__ptr_t) (*tr_old_malloc_hook)(size, caller);elsehdr = (__ptr_t) malloc (size);__malloc_hook = tr_mallochook;tr_where (caller, info);/* We could be printing a NULL here; that's OK. */fprintf (mallstream, "+ %p %#lx\n", hdr, (unsigned long int) size);__libc_lock_unlock (lock);if (hdr == mallwatch)tr_break ();return hdr; }
上面的第9行,因为
tr_old_malloc_hook
是NULL
,所以内存申请走的 第13行,使用的malloc
进行内存申请,malloc
函数最终会调用__libc_malloc
,此函数内部中因为hook
已经被置位为了NULL
,所以不会再次调用hook
递归,后面就是malloc
申请内存的方式了,这部分内容请允许我后面的专题补充,本次只讲解mtrace
的跟踪流程。strong_alias (__libc_free, __free) strong_alias (__libc_free, free) strong_alias (__libc_malloc, __malloc) strong_alias (__libc_malloc, malloc)/* Define ALIASNAME as a strong alias for NAME. */ # define strong_alias(name, aliasname) _strong_alias(name, aliasname) # define _strong_alias(name, aliasname) \extern __typeof (name) aliasname __attribute__ ((alias (#name)));
上面的宏定义会使得
name
与aliasname
完全等价,也就是我们的malloc
等价于__libc_malloc
,free
等价于__libc_free
。__libc_malloc
内容如下:void * __libc_malloc (size_t bytes) {mstate ar_ptr;void *victim;void *(*hook) (size_t, const void *)= atomic_forced_read (__malloc_hook);if (__builtin_expect (hook != NULL, 0))return (*hook)(bytes, RETURN_ADDRESS (0));arena_get (ar_ptr, bytes);victim = _int_malloc (ar_ptr, bytes);/* Retry with another arena only if we were able to find a usable arenabefore. */if (!victim && ar_ptr != NULL){LIBC_PROBE (memory_malloc_retry, 1, bytes);ar_ptr = arena_get_retry (ar_ptr, bytes);victim = _int_malloc (ar_ptr, bytes);}if (ar_ptr != NULL)__libc_lock_unlock (ar_ptr->mutex);assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||ar_ptr == arena_for_chunk (mem2chunk (victim)));return victim; }
上面的
__libc_malloc
接口主要调用_int_malloc
完成内部分配,具体分配细节我们后面专题讲解。当完成内存分配后,函数会继续跳转到tr_mallochook
进行执行。重点来了,看重点:
static __ptr_t tr_mallochook (size_t size, const __ptr_t caller) {__ptr_t hdr;Dl_info mem;Dl_info *info = lock_and_info (caller, &mem);__malloc_hook = tr_old_malloc_hook;if (tr_old_malloc_hook != NULL)hdr = (__ptr_t) (*tr_old_malloc_hook)(size, caller);elsehdr = (__ptr_t) malloc (size);__malloc_hook = tr_mallochook;tr_where (caller, info);/* We could be printing a NULL here; that's OK. */fprintf (mallstream, "+ %p %#lx\n", hdr, (unsigned long int) size);__libc_lock_unlock (lock);if (hdr == mallwatch)tr_break ();return hdr; }
上卖弄的第13行完成内存申请,第14行,将
hook
重新赋值,第18行会把内存申请的信息(地址+大小)进行打印,打印到mallstream
文件描述符中。其中mallstream
是我们环境变量MALLOC_TRACE
实际值得到的,所以我们可以在命令行或者使用setenv
函数设置即可改变实际打印的地方。上面就是大概申请内存
malloc
时候的流程了,最终会将申请内存的信息打印到MALLOC_TRACE
设置的文件里面。 -
下面讲一下
free
的流程,整体上和malloc
流程类似void __libc_free (void *mem) {mstate ar_ptr;mchunkptr p; /* chunk corresponding to mem */void (*hook) (void *, const void *)= atomic_forced_read (__free_hook);if (__builtin_expect (hook != NULL, 0)){(*hook)(mem, RETURN_ADDRESS (0));return;}if (mem == 0) /* free(0) has no effect */return;p = mem2chunk (mem);if (chunk_is_mmapped (p)) /* release mmapped memory. */{/* See if the dynamic brk/mmap threshold needs adjusting.Dumped fake mmapped chunks do not affect the threshold. */if (!mp_.no_dyn_threshold&& chunksize_nomask (p) > mp_.mmap_threshold&& chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX&& !DUMPED_MAIN_ARENA_CHUNK (p)){mp_.mmap_threshold = chunksize (p);mp_.trim_threshold = 2 * mp_.mmap_threshold;LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,mp_.mmap_threshold, mp_.trim_threshold);}munmap_chunk (p);return;} ar_ptr = arena_for_chunk (p);_int_free (ar_ptr, p, 0); }
看见了吧,和上面讲解的
__libc_malloc
流程几乎类似,先是执行hook
,在hook
指向的实际函数tr_freehook
内部完成记录。然后继续回到__libc_free
接口,此接口内部最终使用munmap_chunk
完成内存释放。
总结起来就是,调用了mtrace
接口后,会为内存申请以及释放接口注册hook
,在实际内存申请和释放的时候,hook会先拦截,完成信息的记录,然后继续原有的流程。
-
muntrace 不跟踪接口
void muntrace (void) {if (mallstream == NULL)return;/* Do the reverse of what done in mtrace: first reset the hooks andMALLSTREAM, and only after that write the trailer and close thefile. */FILE *f = mallstream;mallstream = NULL;__free_hook = tr_old_free_hook;__malloc_hook = tr_old_malloc_hook;__realloc_hook = tr_old_realloc_hook;__memalign_hook = tr_old_memalign_hook;fprintf (f, "= End\n");fclose (f); }
muntrace
接口就是吧mtrace
接口内部的相关的hook
指针给置位为NULL
。相关的头文件位于
#include <mcheck.h>
-
总结,本次从源码角度大致分支了
memleak
和mtrace
定位内存泄漏的方法:memleak
使用的宏代替以及逗号表达式,结合全局的双向循环链表,每申请一次内存,就把信息插入到链表的尾部,每释放一次内存,就从链表中找到对应的节点信息,然后进行节点删除。最后剩余的链表节点就是没有释放的内存。mtrace
则是GLIBC内部的一个hook
拦截机制,当调用mtrace()
接口后,就会将相关的内存操作函数绑定到对应的hook
上,每次申请或者释放会在MALLOC_TRACE
环境变量指定的文件中进行信息记录。最终使用脚本分析,没有成对出现的地址、大小就是没有释放的内存。