前言

在上一篇文章中我们讲了使用LeakCanary 内存泄漏检测的原理,但是它只能检测 Java 层的泄漏,对 C/C++ 并没有办法。

所以这篇文章来聊一聊如何检测 C/C++ 内存泄漏。

堆内存的分配与释放

我们知道内存泄漏主要发生在堆内存,所以我们重点主要关注堆内存的泄漏。

内存泄漏发生的原因是该释放掉的内存没有释放,也就是只进行了分配,而没有释放。

在代码中就是只调用了 malloc ,没有调用 free

正常情况下,它们应该是成对出现的。所以我们可以根据是否调用 free 来判断是否发生了内存泄漏。

C 中堆内存的分配主要有 malloccallocrealloc ,我们暂时只关注 malloc

那要怎么监控内存的分配和释放呢?

通常有两种做法

  1. hook glibc 里的 malloc 替换成你自己的内存分配函数,在里面进行记录。这样做的好处是不用修改 malloc 调用的代码。
  2. 编写自己的 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 释放了 p1p2 没有释放,所以最后打印出了 p2 没有被释放。

总结

C/C++ 内存泄漏的检测主要是通过 hook 内存的分配和释放,也就是一系列 malloc 函数和 free

在内存分配的时候进行记录,然后在释放的时候清除记录,最后把没有清除掉的打印出来,就是发生了内存泄漏的。