我想内存问题肯定困扰过不少人,调用方法也很多,著名的valgrind、efence、mudflap在一定程度上也能帮助我们解决不少问题,但一些情况下它们也无能无力,比如多进程模型上valgrind好像支持得不是很好,efence和mudflap在大型项目中特别是用了其他第三方库的情况下,可能就早早的发现其他库的一些不是问题的问题就退出了,在一些小项目中用还是可以的。那我这里讲的这个技巧就是用SystemTap来查内存泄漏和内存重复释放问题,其原理就是给malloc和free打上探测点,分别计数,最后看看调用malloc和free是不是达到平衡,如果调用malloc多free少,那就可能存在内存泄漏,如果malloc少free多那就可能出现内存重复释放。具体看码吧:
/*文件名:cc_mem_test.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
char *p1;
char *p2;
char *p3;
char *p4;
sleep(20);//让程序sleep 20s是因为我们程序先起来之后,等待SystemTap启动设置探测点
p1 = malloc(500);
p2 = malloc(200);
p3 = malloc(300);
p4 = malloc(300);//泄漏
free(p1);
free(p2);
free(p3);
free(p2);//重复释放
printf("p1: %p, p2: %p, p3: %p, p4: %p\n", p1, p2, p3, p4);
return 0;
}
上面代码是一个模拟内存泄漏和内存重复释放的例子,其中p2重复释放,p4没有释放产生泄漏(这个只是例子,因为这个程序运行一下就退出了,malloc的内存即使不释放内核也会帮我们释放的)。
mem.stp:
probe begin {
printf("=============begin============\n")
}
//记录内存分配和释放的计数关联数组
global g_mem_ref_tbl
//记录内存分配和释放的调用堆栈关联数组
global g_mem_bt_tbl
probe process("/lib/x86_64-linux-gnu/libc.so.6").function("__libc_malloc").return, process("/lib/x86_64-linux-gnu/libc.so.6").function("__libc_calloc").return {
if (target() == pid()) {
if (g_mem_ref_tbl[$return] == 0) {
g_mem_ref_tbl[$return]++
g_mem_bt_tbl[$return] = sprint_ubacktrace()
}
}
}
probe process("/lib/x86_64-linux-gnu/libc.so.6").function("__libc_free").call {
if (target() == pid()) {
g_mem_ref_tbl[$mem]--
if (g_mem_ref_tbl[$mem] == 0) {
if ($mem != 0) {
//记录上次释放的调用堆栈
g_mem_bt_tbl[$mem] = sprint_ubacktrace()
}
} else if (g_mem_ref_tbl[$mem] < 0 && $mem != 0) {
//如果调用free已经失衡,那就出现了重复释放内存的问题,这里输出当前调用堆栈,以及这个地址上次释放的调用堆栈
printf("MMMMMMMMMMMMMMMMMMMMMMMMMMMM\n")
printf("g_mem_ref_tbl[%p]: %d\n", $mem, g_mem_ref_tbl[$mem])
print_ubacktrace()
printf("last free backtrace:\n%s\n", g_mem_bt_tbl[$mem])
printf("WWWWWWWWWWWWWWWWWWWWWWWWWWWW\n")
}
}
}
probe end {
//最后输出产生泄漏的内存是在哪里分配的
printf("=============end============\n")
foreach(mem in g_mem_ref_tbl) {
if (g_mem_ref_tbl[mem] > 0) {
printf("%s\n", g_mem_bt_tbl[mem])
}
}
}
首先用两个关联数组全局变量来分别保存内存分配/释放的计数和调用堆栈,在__libc_malloc和__libc_calloc(其实也可以是malloc和calloc)设置return探测点,因为在return的时候就可以通过SystemTap变量$return得到分配的内存地址,并在关联数组g_mem_ref_tbl中以内存地址为key,计数加一。在__libc_free(也可以用free)设置call探测点,__libc_free函数原型是void __libc_free(void *mem);,在call探测点可以通过$mem参数来得到内存地址,然后在关联数组g_mem_ref_tbl中将$mem的计数减一,如果发现计数小于0,那就可以知道有重复释放的问题了,上面的脚本中,当发现重复释放时,就把当前的调用堆栈以及上次释放的调用堆栈打印出来了,这样就很方面定位是在哪里重复释放了,其中保存调用堆栈就用SystemTap的接口sprint_ubacktrace。看一下这个例子的结果:
可见,红框中0x400655和0x40063d这两个frame就是重复free的地址,黄框0x400621就是产生泄漏的内存分配地址,然后再用addr2line或者objdump反汇编看一下这几个地址就可以确定在哪一行了:
虽然地址和行号有一些偏差,但往前一个地址基本就是我们要找的调用源,并不太影响我们的分析。
参考:
stap命令选项
-v 将输出更具体,可以执行stap -vvv script.stp
-o 将标准输出写入文件
-S size,count 分别限制文件大小M和数量
-x pid #使用target()捕获指定的进程ID ("-x procss_id" 指定探测进程ID为'process_id‘的程序)
-c 'command' 将target()指向该命令
-e 'script' 将脚本作为输入传输给systemtap translator,即直接执行-e后的脚本
-F 使用flight recorder mode将脚本后台运行,该脚本允许stap脚本长时间运行,有两种方式存储输出:内存和文件模式,两者都是后台进程;
也可从标准输入读取并运行脚本 echo "probe timer.s(1) {exit()}" | stap -