使用 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()

Reference

http://rickey-nctu.blogspot.tw/search/label/GDB


书籍推荐