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裡面實現的,定位到檔案之後,就可以直接看程式碼了,但我還是想繼續從除錯入手,因為想到了
[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
之後結果如圖:
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反彙編看一下這幾個地址就可以確定在哪一行了:
雖然地址和行號有一些偏差,但往前一個地址基本就是我們要找的呼叫源,並不太影響我們的分析。
參考: