仙居网站设计/深圳最好seo
内存泄漏
内存泄漏的核心原因是调用分配与释放时没有符合开闭原则。有分配却没有释放,这自然会使得进程的堆内存会越来越少,直到耗尽。
一个内存泄漏检测工具需要能够精准地定位到泄漏是由代码中的哪一行所引起的。
可以直接使用一些现成的工具进行内存泄漏的排查,比如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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>void* __malloc(size_t size, const char* file, int line) {void* p = malloc(size);printf("malloc: [%s, line %d]@%p size=%lu\n", file, line, p, size);return p;
}void __free(void* ptr, const char* file, int line) {printf("free: [%s, line %d]@%p\n", 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
来控制:
#include <unistd.h>
void* __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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>#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=%lu\n", 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>#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
调用的位置。