之前寫的《GDB 自動化操作的技術》一文介紹了可在gdb內部使用的DSL(領域特定語言)來自動化gdb的操作。藉助該DSL,我們分別實現了一個名為mv
的自定義命令,和“對賬”用的除錯指令碼。在末尾,我提到了也可以用python來實現擴充指令碼。從本篇開始,我會介紹如何使用python來給gdb編寫指令碼。由於篇幅所限,該教程會分成四篇,爭取在本週內更完。
作為開始的熱身,讓我們用python重新實現前文(《GDB 自動化操作的技術》)的mv
命令。
實現自定義命令
引用前文的mv
命令實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# ~/.gdbinit define mv if $argc == 2 delete $arg0 # 注意新建立的斷點編號和被刪除斷點的編號不同 break $arg1 else print "輸入引數數目不對,help mv以獲得用法" end end # (gdb) help mv 會輸出以下幫助文件 document mv Move breakpoint. Usage: mv old_breakpoint_num new_breakpoint Example: (gdb) mv 1 binary_search -- move breakpoint 1 to `b binary_search` end |
對應的python實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# move.py # 1. 匯入gdb模組來訪問gdb提供的python介面 import gdb # 2. 使用者自定義命令需要繼承自gdb.Command類 class Move(gdb.Command): # 3. docstring裡面的文字是不是很眼熟?gdb會提取該類的__doc__屬性作為對應命令的文件 """Move breakpoint Usage: mv old_breakpoint_num new_breakpoint Example: (gdb) mv 1 binary_search -- move breakpoint 1 to `b binary_search` """ def __init__(self): # 4. 在建構函式中註冊該命令的名字 super(self.__class__, self).__init__("mv", gdb.COMMAND_USER) # 5. 在invoke方法中實現該自定義命令具體的功能 # args表示該命令後面所銜接的引數,這裡通過string_to_argv轉換成陣列 def invoke(self, args, from_tty): argv = gdb.string_to_argv(args) if len(argv) != 2: raise gdb.GdbError('輸入引數數目不對,help mv以獲得用法') # 6. 使用gdb.execute來執行具體的命令 gdb.execute('delete ' + argv[0]) gdb.execute('break ' + argv[1]) # 7. 向gdb會話註冊該自定義命令 Move() |
python指令碼完成了,該怎麼執行呢?在gdb裡使用python指令碼,需要用source
命令:
1 2 3 |
(gdb) so ~/move.py (gdb) mv 1 binary_search.cpp:18 |
在“gdb自動化一的技術”一文中,我們最後把自定義命令的實現放到~/.gdbinit
裡面。這樣gdb每次啟動時就會執行它,而無需手動source
。直接把python程式碼放進~/.gdbinit
當然是不行的。需要變通一下,在~/.gdbinit
加入source ~/move.py
。這樣gdb每次啟動時都會替我們source
一下。
有兩點需要注意的是:
- gdb會用python 3來解釋你的python指令碼,除非你用的gdb還處於版本感人的上古時代。
- 跟一般情況不同,gdb環境中的
sys.path
是不包括當前目錄的。這意味著,如果你的指令碼依賴於當前目錄下的其他模組,你需要手工修改sys.path
。比如(gdb) python import sys; sys.path.append('')
gdb的python介面
gdb通過gdb
模組提供了不少python介面。其中最為常用的是gdb.execute
和gdb.parse_and_eval
。
如前所示,gdb.execute
可用於執行一個gdb命令。預設情況下,結果會輸出到gdb介面上。如果想把輸出結果轉存到字串中,設定to_string
為True:gdb.execute(cmd, to_string=True)
。
gdb.parse_and_eval
接受一個字串作為表示式,並以gdb.Value
的形式返回表示式求值的結果。舉例說,gdb當前上下文中有一個變數i
,i
等於3。那麼gdb.parse_and_eval('i + 1')
的結果是一個gdb.Value
的例項,其value
屬性的值為4。這跟(gdb) i + 1
是等價的。
何為gdb.Value
?在gdb會話裡,我們可以訪問C/C++型別的值。當我們通過python介面跟這些值打交道時,gdb會把它們包裝成一個gdb.Value
物件。
舉個例子,struct Point
有x跟y兩個成員。現在假設當前上下文中有一個Point型別的變數point
和指向該變數的Point指標p
,就意味著:
1 2 3 4 5 6 7 8 |
point = gdb.parse_and_eval('point') point['x'] # 等價於point.x point['y'] # 等價於point.y point.referenced_value() # 等價於&point p = gdb.parse_and_eval('p') point2 = p.dereference() # 等價於*p point2['x'] # 等價於(*p).x,也即p->x |
有時候我們需要轉換gdb.Value的型別。如果能在gdb上下文內完成轉換,那倒是不難:gdb.parse_and_eval('(TypeX)$a')
。
但如果只能在python程式碼這一邊完成轉換,倒是有些複雜,需要使用gdb.Type型別:typeX_point = point.cast(gdb.lookup_type('TypeX'))
。gdb.Value
有一個cast
方法用於型別轉換,接收一個gdb.Type
物件。我們還需要使用lookup_type
來構建一個gdb.Type
物件。看上去是挺囉嗦。值得注意的是,’TypeX *’和’TypeX &’並非獨立的型別。如果你要獲得型別X的指標/引用,需要這麼寫gdb.lookup_type('X').pointer()
/gdb.lookup_type('X').reference()
。
另外一個常用的介面是gdb.events.stop.connect
。你可以使用該介面註冊gdb停止時的回撥函式。當gdb觸發斷點或收到訊號時,就會呼叫事先註冊的回撥函式。對應的,撤銷回撥函式的介面是gdb.events.stop.disconnect
。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
bps = gdb.breakpoints() if bps is None: raise gdb.GdbError('No breakpoints') last_breakpoint_num = bps[-1].number def commands(event): if not isinstance(event, gdb.BreakpointEvent): return if last_breakpoint_num in (bp.number for bp in event.breakpoints): gdb.execute('info locals') gdb.execute('info args') gdb.events.stop.connect(commands) |
藉助這些介面,我們可以這樣重新實現前文用到的“對賬”指令碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# malloc_free.py from collections import defaultdict, namedtuple import atexit import time import gdb Entry = namedtuple('Entry', ['addr', 'bt', 'timestamp', 'size']) MEMORY_POOL = {} MEMORY_LOST = defaultdict(list) def comm(event): if isinstance(event, gdb.SignalEvent): return # handle BreakpointEvent for bp in event.breakpoints: if bp.number == 1: addr = str(gdb.parse_and_eval('p')) bt = gdb.execute('bt', to_string=True) timestamp = time.strftime('%H:%M:%S', time.localtime()) size = int(gdb.parse_and_eval('size')) if addr in MEMORY_POOL: MEMORY_LOST[addr].append(MEMORY_POOL[addr]) MEMORY_POOL[addr] = Entry(addr, bt, timestamp, size) elif bp.number == 2: addr = gdb.parse_and_eval('p') if addr in MEMORY_POOL: del MEMORY_POOL[addr] gdb.execute('c') def dump_memory_lost(memory_lost, filename): with open(filename, 'w') as f: for entries in MEMORY_LOST.values(): for e in entries: f.write("Timestamp: %s\tAddr: %s\tSize: %d" % ( e.timestamp, e.addr, e.size)) f.write('\n%s\n' % e.bt) atexit.register(dump_memory_lost, MEMORY_LOST, '/tmp/log') # Write to result file once signal catched gdb.events.stop.connect(comm) gdb.execute('set pagination off') gdb.execute('b my_malloc') # breakpoint 1 gdb.execute('b my_free') # breakpoint 2 gdb.execute('c') |
用法:sudo gdb -q -p $(pidof $your_project) -x malloc_free.py
。
小結
對比於前文的DSL實現,“對賬”指令碼的python實現裡直接完成了對資料的處理,免去了額外寫一個指令碼來處理輸出結果。能夠靈活方便地處理資料——這是諸如python一類的通用語言對於領域特定語言的優勢。當然,領域特定語言在其擅長的領域裡,具有通用語言無法比擬的親和力——直接輸入gdb命令,顯然比每次都gdb.execute('xxx')
要順暢得多。無論是自定義的mv
命令,還是“對賬”指令碼,python實現都要比DSL實現更長。當然,python比照DSL來說,有其自身的長處。本教程剩餘部分會提及這一點。
如果說本篇主要講了如何用python實現DSL實現過的內容,那麼接下來幾篇將關注於如何用python實現DSL實現不了的內容。敬請期待。
完整的python API參見官方文件:https://sourceware.org/gdb/current/onlinedocs/gdb/Python-API.html
另外本人寫過一個gdb介面的輔助模組,包裝了常用的gdb介面: https://github.com/spacewander/debugger-utils 。感興趣的話可以參考下里面的實現。