前言
在上一篇文章中我们讲了使用LeakCanary 内存泄漏检测的原理,但是它只能检测 Java
层的泄漏,对 C/C++
并没有办法。
所以这篇文章来聊一聊如何检测 C/C++
内存泄漏。
堆内存的分配与释放
我们知道内存泄漏主要发生在堆内存,所以我们重点主要关注堆内存的泄漏。
内存泄漏发生的原因是该释放掉的内存没有释放,也就是只进行了分配,而没有释放。
在代码中就是只调用了 malloc
,没有调用 free
。
正常情况下,它们应该是成对出现的。所以我们可以根据是否调用 free
来判断是否发生了内存泄漏。
在 C
中堆内存的分配主要有 malloc
、 calloc
、 realloc
,我们暂时只关注 malloc
。
那要怎么监控内存的分配和释放呢?
通常有两种做法
- hook glibc 里的
malloc
替换成你自己的内存分配函数,在里面进行记录。这样做的好处是不用修改 malloc
调用的代码。
- 编写自己的
malloc
函数,在里面记录内存分配,这种做法需要替换代码中使用 malloc
的地方,而且移植性差,但是简单。可以通过宏来实现。本文使用这种方式实现。
数据结构
在 hook
之前我们需要定义一下要保存的数据结构,用来在程序最后去检测还有哪些内存没有被释放。
我们需要记录分配的地址、分配的大小(用于提示泄漏了多大),发生泄漏代码所在文件和函数,结构如下
1
2
3
4
5
6
|
typedef struct {
void *p; // 记录内存分配的地址,如果为 NULL 表示已经释放了,不为 NULL 就发生了泄漏
int size; // 分配了多大空间
const char *file; // 内存分配所在的文件
int line; // 内存分配所在的行数
} Mem;
|
然后我们还要创建一个数组来保存这些记录
1
2
|
#define SIZE 100
static Mem memorys[SIZE];
|
hook malloc
接下来我们要对 malloc
进行 hook
,我们通过一个 MALLOC
宏函数来实现,它会记录到上面的数据结构中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#define MALLOC(n) mallocHook(n, __FILE__, __LINE__)
void *mallocHook(size_t size, const char *file, const int line) {
void *ret = malloc(size);
if (ret != NULL) {
for (int i = 0; i < SIZE; ++i) {
if (memorys[i].p == NULL) {
memorys[i].p = ret;
memorys[i].size = size;
memorys[i].file = file;
memorys[i].line = line;
break;
}
}
}
return ret;
}
|
这里实现比较简单,我们使用 MALLOC
替换成 mallocHook
,里面主要调用了 malloc
进行内存分配,然后把地址、大小、文件和函数给记录下来。
hook free
讲完了内存分配,来聊一聊内存的释放,释放也是通过 FREE
这个宏来实现的。
在释放的代码中,我们从记录的数组中找出地址相同的一个,然后把内容清空掉,表示这个被释放了,然后调用 free
来释放内存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#define FREE(p) freeHook(p)
void freeHook(void *p) {
if (p != NULL) {
for (int i = 0; i < SIZE; ++i) {
if (memorys[i].p == p) {
memorys[i].p = NULL;
memorys[i].size = 0;
memorys[i].file = NULL;
memorys[i].line = 0;
free(p);
break;
}
}
}
}
|
打印内存泄漏
在程序结束后,我们需要检测我们记录的数组中还有哪些没有释放的,这些就是发生泄漏的,我们需要把它打印出来,这样就可以知道哪里发生了泄漏
1
2
3
4
5
6
7
8
|
void printLeak() {
for (int i = 0; i < SIZE; ++i) {
if (memorys[i].p != NULL) {
printf("addr: %p, size: %d, Location: %s:%d\n", memorys[i].p,
memorys[i].size, memorys[i].file, memorys[i].line);
}
}
}
|
实现比较简单,就是遍历数组,找到地址不为 NULL
的,然后打印出来。
完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
#include<stdio.h>
#include<stdlib.h>
#define SIZE 100
#define MALLOC(n) mallocHook(n, __FILE__, __LINE__)
#define FREE(p) freeHook(p)
typedef struct {
void* p;
int size;
const char* file;
int line;
} Mem;
static Mem memorys[SIZE];
void* mallocHook(size_t size, const char* file,const int line) {
void* ret = malloc(size);
if (ret != NULL) {
for (int i = 0; i < SIZE; ++i) {
if (memorys[i].p == NULL) {
memorys[i].p = ret;
memorys[i].size = size;
memorys[i].file = file;
memorys[i].line = line;
break;
}
}
}
return ret;
}
void freeHook(void* p) {
if (p != NULL) {
for (int i = 0; i < SIZE; ++i) {
if (memorys[i].p == p) {
memorys[i].p = NULL;
memorys[i].size = 0;
memorys[i].file = NULL;
memorys[i].line = 0;
free(p);
break;
}
}
}
}
void printLeak() {
for (int i = 0; i < SIZE; ++i) {
if (memorys[i].p != NULL) {
printf("addr: %p, size: %d, Location: %s:%d\n", memorys[i].p, memorys[i].size, memorys[i].file, memorys[i].line);
}
}
}
int main(int argc, char *argv[])
{
int* p1 = (int*)MALLOC(sizeof(int));
int* p2 = (int*)MALLOC(sizeof(int));
FREE(p1);
printLeak();
return 0;
}
|
执行结果如下
1
|
addr: 0x6000006e4050, size: 4, Location: main.c:59
|
可以看到我们调用 MALLOC
分配了两个内存,然后用 FREE
释放了 p1
, p2
没有释放,所以最后打印出了 p2
没有被释放。
总结
C/C++
内存泄漏的检测主要是通过 hook
内存的分配和释放,也就是一系列 malloc
函数和 free
。
在内存分配的时候进行记录,然后在释放的时候清除记录,最后把没有清除掉的打印出来,就是发生了内存泄漏的。