基本知识
内存泄漏产生原因
在堆上使用malloc/remalloc/calloc分配了内存空间,但是没有使用free释放对应的空间。
内存泄漏危害
计算机的堆内存是一定的,一段时间后,由于只存在内存的分配,不存在释放操作,会导致无法在堆上分配一块合适的内存(即是说产生了内存泄漏),从而导致程序的崩溃。
如何防止内存方式泄漏
(1)事前预防内存泄漏
(2)实现内存泄露组件检测是否有内存泄漏。
针对(1)个人认为,如果不是一个非常有经验而且足够细心的程序员,一般很难写出没有内存泄漏的程序,那么最好的办法,就是实现一个检测内存是否泄漏的组件。
内存泄漏检测组件
实现一个内存泄漏组件主要包括:(1)如何知道内存发生了泄漏。(2)如何定位代码哪一行引起了内存泄漏。
针对(1),我们可以使用链表或者其他数据结构,每次分配内存时,就将分配的内存地址和大小等信息存入链表中,释放时根据内存地址和大小对其相应的节点进行释放,最后在检测链表是否为空来判断是否存在内存泄漏。但是数据结构的缺点是内存发生泄漏时不能明显的展示出来。如果使用文件的方式来表示是否发生了内存泄漏,具体假如使用一个单独的文件夹来存放内存检测组件生成的所有文件,运行程序时先清空文件夹的文件,系统调用一次malloc会生成一个文件,以malloc生成的内存地址为文件名,free时释放malloc对应生成的文件,最后如果文件夹存在文件时,就说明存在内存泄漏(malloc和free不匹配造成的)。
针对(2)可以使用C语言的__FILE__、FUNCTION、__LINE__宏定义或者builtin_return_address()API定位是哪一行引起了内存泄漏。
内存泄漏检测工具
常用内存泄漏检测工具包括:valgrind,mtrace等
代码运行环境
系统:Ubuntu16.04
编译环境:gcc 5.4
为什么这里要提到代码的运行环境,可能存在不同的情况,可能最新的gcc编译器已经不支持重写系统API的情况,可能存在重定义的错误,这时候需要借助hook来解决这种问题。
内存泄漏检测组件的实现
我们主要介绍3种内存泄漏检测组件的实现方式:重写malloc/free函数、宏定义、mtrace的实现方式。
我们的文件名为memleak.c,可运行文件名为memleak
重写malloc、free函数
为了说明重写malloc和free的最终版本中的enable_malloc_hook和enable_free_hook变量的作用,我们先给出一个最简单的代码,然后说明其错误原因。
递归错误代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <malloc.h> #include <mcheck.h> void *malloc(size_t size){ printf("malloc"); return NULL; } void free(void *p){ // printf("free"); } int main(){ void *p1 = malloc(10); void *p2 = malloc(20); free(p1); return 0; }
递归代码分析
如果编译器支持重写系统的malloc和free,那么main函数就会调用我们自已定义的malloc和free,如果不支持,请使用hook来实现,如果不知道hook的朋友,可以看看关于死锁的hook描述。如果不编译,我们很可能就认为会打印出”malloc”,但是实际上结果如下:
我们的malloc和free函数只有一句printf(“malloc”),难道这句会发生”段报错”?我们使用gdb调试,会发现printf()内部会调用malloc函数,malloc被我们重写,这样就导致malloc和printf的相互调用,从而形成递归。
示例代码(单线程)
#define _GNU_SOURCE #include <stdio.h> #include <stdint.h> #include <dlfcn.h> #include <stdlib.h> #include <sys/syscall.h> #include <syslog.h> #include <unistd.h> extern void *__libc_malloc(size_t size); //malloc.h里面定义的 int enable_malloc_hook = 1; //终止递归的变量(具体参考malloc函数说明) extern void __libc_free(void *p); int enable_free_hook = 1; void *malloc(size_t size) { if (enable_malloc_hook) { //调用系统 enable_malloc_hook = 0; void *p = __libc_malloc(size); //分配内存,系统的malloc实际上也是调用的这个api进行内存分配。 //返回malloc调用完成时的地址,可以结合addr2line命令定位到哪一行内存泄露。 void *caller = __builtin_return_address(0); char buff[128] = {0}; sprintf(buff, "./mem/%p.mem", p); //使用malloc返回的地址作为文件名(p.mem) FILE *fp = fopen(buff, "w"); fprintf(fp, "[+%p] --> addr:%p, size:%ld\n", caller, p, size); fflush(fp); //fclose(fp);//注意不能close文件 //printf函数内部会调用malloc,如果不用enable_malloc_hook变量会导致递归malloc的使用 printf("malloc :%p\n", p); //保证下次调用malloc进入到if,注意,多线程不是线程安全的 enable_malloc_hook = 1; return p; } else { //如果是其他API(比如printf)调用了malloc,会直接调用__lib_malloc(size_t size)进行分配,从而使得递归得以退出 return __libc_malloc(size); } } void free(void *p) { if (enable_free_hook) { //调用free enable_free_hook = 0; __libc_free(p); char buff[128] = {0}; sprintf(buff, "./mem/%p.mem", p); if (unlink(buff) < 0) //删除文件,如果返回小于0,说明释放了2次 { printf("double free:%p\n", p); } printf("free:%p\n", p); enable_free_hook = 1; //保证下次free,还走if } else { //其他系统API调用会直接调用__libc_free. __libc_free(p); } } int main() { void *p1 = malloc(10); void *p2 = malloc(20); free(p1); return 0; }
结果分析
运行结果如下:
对上图结果进行说明:首先保证文件夹mem存在。
1.编译c文件(带-g参数):gcc -o memleak memleak.c -g
2.删除mem文件夹里面的文件(如果没有mem文件夹,先创建mem文件夹,保证mem文件夹和memleak.c在同一目录下):rm -rf mem/*
3.运行程序:./memleak
通过运行程序可以发现malloc 2次,free了一次,按照预期,在mem文件夹下应该新产生了0x9a7680.mem的文件,我们使用cat看看里面的内容:[+0x400a29] --> addr:0x9a7680, size:20
4.使用addr2line命令来查看内存泄漏的具体位置:addr2line -f -e memleak -a 0x400a29得到内存泄漏发生在main函数77行位置。
结论:通过代码运行结果,可以检查出具体是哪一行出现了问题,但是可以很容易看出,**代码不是线程安全的,**enable_malloc_hook和enable_free_hook都是全局变量,如果是多线程同时malloc,当一个线程malloc时,会出现enable_malloc_hook起初就为0的情况,此时会走malloc的else,这样就会出现malloc分配了空间,却没添加文件的情况,就会出现”double free”的假象,同样,当多个线程同时free的情况,会出现enable_free_hook起初为0的情况,此时就会导致本来已经释放了空间,但是还存在分配的文件,会被误认为内存分配后没有被释放的情况。当然还有其他的复杂情况,这里就不分析了。
我也考虑过使用锁和条件变量来控制,但是锁造成的死锁(递归造成多次对锁加锁)问题不知道如何解决。如果可以,可以在调用malloc函数的前后对其加锁(可以自己使用一个条件语句,如果是调试模式,则加锁,否则就使用系统的malloc),如果大家有什么好办法可以跟我说说。
其他情况说明:使用__builtin_return_address(LEVEL) API 和addr2line命令结合使用在很多实验环境下都出现了如下的现象(出现了??):
有的环境会出现这种情况,目前我不知道是什么原因。
要注意这里__builtin_return_address()的参数,如果传递0,代表调用malloc函数的直接函数的调用处 ,在我的代码就是main函数,如果假如你代码结构是main->func->malloc,这时候如果传递1,就会找到main函数调用func函数说明出现了内存泄漏。
宏定义(推荐)
代码实现
为了防止递归条件的发生,使用了enable_malloc_hook和enable_free_hook变量来作为退出递归的条件,具体的使用参考如下代码:
#define _GNU_SOURCE #include <stdio.h> #include <stdint.h> #include <dlfcn.h> #include <stdlib.h> #include <sys/syscall.h> #include <syslog.h> #include <unistd.h> void *malloc_hook(size_t size, const char *file, const char *func, int line) { void *p = malloc(size); char str[256] = {0}; sprintf(str, "./mem/%p.mem", p); FILE *fp = fopen(str, "w"); fprintf(fp, "[info:]file:%s,func:%s,line:%d, addr:%p,size:%ld\n", file, func, line, p, size); fflush(fp); fclose(fp); return p; } void free_hook(void *p, const char *file, const char *func, int line) { free(p); char str[256] = {0}; sprintf(str, "./mem/%p.mem", p); if (unlink(str) < 0) { printf("double free.\n"); } } //宏定义要定义在函数的下面,因为malloc_hook里面使用了malloc //如果宏定义在函数的前面,会导致递归 #define malloc(size) malloc_hook(size, __FILE__, __FUNCTION__, __LINE__) #define free(p) free_hook(p, __FILE__, __FUNCTION__, __LINE__); #endif int main() { void *p1 = malloc(10); void *p2 = malloc(20); free(p1); return 0; }
实验结果
通过宏定义和__FILE__,LINE,__FUNCTION__可以很容易知道内存泄漏的具体位置,比如该结果为main函数的99行出现了内存泄漏问题。
hook方式(mtrace方式)
完整代码
#define _GNU_SOURCE #include <stdio.h> #include <stdint.h> #include <dlfcn.h> #include <stdlib.h> #include <sys/syscall.h> #include <syslog.h> #include <unistd.h> typedef void *(*malloc_hook_t)(size_t size, const void *caller); //系统的__malloc_hook实际的函数类型 malloc_hook_t malloc_f; //用于保存系统默认的__malloc_hook函数地址 typedef void (*free_hook_t)(void *p, const void *caller); //系统的__free_hook的实际函数类型 free_hook_t free_f; //用于保存系统默认的__free_hook函数指针地址 int replaced = 0; //如果为1,malloc/free指向我们自定义的函数 void mem_trace(void); //让其malloc指向我们自己定义的函数 void mem_untrace(void); //让其free指向我们自己定义的函数 //自定义的malloc函数,与系统的__malloc_hook保持一致 //caller参数代表调用该函数的地址(__builtin_return_address(0)返回的地址就是这个地址) void *malloc_hook_f(size_t size, const void *caller) { //防止递归-如果不加这句,会让下面的malloc继续执行malloc_hook_f,从而造成递归 //我们只要得到caller指针这个值就可以了。 mem_untrace(); void *ptr = malloc(size); //printf("+%p: addr[%p]\n", caller, ptr); char buff[128] = {0}; sprintf(buff, "./mem/%p.mem", ptr); FILE *fp = fopen(buff, "w"); fprintf(fp, "[+%p] --> addr:%p, size:%ld\n", caller, ptr, size); fflush(fp); fclose(fp); //free mem_trace(); //保证下次malloc还是用我们自定义的 return ptr; } void free_hook_f(void *p, const void *caller) { mem_untrace(); //防止free函数递归 //printf("-%p: addr[%p]\n", caller, p); char buff[128] = {0}; sprintf(buff, "./mem/%p.mem", p); if (unlink(buff) < 0) { // no exist printf("double free: %p\n", p); return; } free(p); mem_trace(); } void mem_trace(void) { //mtrace replaced = 1; malloc_f = __malloc_hook; //__malloc_hook是系统本身提供的函数指针(会在malloc调用时初始化) free_f = __free_hook; //__free_hook是系统本身提供的(free调用时会初始化) __malloc_hook = malloc_hook_f; //指向我们自定义函数,malloc会调用我们定义的函数 __free_hook = free_hook_f; //指向我们自定义函数,free会调用我们自定义的函数 } // void mem_untrace(void) { __malloc_hook = malloc_f; __free_hook = free_f; replaced = 0; } int main() { mem_trace(); void *p1 = malloc(10); void *p2 = malloc(20); free(p1); mem_untrace(); return 0; }
实验结果
实验结果得到代码189行出分配了内存,没有进行内存释放,其操作见2.2.3章节。
实现方式比较
个人比较推荐宏定义的方式来实现内存泄漏组件的实现方式,实现方式最简单,而且应该是支持多线程的,malloc_hook里面的系统本身的malloc/free函数是线程安全的,也就是进入到malloc_hook和free_hook函数会被阻塞,当然这是我的个人理解,不知道是否正确?
mtrace工具使用
产生日志文件
命令:
(1)生成mtrace的环境变量MALLOC_TRACE(用于指定mtrace的日志路径)
export MALLOC_TRACE=./test.log
(2)删除已有的日志文件(可选,如果存在日志文件的话,建议提前删除日志文件)
rm -rf test.log
代码
#define _GNU_SOURCE #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <malloc.h> #include <mcheck.h> int main() { mtrace(); void *p1 = malloc(10); void *p2 = malloc(20); // free(p1); muntrace(); return 0; }
实验结果
文档说明
本文使用了3种方式来实现内存泄漏检测组件,可以用来检测自己和第三方库的内存泄漏(我们在代码中使用了第3方库的地方,如果调用了malloc/free,那么实际上也是调用了我们这里的malloc/free)情况,另外remalloc/calloc的代码可以自己一样去实现。
代码来源于腾讯课堂-零声学院king老师
ps:如果有什么不懂的可以直接给我留言。