当前位置: 首页 > news >正文

mtrace和memleak源码分析

文章目录

    • 1. 内存泄漏
    • 2. 定位工具
      • 2.1 memleak 工具定位内存泄漏
        • 2.1.1 源码解读
      • 2.2 mtrace 工具定位内存泄漏
        • 2.2.1 源码解读

嵌入式内存泄漏定位工具mtrace和memleak源码分析

1. 内存泄漏

内存泄漏(Memory Leak)指程序中已动态分配的堆内存因未正确释放或无法释放,导致系统内存资源被持续占用且无法回收的现象。其核心特征是隐蔽性积累性,可能不会立即引发程序崩溃,但会随着时间推移逐渐消耗系统资源,最终引发性能下降或系统崩溃。

内存泄漏影响如下:

  • 性能下降
    • 内存资源耗尽:内存泄漏会持续占用可用内存,导致程序可用内存减少,频繁触发垃圾回收(GC),增加CPU负担;
    • 内存碎片化:泄漏的内存块可能分散在堆中,导致可用内存被分割为碎片,降低内存分配效率;
  • 系统稳定性受损
    • 程序崩溃:当内存泄漏累积到系统可用内存极限时,可能引发内存溢出(OOM),导致程序强制终止;
    • 资源竞争加剧:泄漏的内存可能影响其他进程或线程的正常运行,导致系统整体响应延迟;

2. 定位工具

内存泄漏,直接的表现就是 free指令会显示剩余内存越来越少,少到一定程度先是会触发SWAP交换,如果SWAP交换后还是内存不足,就会触发OOM现象。

常见的定位工具有vargrindmemleakmtrace,其中嵌入式设备因为内存限制,使用最普遍的是memleakmtrace工具。

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库的标准函数mallocrealloc callocfree都进行了宏替换,而且还使用了一个逗号表达式来将当前fileline进行赋值,便于后面对链表节点的赋值。

  • 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函数
    freetr_freehook
    malloctr_mallochook
    calloctr_mallochook
    realloctr_reallochook
    memaligntr_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)));   
    

    上面的宏定义会使得namealiasname完全等价,也就是我们的malloc等价于__libc_mallocfree等价于__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>

  • 总结,本次从源码角度大致分支了 memleakmtrace定位内存泄漏的方法:

    • memleak使用的宏代替以及逗号表达式,结合全局的双向循环链表,每申请一次内存,就把信息插入到链表的尾部,每释放一次内存,就从链表中找到对应的节点信息,然后进行节点删除。最后剩余的链表节点就是没有释放的内存。
    • mtrace则是GLIBC内部的一个hook拦截机制,当调用mtrace()接口后,就会将相关的内存操作函数绑定到对应的hook上,每次申请或者释放会在MALLOC_TRACE环境变量指定的文件中进行信息记录。最终使用脚本分析,没有成对出现的地址、大小就是没有释放的内存。

相关文章:

  • 从困局到破局的AI+数据分析
  • 【机器学习】​碳化硅器件剩余使用寿命稀疏数据深度学习预测
  • UE 滚动提示条材质制作
  • 民锋视角下的价格风险管理策略
  • 0805登录_注册_token_用户信息_退出-网络ajax请求2-react-仿低代码平台项目
  • 八大排序——快速排序/快排优化
  • 【javascript】竞速游戏前端优化:高频操作与并发请求的解决方案
  • jaffree 封装ffmpeg 转换视频格式,获取大小,时间,封面
  • 汤晓鸥:计算机视觉的开拓者与AI产业化的先行者
  • python数据分析(五):Pandas 数据检索技术
  • Android学习总结之Java篇(一)
  • 关于https请求丢字符串导致收到报文解密失败问题
  • java.lang.AssertionError: Binder ProxyMap has too many entries: 问题处理
  • 深入理解链表:从基础操作到高频面试题解析
  • Linux[开发工具]
  • 主流AI推理模型的详细说明、对比及总结表格
  • android录音生成wav
  • 铭记之日(3)——4.28
  • 【软件工程】需求分析详解
  • maven私服配置
  • 伊朗港口爆炸死亡人数升至70人
  • 新经济与法|如何治理网购刷单与控评?数据合规管理是关键
  • 马上评丨学生举报食堂饭菜有蛆,教育局应该护谁的犊子
  • 新任浙江省委常委、杭州市委书记刘非开展循迹溯源学习调研
  • 人民日报:广东全力推动外贸稳量提质
  • 读科学发展的壮丽史诗,也读普通人的传奇