SystemTap使用技巧【四】

1、查看内核文件中函数的执行流程

        前段时间研究了一下Linux内核信号处理流程,记录一下用到的技巧吧。

        其实如果不用工具,硬是看代码去分析这个信号处理流程的话,还真的可能搞不定,因为不知道看到的代码是否得到执行,有可能都没有编译进去,所以适当的用工具去分析和调试,真的事半功倍。那信号处理从哪里入手呢,当然从系统调用开始,这就用到SystemTap使用技巧【一】中讲的一个技巧,看看signal和kill系统调用在哪个文件:

root@jusse ~# stap -l 'kernel.function("sys_signal")'
kernel.function("SyS_signal@/build/buildd/linux-lts-trusty-3.13.0/kernel/signal.c:3525")

root@jusse ~# stap -l 'kernel.function("sys_kill")'  
kernel.function("SyS_kill@/build/buildd/linux-lts-trusty-3.13.0/kernel/signal.c:2909")

可见这两个系统调用是在kernel/signal.c里面实现的,定位到文件之后,就可以直接看代码了,但我还是想继续从调试入手,因为想到了SystemTap使用技巧【二】讲到的一个技巧——跟踪进程的执行流程,内核肯定也可以跟踪代码的执行流程,于是就写了下面的SystemTap脚本:

root@jusse ~/systemtap# cat kernel_signal_process.stp 
probe begin {
    printf("begin\n")
}

probe kernel.function("*@/build/buildd/linux-lts-trusty-3.13.0/kernel/signal.c").call {
    if (target() == pid()) {
        printf("%s -> %s\n", thread_indent(4), ppfunc())
    }
}

probe kernel.function("*@/build/buildd/linux-lts-trusty-3.13.0/kernel/signal.c").return {
    if (target() == pid()) {
        printf("%s <- %s\n", thread_indent(-4), ppfunc())
    }
}

这个脚本用到*@,也就是在指定文件中匹配所有函数并打上探测点。那上面这个脚本就比较明了了,就是在signal.c这个文件中所有函数打上call和return两个探测点,call和retrun的时候输出函数名,并利用thread_indent函数增加缩进,这样就可以体现出函数的调用过程了,因为内核处理信号比较频繁,所以上面脚本中就用target()来过滤,只要一个pid的信号处理流程,这样输出比较少才好分析。

先在一个shell中启动SystemTap安装探测点:

root@jusse ~/systemtap# tty
/dev/pts/32

root@jusse ~/systemtap# stap -x 26850 ./kernel_signal_process.stp
WARNING: function signals_init is in blacklisted section: keyword at ./kernel_signal_process.stp:5:1
 source: probe kernel.function("*@/build/buildd/linux-lts-trusty-3.13.0/kernel/signal.c").call {
         ^
WARNING: function setup_print_fatal_signals is in blacklisted section: keyword at :5:1
 source: probe kernel.function("*@/build/buildd/linux-lts-trusty-3.13.0/kernel/signal.c").call {
         ^
begin

接着给一个bash进程发送INT信号:

root@jusse ~# tty
/dev/pts/33

root@jusse ~# ps -ef | grep bash
root      9795  9794  0 Feb10 pts/1    00:00:00 /bin/bash
root     26850 26835  0 21:19 pts/32   00:00:00 -bash
root     25439 25424  0 09:53 pts/33   00:00:00 -bash

root@jusse ~# kill -INT 26850

之后结果如图:


从这个图中就可以清晰看见信号处理的大概流程,再根据这个流程去对照代码就更容易理解了。至于get_signal_to_deliver这个函数在哪里被调用的,可以在这个函数打个探测点,然后把调用堆栈打出来就知道了,这里就不贴码贴图了。


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

        我想内存问题肯定困扰过不少人,调用方法也很多,著名的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

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值