systemtap调试内存泄漏以及内存重复释放

我想内存问题肯定困扰过不少人,调用方法也很多,著名的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反汇编看一下这几个地址就可以确定在哪一行了:

虽然地址和行号有一些偏差,但往前一个地址基本就是我们要找的调用源,并不太影响我们的分析。

参考:

systemtap如何跟踪libc.so

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 -

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值