1. 程式人生 > >SystemTap使用技巧【四】

SystemTap使用技巧【四】

1、檢視核心檔案中函式的執行流程

        前段時間研究了一下Linux核心訊號處理流程,記錄一下用到的技巧吧。

        其實如果不用工具,硬是看程式碼去分析這個訊號處理流程的話,還真的可能搞不定,因為不知道看到的程式碼是否得到執行,有可能都沒有編譯進去,所以適當的用工具去分析和除錯,真的事半功倍。那訊號處理從哪裡入手呢,當然從系統呼叫開始,這就用到SystemTap使用技巧【一】中講的一個技巧,看看signal和kill系統呼叫在哪個檔案:

[email protected] ~# stap -l 'kernel.function("sys_signal")'
kernel.function("
[email protected]
/build/buildd/linux-lts-trusty-3.13.0/kernel/signal.c:3525") [email protected] ~# stap -l 'kernel.function("sys_kill")' kernel.function("[email protected]/build/buildd/linux-lts-trusty-3.13.0/kernel/signal.c:2909")

可見這兩個系統呼叫是在kernel/signal.c裡面實現的,定位到檔案之後,就可以直接看程式碼了,但我還是想繼續從除錯入手,因為想到了

SystemTap使用技巧【二】講到的一個技巧——跟蹤程序的執行流程,核心肯定也可以跟蹤程式碼的執行流程,於是就寫了下面的SystemTap指令碼:

[email protected] ~/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安裝探測點:

[email protected] ~/systemtap# tty
/dev/pts/32

[email protected] ~/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訊號:

[email protected] ~# tty
/dev/pts/33

[email protected] ~# 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

[email protected] ~# 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反彙編看一下這幾個地址就可以確定在哪一行了:


雖然地址和行號有一些偏差,但往前一個地址基本就是我們要找的呼叫源,並不太影響我們的分析。

參考: