以前寫的《GDB 自動化操做的技術》一文介紹了可在gdb內部使用的DSL(領域特定語言)來自動化gdb的操做。藉助該DSL,咱們分別實現了一個名爲mv
的自定義命令,和「對帳」用的調試腳本。在末尾,我提到了也能夠用python來實現拓展腳本。從本篇開始,我會介紹如何使用python來給gdb編寫腳本。因爲篇幅所限,該教程會分紅四篇,爭取在本週內更完。html
做爲開始的熱身,讓咱們用python從新實現前文(《GDB 自動化操做的技術》)的mv
命令。python
引用前文的mv
命令實現以下:git
# ~/.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實現以下:github
# 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
命令:segmentfault
(gdb) so ~/move.py (gdb) mv 1 binary_search.cpp:18
在「gdb自動化一的技術」一文中,咱們最後把自定義命令的實現放到~/.gdbinit
裏面。這樣gdb每次啓動時就會運行它,而無需手動source
。直接把python代碼放進~/.gdbinit
固然是不行的。須要變通一下,在~/.gdbinit
加入source ~/move.py
。這樣gdb每次啓動時都會替咱們source
一下。數組
有兩點須要注意的是:app
gdb會用python 3來解釋你的python腳本,除非你用的gdb還處於版本感人的上古時代。函數
跟通常狀況不一樣,gdb環境中的sys.path
是不包括當前目錄的。這意味着,若是你的腳本依賴於當前目錄下的其餘模塊,你須要手工修改sys.path
。好比(gdb) python import sys; sys.path.append('')
spa
gdb經過gdb
模塊提供了很多python接口。其中最爲經常使用的是gdb.execute
和gdb.parse_and_eval
。debug
如前所示,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
,就意味着:
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
。
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)
藉助這些接口,咱們能夠這樣從新實現前文用到的「對帳」腳本:
# 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 。感興趣的話能夠參考下里面的實現。