使用 mtrace 分析 “内存泄漏”

发布于:2022-12-21 ⋅ 阅读:(443) ⋅ 点赞:(0)

1 内存泄漏导论

在工作中,特别是采用 C 语言编写程序时,动态内存分配是常有的事,而伴随动态内存分配而来的最大的问题就是所谓 “内存泄漏”。所谓 “内存泄漏” 的意思就是我们申请了内存,但忘记归还给系统,长此以往,系统的可分配内存越来越少,这种问题一旦出现必然很难查找,原因很简单,程序是人写的,写的人都忘记自己曾经在哪里分配了而没有释放,那系统就更不能随便帮助我们回收内存了。一旦 “内存泄漏” 发生,特别是放生在一些生命周期较长的程序中(譬如后台服务这样的),从系统的角度来说,可用内存莫名其妙地越来越少,形象地我们就比喻系统上好像真的出现了一个洞,安装的内存从这个洞里被 “漏掉” 不见了。

2 mtrace 使用介绍

一旦发现系统有这个 “苗头”,当务之急就是要找到代码里哪里忘记归还了动态分配的内存。 而 “内存分配跟踪(malloc tracing)” 机制则是帮助我们检查 “内存泄漏” 的好帮手,本文就来给大家介绍一下这个工具的使用,习惯上这个工具我们简称为 mtrace,下文也直接用 mtrace 指称这个工具。

mtrace 工具的主要思路是在我们的调用内存分配和释放的函数中装载 “钩子(hook)” 函数,通过 “钩子(hook)” 函数打印的日志来帮助我们分析对内存的使用是否存在问题。对该工具的使用包括两部分内容,一个是要修改源码,装载 hook 函数,另一个是通过运行修改后的程序,生成特殊的 log 文件,然后利用 mtrace 工具分析日志,判断是否存在内存泄漏以及定位可能发生内存泄漏的代码位置。

下面我们通过一个简单的例子,看一下如何利用 mtrace 机制分析 “内存泄漏” 问题。mtrace 这个工具本身是 Glibc 的一部分,所以一般情况下大家的机器上都会有,无须特殊安装,本文演示的环境是 Ubuntu 16.04.6 LTS

2.1 修改源码,装载 “钩子” 函数

我们首先需要改动一下我们的源码。添加以下两个辅助函数:

#include <mcheck.h>
void mtrace(void);
void muntrace(void);

函数的具体介绍参考 man 3 mtrace。其中 mtrace() 用于开启内存分配跟踪,muntrace() 用于取消内存分配跟踪。具体的做法是 mtrace() 函数中会为那些和动态内存分配有关的函数(譬如 malloc()、realloc()、memalign() 以及 free())安装 “钩子(hook)” 函数,这些 hook 函数会为我们记录所有有关内存分配和释放的跟踪信息,而 muntrace() 则会卸载相应的 hook 函数。基于这些 hook 函数生成的调试跟踪信息,我们就可以分析是否存在 “内存泄漏” 这类问题了。

这里演示用的源码文件 test_memleak.c 如下所示。


     1  #include <stdlib.h>
     2  #include <stdio.h>
     3  #include <mcheck.h>
     4
     5  int main(int argc, char **argv)
     6  {
     7          mtrace();
     8
     9          char *p = malloc(16);
    10
    11          free(p);
    12
    13          p = malloc(32);
    14
    15          muntrace();
    16
    17          return 0;
    18  }

其中我们希望调试的代码段是第 9 行到第 13 行,第 9 行调用 malloc() 申请了 16 个字节的内存,第 11 行调用 free() 函数释放了第 9 行分配的内存,第 13 行又调用 malloc() 申请了 32 个字节的内存。很显然,这段代码的第 13 行存在问题,由于第 13 行分配的内存没有被释放掉,会引起 “内存泄漏”。以上是我们人工阅读代码后的分析结果,现在我们来看看如何利用 mtrace 机制帮助我们得到相同的结论。

首先我们需要用 mtrace()/muntrace() 这一对函数将我们关系的代码段括起来,所以我们在第 7 行添加了 mtrace() 函数,第 15 行添加了 muntrace() 函数。另外不要忘记包含 mcheck.h,这个可以参见上面代码的第 3 行。

然后就可以直接编译链接,生成可执行程序:

$ gcc -g test_memleak.c -o a.out

注意这里不要忘记加上 -g 参数,这个很重要,因为后面我们需要调试信息帮助我们定位出问题的代码行数。

2.2 生成日志文件并分析定位问题

mtrace 机制需要我们实际运行一下程序,然后才能生成跟踪的日志,但在实际运行程序之前还有一件要做的事情是需要告诉 mtrace (即前文提到的 hook 函数)生成日志文件的路径。具体的方法是通过定义并导出一个环境变量 MALLOC_TRACE,如下所示。

$ export MALLOC_TRACE=./test.log

上述的结果就是告诉 mtrace 在生成日志信息时,在当前路径下创建一个名为 test.log 的文件,并将日志输出到这个文件中去。

然后就可以直接运行程序了。

$ ./a.out

运行结束后,我们可以发现当前路径下果然生成了一个 test.log 文件。

$ ls

a.out test_memleak.c test.log

系统提供了一个叫做 mtrace 的命令行工具可以帮助我们完成对日志的分析。

$ mtrace ./a.out $MALLOC_TRACE
Memory not freed:
-----------------
           Address     Size     Caller
0x0000000000852470     0x20  at /home/u/samples/test_memleak.c:13

输出的结果已经告诉我们了一切。mtrace 这个工具需要至少两个参数,一个是我们生成的可执行程序文件的路径,还有一个是日志文件的路径。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到