使用 GDB 以樹狀方式將函式流程印出
若在 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
準備工作完成。把 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
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 的輸出,並拿來跟之前輸出比較,將最後結果印出。
#!/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()