内存泄漏的核心原因是调用分配与释放时没有符合开闭原则。有分配却没有释放,这自然会使得进程的堆内存会越来越少,直到耗尽。
一个内存泄漏检测工具需要能够精准地定位到泄漏是由代码中的哪一行所引起的。
可以直接使用一些现成的工具进行内存泄漏的排查,比如valgrind、mtrace、ASAN等等。但这些工具往往不适用于在生成环境中实时地检测内存泄漏,而多是用于在发现内存泄漏后进行测试。特别是valgrind和ASAN对性能的影响也比较大,而且不适用于嵌入式系统。
那么,是否可以自己写一些小工具,对malloc和free做一些简单的跟踪,然后可以随时比较直观地查看是否有内存泄漏呢?
跟踪malloc和free显然,要检测内存泄漏最基本的思路就是对malloc和free的调用情况进行记录,调用malloc返回一个地址时,对该地址增加一条记录,然后对该地址调用free时,则撤销该记录。最后程序正常运行结束后,还保留下来的那些记录就是内存泄漏的证据了。
首先想到的就是加hook。那么为什么不能像死锁检测组件那样,用dlsym直接对malloc和free加钩子呢?
大量的函数如printf等等本身内部存在调用malloc,在我们重写malloc时难以避免会使用这些函数,这可能引起递归调用:
malloc() ... sprintf() ... malloc() ....使用宏定义替换
那怎么办呢?可以考虑用宏定义去替换malloc,比如这样:
void* __malloc(size_t size, const char* file, int line) { malloc(size); } void __free(void* ptr, const char* file, int line) { free(ptr); } #define malloc(size) __malloc(size, __FILE__, __LINE__) #define free(ptr) __free(ptr, __FILE__, __LINE__)
这样一来在该宏定义之后,调用malloc和free时实际就是调用__malloc和__free了。并且因为这两个函数都传入了文件名和调用位置,因此可以很方便地查看出是在哪些位置调用了某个malloc。
修改后的测试代码如下:
#include#include #include void* __malloc(size_t size, const char* file, int line) { void* p = malloc(size); printf("malloc: [%s, line %d]@%p size=%lun", file, line, p, size); return p; } void __free(void* ptr, const char* file, int line) { printf("free: [%s, line %d]@%pn", file, line, ptr); free(ptr); } #define malloc(size) __malloc(size, __FILE__, __LINE__) #define free(ptr) __free(ptr, __FILE__, __LINE__) int main() { char* s1 = (char*)malloc(10); char* s2 = (char*)malloc(20); free(s1); free(s2); return 0; }
打印输出:
malloc: [memleak_detector.c, line 24]@0x555555756260 size=10 malloc: [memleak_detector.c, line 25]@0x555555756690 size=20 free: [memleak_detector.c, line 27]@0x555555756260 free: [memleak_detector.c, line 28]@0x555555756690
可以正常打印出malloc调用的位置,非常直观。但这种方法有一点不太好,就是每一个需要替换malloc的源文件都要单独用宏进行替换。可以将其封装成头文件memleak_detector.h,然后通过一个宏__MALLOC_DEBUG来控制:
#includevoid* __malloc(size_t size, const char* file, int line); void __free(void* ptr, const char* file, int line); #ifdef __MALLOC_DEBUG #define malloc(size) __malloc(size, __FILE__, __LINE__) #define free(ptr) __free(ptr, __FILE__, __LINE__) #endif
在源文件中包含该头文件:
#include#include #include #define __MALLOC_DEBUG // 放在所有头文件之后 #include "memleak_detector.h" int main() { ...... }
现在我们都能够成功地“截获”了malloc和free的调用,接下来就看如何把这些记录下来了。
记录内存泄漏时的malloc位置光光通过看打印的方式来查是否存在内存泄漏显然很不方便。因此我们需要将其记录下来,以便程序长时间运行后进行排查。
如果只是临时记录,那么有很多方法,比如我们可以将malloc的记录保存到链表、哈希表等等结构,然后在free时删除对应记录,那么最终留下来的记录就表明出现了内存泄漏。但是这些数据是保存在内存中的,一旦进程结束,就消失了。
我们不妨采用文件来记录,用malloc申请到的地址给文件命名,一次malloc就产生一个文件,其中记录了我们前面的打印信息,然后对应地在free时删除这个文件。这样一来,即使程序终止了,文件也会保存下来,就可以很方便地查看了。
于是修改__malloc和__free如下:
void* __malloc(size_t size, const char* file, int line) { void* p = malloc(size); if(p) { // 首先确保成功申请到了内存 char buf[256] = {0}; char filename[64] = {0}; snprintf(filename, 63, "mem/%p.mem", p); int len = snprintf(buf, 255, "[%s, line %d] size=%lun", file, line, size); int fd = open(filename, O_RDWR | O_CREAT, S_IRUSR); if(fd < 0) { printf("open %s failed in __malloc.n", filename); } else { write(fd, buf, len); // 写入到文件 close(fd); } } return p; } void __free(void* ptr, const char* file, int line) { char filename[64] = {0}; snprintf(filename, 63, "mem/%p.mem", ptr); if(unlink(filename) < 0) { // 使用 unlink 来删除文件 printf("[%s, line %d] %p double free!n", file, line, ptr); } free(ptr); }测试
我们在代码中为s1和s2申请内存,但不释放s2的内存:
#include#include #include #define __MALLOC_DEBUG // 放在所有头文件之后 #include "memleak_detector.h" int main() { char* s1 = (char*)malloc(10); char* s2 = (char*)malloc(20); free(s1); //free(s2); return 0; }
在运行之前,需在程序的运行目录下创建mem目录,运行后mem目录下产生了一个文件0x555555757280.mem,使用cat查看其内容:
[memleak_demo.c, line 11] size=20
可以正确地查看到malloc调用的位置。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)