C/C++开发的程序效率很高,但经常会出现内存泄漏。本文提供了一种通过wrapmalloc查找内存泄漏的方法。什么是内存泄漏?动态分配的内存丢失了引用,导致无法回收(我知道在进程退出前系统会统一回收),这就是内存泄漏。Java等编程语言会自动管理内存回收,而C/C++则需要显式释放。避免内存泄漏的方法有很多,比如RAII,比如智能指针(大多基于引用计数),比如内存池。理论上只要我们足够小心,每次申请都记得释放,那么世界就干净了,但现实往往没有那么美好,比如抛出异常,释放内存的语句无法执行,或者菜鸟程序员不小心埋下了地雷,所以我们必须面对现实世界,也就是会遇到内存泄漏。如何检查内存泄漏?我们可以review代码,但是从大量的代码中找出隐藏的问题就像大海捞针,往往是两手空空。所以,我们需要借助工具,比如valgrind,但是这些寻找内存泄漏的工具往往对你使用动态内存的方式有一定的期望或者约束,比如驻留在内存中的对象会被误报,然后才是真正有用的互联网上的信息将被淹没在误报的海洋中。很多时候,即使是valgrind也根本无法解决日常项目中的问题。因此,很多著名的开源项目,为了能够和valgrind一起运行,花费了很大的精力,对源代码进行了大幅度的修改,使项目符合valgrind的要求。为了满足这些要求,一个使用vargrind运行且没有任何警报的项目称为valgrindclean。既然这些东西花哨而无用,与其向自己求,不如向别人求,还是要自力更生。什么是动态内存分配器?动态内存分配器是内核和应用程序之间的函数库。glibc提供的动态内存分配器叫做ptmalloc,也是使用最广泛的动态内存分配器实现。从内核的角度来看,动态内存分配器属于应用层;从应用程序的角度来看,动态内存分配器属于系统层。应用程序可以直接通过mmap系统向内核申请动态内存,也可以通过动态内存分配器的malloc接口分配内存,动态内存分配器会通过sbrk和mmap向内核分配内存,因此释放的内存申请通过free,不一定还给系统,也有可能被动态内存分配器缓存了。Google有自己的动态内存分配器tcmalloc,jemalloc也是一个著名的动态内存分配器。它们具有不同的性能和不同的缓存和分配策略。你可以用它们来代替linux系统中glibc自带的ptmalloc。new/delete和malloc/freeNew的关系是C++的用法,比如Foo*f=newFoo,其实分3步。sizeof(Foo)的内存是通过operatornew()分配的,最后通过malloc分配。在新分配的内存上构造Foo对象。返回新构造的对象的地址。new=分配内存+构造+返回,delete等于销毁+释放。所以得到malloc和free从根本上就是得到动态内存分配。1、一个chunk每次通过malloc返回的一块内存称为一个chunk。动态内存分配器就是这样定义的,后面我们也会这样调用。2.wrapmallocgcc支持wrap,即通过传递-Wl、--wrap、malloc,可以改变调用malloc的行为,调用malloc可以链接到自定义的__wrap_malloc(size_t)函数,我们可以使用在_wrap_malloc(size_t)函数的实现实际上是通过__real_malloc(size_t)来分配内存的,然后我们可以做一些小技巧。同样,我们可以免费包装。malloc和free是配对的。当然还有其他相关的API,比如calloc、realloc、valloc,但这基本就是malloc+free。比如realloc就是malloc+free。如何定位内存泄漏?我们将malloc各种大小的块,也就是说,每种大小的块的数量都不同。如果我们可以跟踪每个大小的块的数量,那么我们就可以知道泄漏中是哪个大小的块。很简单,如果那个大小的块的数量不断增加,它很可能会泄漏。仅仅知道某个大小的块已经泄漏是不够的。我们需要知道是哪个调用路径导致分配了这个大小的chunk,从而检查是否正确释放。如何跟踪每个大小的块数?我们可以维护一个全局的unsignedintmalloc_map[1024*1024]数组,数组下标为chunk的大小,malloc_map[size]的值对应size的chunk分配量。这相当于维护了一张chunksize到chunkcount的映射表,速度够快,而且可以覆盖0到1M大小的chunk范围。它已经足够大了。试想一下,一次分配1兆字节的块是多么可怕。可以覆盖大部分场景。大于1M的块呢?我们可以通过日志记录它们。在__wrap_malloc中,++malloc_map[size]在__wrap_free中,--malloc_map[size]非常简单。我们通过malloc_map记录每个大小的chunk的分配情况。我怎么知道释放的块的大小?不,free(void*p)只有一个参数。我怎么知道释放的块的大小?我应该怎么办?我们在__wrap_malloc(size_t)中分配8+sizechunk,即多分配8个字节,前8个字节存放chunk的大小,然后返回(char*)chunk+8,即偏移8个字节并返回到调用malloc的应用程序。这样,在free的时候,传入参数void*p,我们将p向前移动8个字节,dereferencing可以得到chunk的size,size值为上一步__wrap_malloc时设置的size。嗯,其实我们已经记录了每个大小的chunk的个数,存在于malloc_map[1M]的数组中。假设已经分配了64字节的chunk,并且数量一直在增加,我们认为这个大小的chunk很可能会泄漏,那么如何定位调用是从哪里来的呢?如何记录调用链?我们可以维护一个toplist数组,假设有10个元素,它存储了chunk数量最多的10个尺寸。这个很容易做到,malloc_map取top10即可。然后我们测试size是否是__wrap_malloc(size_t)中的toplist之一。如果是这样,我们通过glibc的回溯将调用堆栈转储到日志文件中。注意:这里不能再分配内存,所以只能用backtrace,不能用backtrace_symbols,所以只能得到调用栈的符号地址,不能得到符号名。如何将符号地址转换为符号名称,即对应代码行?addr2lineaddr2line工具可以做到,可以跟踪调用链,进而定位内存泄漏的问题。到目前为止,您已经掌握了整个核心思想。当然,在实际项目中,我们做的更多。我们不仅记录toplist大小,还记录每个sizechunk的增量toplist,记录malloc/free的largechunk,封装更多的API。总结一下:通过wrapmalloc/free+backtrace+addr2line,可以定位内存泄漏。
