1.内存泄露介绍
在工作中,动态内存分配是常有的事,而伴随动态内存分配而来的最大的问题就是“内存泄露”。所谓“内存泄露”的意思就是申请了内存,但忘记归还给系统,长此以往,系统的可分配内存越来越少,这种问题一旦出现必然很难查找。原因很简单,程序是人写的,写的人都忘记自己曾经在哪里分配了而没有释放,那系统就更不能随便帮助回收内存了。一旦“内存泄露”发生,特别是放生在一些生命周期较长的程序中,从系统的角度来说,可用内存莫名其妙地越来越少,形象地就比喻系统上好像真的出现了一个洞,内存从这个洞里被“漏掉”不见了。
2.mtrace使用介绍
一旦发现系统有这个“苗头”,当务之急就是要找到代码里哪里忘记归还了动态分配的内存。而“内存分配跟踪(malloctracing)”机制则是帮助检查“内存泄露”的好帮手,本文就来给大家介绍一下这个工具的使用,习惯上这个工具简称为mtrace,下文也直接用mtrace指称这个工具。
mtrace工具的主要思路是在的调用内存分配和释放的函数中装载“钩子(hook)”函数,通过钩子函数打印的日志来帮助分析对内存的使用是否存在问题。对该工具的使用包括两部分内容,一个是要修改源码,装载hook函数,另一个是通过运行修改后的程序,生成特殊的log文件,然后利用mtrace工具分析日志,判断是否存在内存泄露以及定位可能发生内存泄露的代码位置。
mtrace是Glibc的一部分,无须特殊安装。mtrace钩子函数定义如下:
#include <mcheck.h>
void mtrace(void);
void muntrace(void);
其中mtrace()用于开启内存分配跟踪,muntrace()用于取消内存分配跟踪。
具体的做法是mtrace()函数中会为那些和动态内存分配有关的函数(如malloc、realloc、memalign以及free)安装“钩子(hook)”函数,这些hook函数会为记录所有有关内存分配和释放的跟踪信息,而muntrace()则会卸载相应的hook函数。基于这些hook函数生成的调试跟踪信息,就可以分析是否存在“内存泄露”这类问题了。
2.1. 示例代码
示例代码如下:
demo_mtrace_memleak.c
/* gcc demo_mtrace_memleak.c -o demo_mtrace_memleak -g -Wall */
#include<stdio.h>
#include<stdlib.h>
#include<mcheck.h>
int main(int argc, char **argv)
{
mtrace();
char *p = malloc(16);
free(p);
p = malloc(32);
muntrace();
return 0;
}
如上,进行了两次malloc申请内存的操作,但只释放一次,因此会导致“内存泄漏”。看看mtrace是否能的到相同的结论。
2.2. mtrace命令行使用
mtrace机制需要实际运行一下程序,然后才能生成跟踪的日志,但在运行程序前还需要定义并导出一个环境变量MALLOC_TRACE用于记录分析日志,如下所示。
export MALLOC_TRACE=./memleak.log
上述的结果就是告诉mtrace在生成日志信息时,在当前路径下创建一个名为memleak.log的文件,并将日志输出到这个文件中去。
运行程序后,查看memleak.log内容如下:
= Start
@ ./demo_mtrace_memleak:[0x7f6ca1400738] + 0x7fffc93796a0 0x10
@ ./demo_mtrace_memleak:[0x7f6ca1400748] - 0x7fffc93796a0
@ ./demo_mtrace_memleak:[0x7f6ca1400752] + 0x7fffc93796c0 0x20
= End
这个文件的内容有三行“有效”记录(除去第一行=Start和最后一行=End),分别对应源码的malloc->free->malloc操作。
具体格式以第一行@ ./demo_mtrace_memleak:[0x7f6ca1400738] + 0x7fffc93796a0 0x10
为例。./demo_mtrace_memleak
显然指的是运行的可执行程序的名字。[0x7f6ca1400738]
这里的数值是对应代码中第一次调用地址,但注意这是机器码的地址,恰好在编译可执行程序的时候利用-g带上了调试信息,所以完全可以利用addr2line这个工具,反推出源文件的行数。具体做法如下:
# addr2line -f -e ./demo_mtrace_memleak 0x00748
main
/mnt/d/MingruiZhou/tinylab/demo_mtrace_memleak.c:11
的确在第11行。
接着后面的是一个符号+,表明这一行对应的是分配内存,反之-表示是释放。再往后是一个数值0x7fffc93796a0,是malloc()函数分配的内存的首地址。最后是0x10,换算成十进制就是16,正是分配的内存的大小。
了解了具体格式后从三行有效日志中可以得出,因为第一行是分配,其分配的内存首地址是0x7fffc93796a0,而第二行释放的内存的首地址也是0x7fffc93796a0,自然说明是一对,相互抵消,不存在内存泄露。第三行分配的内存首地址是0x7fffc93796c0,后面没有匹配的释放日志,则说明这里出现了“内存泄露”。
在实际工作中的场景代码绝对不会就这么几行的,系统提供了一个叫做mtrace的命令行工具可以帮助完成对日志的分析。
赶紧来试一下。输入如下命令:
# mtrace ./demo_mtrace_memleak $MALLOC_TRACE
Memory not freed:
-----------------
Address Size Caller
0x00007fffc93796c0 0x20 at 0x7f6ca1400752
输出的结果已经告诉了一切。mtrace这个工具需要至少两个参数,一个是生成的可执行程序文件的路径,还有一个是日志文件的路径。
mtrace还能帮助查找“重复释放”问题。示例如下:
/* gcc demo_mtrace_double_free.c -o demo_mtrace_double_free -g -Wall */
#include<stdio.h>
#include<malloc.h>
#include<mcheck.h>
int main(int argc,char **argv)
{
char *s = NULL;
mtrace();
s = malloc(32);
free(s);
free(s); //<--double free
muntrace();
return 0;
}
结果如下:
# gcc demo_mtrace_double_free.c -o demo_mtrace_double_free -g -Wall
#
# export MALLOC_TRACE=./double_free.log
# ./demo_mtrace_double_free
# mtrace ./demo_mtrace_double_free $MALLOC_TRACE
- 0x00007fffd74056a0 Free 4 was never alloc'd 0x7f07ff60075c
No memory leaks.
# addr2line -f -e ./demo_mtrace_double_free 0x75c
main
/mnt/d/MingruiZhou/tinylab/demo_mtrace_double_free.c:19
虽然最终并没有准确找到出错行,但也接近错误行。
email: [email protected]