用 python gdb 客製化 backtrace 的結果

需求

想要瞭解模組之間函式呼叫的關係時, 與其一層層比對多個類別之間的呼叫關係, 不如直接在最後一個呼叫函式放中斷點, 直接顯示 backtrace。但是當函式裡有太多參數或 template 時, backtrace 的 frame 訊息會變得很長, 不易閱讀。我的目的只是找出呼叫的函式名稱、檔名和行數, 函式帶的參數反而是困擾。

作法一: 用 gdb.execute()

一個簡單的作法是截取 gdb 的輸出, 然後解析文字去掉不要的部份:

import gdb
import re
class ShorternBacktraceCommand(gdb.Command):
    '''Show a backtrace without argument info in each frame.'''

    def __init__(self):
        super(ShorternBacktraceCommand, self).__init__ ("bt",
                                                        gdb.COMMAND_SUPPORT,
                                                        gdb.COMPLETE_NONE)
    def invoke(self, arg, from_tty):
        if not arg:
            arg = ''
        raw = gdb.execute("backtrace %s" % arg, True, True)
        lines = raw.split('\n')
        for i, line in enumerate(lines):
            if not line:
                continue

            tokens = line.split()
            # first line format: e.g., #0  A::hello (...) at a.cpp:8
            # the rest         : e.g., #2  0x0..0 in A::foo (...) at a.cpp:18
            func_index = 1 if i == 0 else 3
            print ('\033[1;33m%2s\033[m  %s at %s'
                   '' % (tokens[0], tokens[func_index], tokens[-1]))

ShorternBacktraceCommand()

Btw, 上面的作法還順便幫行首的標號上色。

但是, 使用 cgdb 時會無法運作, 理由是 cgdb 使用 GDB MI, gdb.execute('backtrace') 的結果不是原本看到的格式, 難以解析。

作法二: 用 gdb.Frame() API

只好改用中規中矩的方式逐一讀取 frame, 取出需要的資訊:

import gdb

class ShorternBacktraceCommand(gdb.Command):
    '''Show a backtrace without argument info in each frame.'''

    def __init__(self):
        super(ShorternBacktraceCommand, self).__init__ ("bt",
                                                        gdb.COMMAND_SUPPORT,
                                                        gdb.COMPLETE_NONE)
    def invoke(self, arg, from_tty):
        num = 0;
        try:
            num = int(arg)
        except Exception, e:
            pass

        lines = []
        f = gdb.newest_frame()
        fn = 0
        while f is not None:
            symtab_and_line = gdb.Frame.find_sal(f)
            frame_name = gdb.Frame.name(f)
            if frame_name:
                args = [
                    fn,
                    frame_name,
                    symtab_and_line.symtab.filename,
                    symtab_and_line.line,
                ]
            else:
                args = [fn, '??', 'unknown', 0]
            lines.append('#%2d  %s at %s:%s' % tuple(args))

            f = gdb.Frame.older(f)
            fn += 1

        if num > 0:
            lines = lines[:num]
        elif num < 0:
            lines = lines[len(lines) + num:]

        for line in lines:
            print line


ShorternBacktraceCommand()

將上面的 script 存到 /path/to/gdb/scripts/backtrace.py, 接著在 $HOME/.gdbinit 裡加入以下設定:

python
sys.path.insert(0, '/path/to/gdb/scripts')
import backtrace
end

之後就能用 bt 顯示精簡後的 backtrace 了, 也方便手動複製貼上到筆記裡。以下是一個輸出例子:

(gdb) bt
# 0  A::hello at a.cpp:8
# 1  A::bar at a.cpp:13
# 2  A::foo at a.cpp:18
# 3  main at a.cpp:25

Btw, 若是需求比較簡單, 可以試看看 Print Settings, 有些選項可以改變 backtrace 顯示的訊息。

參考資料:

  • The Cliffs of Inanity › 2. Writing a new gdb command
  • Pruning backtrace output with gdb script - Stack Overflow
  • Frames In Python - Debugging with GDB
  • Symbols In Python - Debugging with GDB
  • Symbol Tables In Python - Debugging with GDB