用 SystemTap 追踪 user space 程式執行的流程

fcamel
fcamel的程式開發心得
14 min readNov 12, 2017

前幾篇文章 (例一例二) 介紹用 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 就是要找的目標。

相關文章

--

--