用 Python 拓展 GDB(一)

以前寫的《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

  1. gdb會用python 3來解釋你的python腳本,除非你用的gdb還處於版本感人的上古時代。函數

  2. 跟通常狀況不一樣,gdb環境中的sys.path是不包括當前目錄的。這意味着,若是你的腳本依賴於當前目錄下的其餘模塊,你須要手工修改sys.path。好比(gdb) python import sys; sys.path.append('')spa

gdb的python接口

gdb經過gdb模塊提供了很多python接口。其中最爲經常使用的是gdb.executegdb.parse_and_evaldebug

如前所示,gdb.execute可用於執行一個gdb命令。默認狀況下,結果會輸出到gdb界面上。若是想把輸出結果轉存到字符串中,設置to_string爲True:gdb.execute(cmd, to_string=True)

gdb.parse_and_eval接受一個字符串做爲表達式,並以gdb.Value的形式返回表達式求值的結果。舉例說,gdb當前上下文中有一個變量ii等於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 。感興趣的話能夠參考下里面的實現。

相關文章
相關標籤/搜索