用 SystemTap 追踪 user space 程式執行的流程
前幾篇文章 (例一、例二) 介紹用 SystemTap 追踪 kernel 內的狀態,其實用來協助了解 user space 的程式,也是相當地好用。
顯示執行的行數
除錯的時候,需要快速地掌握相關的程式碼,然後深入閱讀細節。通常可以用 gdb 設中斷點或塞入程式 log 看看執行到那些地方。視情況的複雜度,有時用 SystemTap 作更有效率。
範例程式:
$ cat conditions.cpp -n
1 #include <iostream>
2 #include <cstdlib>
3
4 struct Foo {
5 int Do(int n) {
6 if (n % 3 == 0) {
7 if (n % 2 == 0) {
8 n += 1;
9 } else {
10 n += 2;
11 }
12 } else if (n % 3 == 1) {
13 if (n % 2 == 0) {
14 n += 3;
15 } else {
16 n += 4;
17 }
18 } else {
19 if (n % 2 == 0) {
20 n += 5;
21 } else {
22 n += 6;
23 }
24 }
25 return n;
26 }
27 };
28
29 int main(int argc, char** argv) {
30 int n = std::atoi(argv[1]);
31 Foo foo;
32 std::cout << foo.Do(n) << std::endl;
33 return 0;
34 }
$ ./conditions 7
11
想像 Foo::Do() 內每個 if/else 內的程式碼有很多行,可能還會呼叫其它函式。若我們想知道 n = 7 的時候會執行到那些區塊 (這個簡化的程式各區塊只有一行),可以用以下的 script:
$ cat flow.stp
probe begin {
printf("ready\n");
}probe process("./conditions").statement("Do@conditions.cpp:*") {
printf("%s\n", pp());
}
- process(“…”) 用來偵測執行檔或 shared library。
- statement(“Do@conditions.cpp:*”) 表示偵測在 conditions.cpp 內函式 Do() 內所有的 statements。
- pp() 回傳目前偵測的位置。
編譯 conditions.cpp 時加上 -g
,然後執行 flow.stp、執行 ./conditions 7
,得到以下結果:
$ sudo stap flow.stp
ready
process("/tmp/conditions").statement("Do@/tmp/conditions.cpp:5")
process("/tmp/conditions").statement("Do@/tmp/conditions.cpp:6")
process("/tmp/conditions").statement("Do@/tmp/conditions.cpp:12")
process("/tmp/conditions").statement("Do@/tmp/conditions.cpp:13")
process("/tmp/conditions").statement("Do@/tmp/conditions.cpp:16")
process("/tmp/conditions").statement("Do@/tmp/conditions.cpp:25")
process("/tmp/conditions").statement("Do@/tmp/conditions.cpp:26")
由輸出可知 n = 7 的時候會執行 5、6、12、13、16、25、26 行。
顯示函式呼叫流程
有時需要了解某個檔案 (模組) 內各個函式之間如何運作,針對某個簡單的輸入,觀察各函式呼叫順序,有助於掌握整體的輸廓。這裡以費氏數列為例:
$ cat fibonacci.c
#include <stdio.h>
#include <stdlib.h>int fib(int n) {
if (n == 0 || n == 1)
return 1;
return fib(n-1) + fib(n-2);
}int main(int argc, char **argv) {
int n = atoi(argv[1]);
printf("fib(%d) = %d\n", n, fib(n));
return 0;
}
偵測用的 script:
$ cat flow2.stp
global indent = 4;probe process("./fibonacci").function("*").call {
printf("%s -> %s: %s\n", thread_indent(indent), ppfunc(), $$parms);
}probe process("./fibonacci").function("*").return {
printf("%s <- %s\n", thread_indent(-indent), ppfunc());
}
- .call() 是函式進入的位置。
- .return() 是返回的位置。
- thread_indent() 會記住 indent 的間隔。
- ppfunc() 回傳函式名稱。
編譯和執行:
$ gcc -O0 fibonacci.c -o fibonacci -g
$ ./fibonacci 5
fib(5) = 8
偵測結果:
$ sudo stap flow2.stp
WARNING: function _start return probe is blacklisted: keyword at flow2.stp:7:1
source: probe process("./fibonacci").function("*").return {
^
0 fibonacci(8535): -> _start:
13 fibonacci(8535): -> __libc_csu_init:
16 fibonacci(8535): -> _init:
18 fibonacci(8535): <- _init
20 fibonacci(8535): -> frame_dummy:
22 fibonacci(8535): -> register_tm_clones:
24 fibonacci(8535): <- register_tm_clones
25 fibonacci(8535): <- frame_dummy
26 fibonacci(8535): <- __libc_csu_init
30 fibonacci(8535): -> main: argc=0x2 argv=0x7ffdb2de9078
37 fibonacci(8535): -> fib: n=0x5
40 fibonacci(8535): -> fib: n=0x4
44 fibonacci(8535): -> fib: n=0x3
47 fibonacci(8535): -> fib: n=0x2
51 fibonacci(8535): -> fib: n=0x1
53 fibonacci(8535): <- fib
55 fibonacci(8535): -> fib: n=0x0
58 fibonacci(8535): <- fib
58 fibonacci(8535): <- fib
61 fibonacci(8535): -> fib: n=0x1
63 fibonacci(8535): <- fib
64 fibonacci(8535): <- fib
66 fibonacci(8535): -> fib: n=0x2
70 fibonacci(8535): -> fib: n=0x1
72 fibonacci(8535): <- fib
74 fibonacci(8535): -> fib: n=0x0
76 fibonacci(8535): <- fib
77 fibonacci(8535): <- fib
78 fibonacci(8535): <- fib
80 fibonacci(8535): -> fib: n=0x3
84 fibonacci(8535): -> fib: n=0x2
87 fibonacci(8535): -> fib: n=0x1
89 fibonacci(8535): <- fib
91 fibonacci(8535): -> fib: n=0x0
93 fibonacci(8535): <- fib
94 fibonacci(8535): <- fib
97 fibonacci(8535): -> fib: n=0x1
99 fibonacci(8535): <- fib
100 fibonacci(8535): <- fib
101 fibonacci(8535): <- fib
150 fibonacci(8535): <- main
152 fibonacci(8535): -> __do_global_dtors_aux:
155 fibonacci(8535): -> deregister_tm_clones:
157 fibonacci(8535): <- deregister_tm_clones
158 fibonacci(8535): <- __do_global_dtors_aux
159 fibonacci(8535): -> _fini:
162 fibonacci(8535): <- _fini
可看出 main() 呼叫 fib(5),fib(5) 呼叫 fib(4) 和 fib(3),全部流程都有。
附帶一提,用 -O2 -g
編譯的話,一樣是./fibonacci 5
,輸出的結果很不一樣:
$ sudo stap flow2.stp
WARNING: function _start return probe is blacklisted: keyword at flow2.stp:7:1
source: probe process("./fibonacci").function("*").return {
^
0 fibonacci(9205): -> _start:
19 fibonacci(9205): -> __libc_csu_init:
22 fibonacci(9205): -> _init:
24 fibonacci(9205): <- _init
25 fibonacci(9205): -> frame_dummy:
28 fibonacci(9205): -> register_tm_clones:
30 fibonacci(9205): <- register_tm_clones
31 fibonacci(9205): <- frame_dummy
32 fibonacci(9205): <- __libc_csu_init
35 fibonacci(9205): -> main: argc=0x2 argv=0x7ffe53a260f8
41 fibonacci(9205): -> fib: n=0x4
43 fibonacci(9205): -> fib: n=0x3
45 fibonacci(9205): -> fib: n=0x2
47 fibonacci(9205): -> fib: n=0x1
50 fibonacci(9205): <- fib
51 fibonacci(9205): <- fib
52 fibonacci(9205): <- fib
53 fibonacci(9205): -> fib: n=0x1
55 fibonacci(9205): <- fib
56 fibonacci(9205): <- fib
57 fibonacci(9205): -> fib: n=0x2
59 fibonacci(9205): -> fib: n=0x1
61 fibonacci(9205): <- fib
62 fibonacci(9205): <- fib
114 fibonacci(9205): <- main
116 fibonacci(9205): -> __do_global_dtors_aux:
119 fibonacci(9205): -> deregister_tm_clones:
122 fibonacci(9205): <- deregister_tm_clones
123 fibonacci(9205): <- __do_global_dtors_aux
124 fibonacci(9205): -> _fini:
126 fibonacci(9205): <- _fini
main() 直接呼叫 fib(4) 和 fib(2),然後 fib(4) 是呼叫 fib(3) 和 fib(1),另外試別的數字觀察到 fib(9) 會呼叫 fib(8)、fib(6)、fib(4)、fib(2)。有了之前遇到 loop unrolling 的經驗,猜測這大概是編譯器展開遞迴的結果。於是檢視 man gcc 是否有相關的參數,看到這個:
-foptimize-sibling-calls
Optimize sibling and tail recursive calls.
Enabled at levels -O2, -O3, -Os.
試著關掉這項重編:
$ gcc -O2 fibonacci.c -o fibonacci -g -fno-optimize-sibling-calls
重新觀察一次,就看到正常的結果了:
...
33 fibonacci(12179): -> fib: n=0x4
36 fibonacci(12179): -> fib: n=0x3
38 fibonacci(12179): -> fib: n=0x2
40 fibonacci(12179): -> fib: n=0x1
42 fibonacci(12179): <- fib
43 fibonacci(12179): -> fib: n=0x0
45 fibonacci(12179): <- fib
46 fibonacci(12179): <- fib
47 fibonacci(12179): -> fib: n=0x1
49 fibonacci(12179): <- fib
50 fibonacci(12179): <- fib
51 fibonacci(12179): -> fib: n=0x2
53 fibonacci(12179): -> fib: n=0x1
55 fibonacci(12179): <- fib
56 fibonacci(12179): -> fib: n=0x0
59 fibonacci(12179): <- fib
59 fibonacci(12179): <- fib
...
系統的 shared library
和自己編的執行檔或 shared library 一樣,只要有 debug symbol 就可以觀察。那要怎麼找到系統用的 shared library debug symbol?以下以 <math.h>
內的函式為例說明在 Ubuntu 的作法。
首先要知道 math.h 用到的函式存在 libm.so 裡,函式庫的文件應該會有說明。math.h 的情況是寫在 man page 裡:
$ man floor
... Link with -lm.
Linux 的規則是 -lX 會尋找 libX.so,所以接著用 locate
找出 libm.so 的位置:
$ locate libm.so | grep "^/usr/"
...
/usr/lib/x86_64-linux-gnu/libm.so
/usr/lib32/libm.so
/usr/libx32/libm.so
用 apt-file
找出 libm 所在的 package:
$ apt-file search /usr/lib/x86_64-linux-gnu/libm.so
libc6-dev: /usr/lib/x86_64-linux-gnu/libm.so
然後用 aptitude
找出安裝 debug symbol 的套件:
$ aptitude search libc6 | grep dbg
...
i A libc6-dbg - GNU C Library: detached debugging symbols
還沒裝的話就用 apt-get
/ aptitude
裝一下,然後用 dpkg
列出套件內容:
$ dpkg -L libc6-dbg | grep libm
/usr/lib/debug/lib/x86_64-linux-gnu/libm-2.23.so
/usr/lib/debug/lib/x86_64-linux-gnu/libmvec-2.23.so
/usr/lib/debug/lib/x86_64-linux-gnu/libmemusage.so
再來可以用 nm 確認一下目標:
$ nm -C /usr/lib/debug/lib/x86_64-linux-gnu/libm-2.23.so | grep floor
0000000000017b70 i floor
...
確認 /usr/lib/debug/lib/x86_64-linux-gnu/libm-2.23.so 就是要找的目標。
相關文章
- 用 SystemTap 找出送 SIGKILL 的 process: 含安裝說明
- 用 SystemTap 找出 TCP 如何決定 MSS 的值
- 用 SystemTap 追踪 user space 程式執行的流程