
若在 Linux Kernel 內,想追蹤每個函式的呼叫流程,第一個會想到的工具應該是 ftrace;那針對 user space 的程式是否有類似的東西?用 gdb,再加上一些簡單 script 就可以完成。
以 xdotool 為例,舉例來說,xdotool getmouselocation 將會得當前鼠標的位置,正常狀況下輸出結果如下:
$ xdotool getmouselocation findclient: 65011716 findclient: 65011716 x:2769 y:656 screen:0 window:65011716
若搭配本文將介紹的方法,輸出結果如下:
(gdb) r Starting program: /usr/bin/xdotool getmouselocation [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". | main at xdotool.c:287 | _start at ??:0 | __libc_start_main at ??:0 | xdotool_main at xdotool.c:290 | xdotool_main at xdotool.c:316 | args_main at xdotool.c:456 | args_main at xdotool.c:493 | xdo_new at xdo.c:89 | xdo_new_with_opened_display at xdo.c:106 | xdo_new_with_opened_display at xdo.c:131 | xdo_enable_feature at xdo.c:2053 | xdo_new_with_opened_display at xdo.c:140 | _xdo_populate_charcode_map at xdo.c:1360 | _keysym_to_char at xdo.c:1390 | args_main at xdotool.c:509 | context_execute at xdotool.c:519 | context_execute at xdotool.c:536 | cmd_getmouselocation at cmd_getmouselocation.c:3 | cmd_getmouselocation at cmd_getmouselocation.c:38 | consume_args at xdotool.c:46 | cmd_getmouselocation at cmd_getmouselocation.c:40 | xdo_mouselocation2 at xdo.c:858 | xdo_mouselocation2 at xdo.c:886 | xdo_window_find_client at xdo.c:1222 | xdo_getwinprop at xdo.c:1541 | xdo_mouselocation2 at xdo.c:889 | xdo_window_find_client at xdo.c:1222 | xdo_getwinprop at xdo.c:1541 | xdo_window_find_client at xdo.c:1241 | xdo_window_find_client at xdo.c:1222 | xdo_getwinprop at xdo.c:1541 | xdo_window_find_client at xdo.c:1241 | xdo_window_find_client at xdo.c:1222 | xdo_getwinprop at xdo.c:1541 findclient: 65011716 findclient: 65011716 | xdo_mouselocation2 at xdo.c:909 | _is_success at xdo.c:1533 | cmd_getmouselocation at cmd_getmouselocation.c:48 | xdotool_output at xdotool.c:583 x:2262 y:689 screen:0 window:65011716 | args_main at xdotool.c:511 | xdo_free at xdo.c:144
是不是整個程式內函式呼叫順序都一目瞭然。
快速開始
先安裝 debug symbol,可以參考這裡將 debug symbol 來源設好。 接著安裝這 2 個套件 libxdo2-dbgsym xdotool-dbgsym
$ sudo apt-get install libxdo2-dbgsym xdotool-dbgsym $ apt-get source xdotool $ cd path/to/xdotool/source $ wget https://gist.github.com/fcwu/7466813/raw/69daa7ad2227eb99e66b87e832e04ad8a8798385/supertrace.gdb $ wget https://gist.github.com/fcwu/7466823/raw/fb9fee4cf987aaa291bef7fb50959e9eb0f5247b/supertrace.py
準備工作完成。把 gdb 跑起來吧!
$ gdb -x supertrace.gdb --args xdotool getmouselocation (gdb) c Starting program: /usr/bin/xdotool getmouselocation [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". | main at xdotool.c:287 | _start at ??:0 | __libc_start_main at ??:0 | xdotool_main at xdotool.c:290 | xdotool_main at xdotool.c:316 | args_main at xdotool.c:456 | args_main at xdotool.c:493 以下略...
深入一點
GDB 指令檔:supertrace.gdb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
set pagination off | |
b main | |
r | |
python | |
sys.path.insert(0, './') | |
import supertrace | |
end | |
breakall | |
set $x=$lastbp() | |
commands 1-$x | |
silent | |
supertrace | |
cont | |
end |
裡頭會載入 supertrace.py (本文末),supertrace.py 內,提供了 2 個指令 breakall and supertrace 和 1 個函式 lastbp。breakall 會把所有有 debug info 的函式當做追蹤目標,指令 xdotool 因為安裝了 xdotool-dbgsym 確定有 debug info 外,shared library 的部分可以用 info sharedlibrary 來觀察:
(gdb) info sharedlibrary From To Syms Read Shared Object Library 0x00007ffff7ddaaf0 0x00007ffff7df4eba Yes (*) /lib64/ld-linux-x86-64.so.2 0x00007ffff7bd12d0 0x00007ffff7bd5f28 Yes /usr/lib/libxdo.so.2 0x00007ffff78d7570 0x00007ffff793ffe8 Yes (*) /lib/x86_64-linux-gnu/libm.so.6 0x00007ffff75b5a70 0x00007ffff763d6c8 Yes (*) /usr/lib/x86_64-linux-gnu/libX11.so.6 0x00007ffff73971e0 0x00007ffff739a6b8 Yes (*) /lib/x86_64-linux-gnu/librt.so.1 0x00007ffff6ff3fa0 0x00007ffff71385d0 Yes (*) /lib/x86_64-linux-gnu/libc.so.6 0x00007ffff6dd0360 0x00007ffff6dd2d88 Yes (*) /usr/lib/x86_64-linux-gnu/libXtst.so.6 0x00007ffff6bcc9f0 0x00007ffff6bcd318 Yes (*) /usr/lib/x86_64-linux-gnu/libXinerama.so.1 0x00007ffff69b7350 0x00007ffff69c36d8 Yes (*) /usr/lib/x86_64-linux-gnu/libxcb.so.1 0x00007ffff67aade0 0x00007ffff67ab918 Yes (*) /lib/x86_64-linux-gnu/libdl.so.2 0x00007ffff6592740 0x00007ffff659e358 Yes (*) /lib/x86_64-linux-gnu/libpthread.so.0 0x00007ffff637f490 0x00007ffff63894f8 Yes (*) /usr/lib/x86_64-linux-gnu/libXext.so.6 0x00007ffff6179d90 0x00007ffff617aae8 Yes (*) /usr/lib/x86_64-linux-gnu/libXau.so.6 0x00007ffff5f74090 0x00007ffff5f75aa8 Yes (*) /usr/lib/x86_64-linux-gnu/libXdmcp.so.6 (*): Shared library is missing debugging information.
值得注意的是若是觀察的 library 太多,會因為 debug info 太大,啟動速度變很慢。接著在 supertrace.gdb 裡用了 lastbp 函式取得最後的 breakpoint number,等會設 breakpoint command 會用。
最後,在 breakpoint command 裡,最重要的就是 supertrace 這個命令,它主要會收集 backtrace 的輸出,並拿來跟之前輸出比較,將最後結果印出。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
import gdb | |
INDENT = ' ' | |
class SuperTrace(gdb.Command): | |
old_stack = [] | |
def __init__(self): | |
super(SuperTrace, self).__init__("supertrace", | |
gdb.COMMAND_SUPPORT, | |
gdb.COMPLETE_NONE) | |
def supertrace(self): | |
def backtrace_generator(): | |
f = gdb.newest_frame() | |
while f is not None: | |
yield f | |
f = gdb.Frame.older(f) | |
fstack = [] | |
f = gdb.newest_frame() | |
for f in backtrace_generator(): | |
frame_name = gdb.Frame.name(f) | |
if frame_name is None: | |
continue | |
#frame_name = '??' | |
filename = '??' | |
line = 0 | |
try: | |
symtab_and_line = gdb.Frame.find_sal(f) | |
filename = symtab_and_line.symtab.filename | |
line = symtab_and_line.line | |
except: | |
#continue | |
pass | |
args = [ | |
frame_name, | |
filename, | |
line, | |
] | |
fstack.append(args) | |
fstack.reverse() | |
ostack = self.__class__.old_stack | |
is_print = False | |
for f in enumerate(fstack): | |
if f[0] >= len(ostack): | |
is_print = True | |
else: | |
if f[1] != ostack[f[0]]: | |
is_print = True | |
if is_print: | |
print('{}| {} at {}:{}'.format( | |
INDENT * f[0], f[1][0], f[1][1], f[1][2])) | |
self.__class__.old_stack = fstack | |
def invoke(self, arg, from_tty): | |
#num = 0 | |
#try: | |
# num = int(arg) | |
#except Exception: | |
# pass | |
try: | |
self.supertrace() | |
except Exception as e: | |
print(str(e)) | |
class LastBreakpoints(gdb.Function): | |
def __init__(self): | |
super(LastBreakpoints, self).__init__("lastbp") | |
def invoke(self): | |
return len(gdb.breakpoints()) | |
class BreakAll(gdb.Command): | |
def __init__(self): | |
super(self.__class__, self).__init__("breakall", | |
gdb.COMMAND_SUPPORT, | |
gdb.COMPLETE_NONE) | |
def filelist(self): | |
files = set() | |
raw = gdb.execute('info functions', True, True) | |
lines = raw.split('\n') | |
for line in enumerate(lines): | |
if line[1].startswith('File '): | |
files.add(line[1][5:line[1].find(':')]) | |
#print '{0:3d}: {1}'.format(line[0], line[1]) | |
return files | |
def invoke(self, arg, from_tty): | |
for f in self.filelist(): | |
gdb.execute('rbreak {}:.'.format(f)) | |
SuperTrace() | |
LastBreakpoints() | |
BreakAll() |