歡迎來到《用python擴充gdb》的最後一篇。第一篇結尾,我提到了通用語言相對於領域特定語言的一項優勢,即在處理資料上更加靈活。其實通用語言還有著另一樣優勢,領域特定語言只能侷限在宿主程式中使用,而通用語言則無此限制。對於通用語言來說,gdb暴露的介面不過是又一個庫而已。
在本篇中,我們會把python當作一門“膠水語言”,A面是gdb的介面,B面是一個終端介面的程式。姑且把這個終端介面程式稱之為gti(gdb’s terminal interface)吧。我們會實現從gdb到gti的單向資料傳輸。每當gdb觸發斷點時,就在gti上自動輸出各項相關資訊。這兩者間的通訊使用UDP協議。換言之,接下來要完成的是一個位於gdb內部UDP客戶端,和監聽指定埠的帶終端介面的UDP服務端。
gdb 端實現
gdb端功能如下:
- 每當斷點被觸發時,通過gdb介面獲取
info breakpoints
和info args
,以及info locals
三者的值 - 把上述三者的值轉換成json格式
- 通過UDP協議傳送到埠9876
功能要求看上去很多,不過實現成程式碼其實也就二三十行:
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 |
import json import socket import gdb HOST = 'localhost' PORT = 9876 SOCK = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) SOCK.connect((HOST, PORT)) def send_data(event): cur = event.breakpoints[0].location if cur is None: cur = event.breakpoints[0].expr local_vars = gdb.execute('info locals', to_string=True) args = gdb.execute('info args', to_string=True) bps = gdb.execute('info breakpoints', to_string=True) data = { 'current': cur, 'locals': local_vars, 'args': args, 'breakpoints': bps } data = json.dumps(data) SOCK.send(bytes(data, 'utf-8')) gdb.events.stop.connect(send_data) |
在此之前,需要設定一個監聽9876埠的服務端,不然客戶端這邊就建立不了連線。執行nc -l 9876
作為服務端的mock,暫時只需觀察下傳送過來的資料是否正確。
寫一個自動化指令碼,讓gdb設定若干斷點並執行,連續執行多次continue
。你應該可以觀察到接連有資料顯示在nc
的輸出中:
1 2 |
$ nc -l 9876 {"locals": "pointers = ... |
gti 端實現
gti 端功能如下:
- 監聽埠9876
- 每當收到資料包時,提取出json格式的資料
- 根據收到的資料,重繪當前介面
在繪製終端介面時,我用的是自帶的curses模組。在監聽埠方面,我用的是python3.4之後才有的async模組。當然蘿蔔白菜,各有所愛,大可改用你自己喜歡的庫。
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 48 49 50 51 52 53 54 |
#!/usr/bin/env python3 import asyncio import curses import json def main(): loop = asyncio.get_event_loop() # 1. 監聽埠9876 server = loop.create_datagram_endpoint( GtiProtocol, local_addr=('127.0.0.1', 9876)) try: loop.run_until_complete(server) loop.run_forever() except KeyboardInterrupt: pass finally: curses.endwin() class GtiProtocol(asyncio.Protocol): def __init__(self): self.ui = TextPad() def datagram_received(self, byte, _): "2. 將收到的資料從byte轉成json" data = byte.decode() data = json.loads(data) self.ui.display(data) class TextPad: def __init__(self): self.pad = curses.initscr() curses.start_color() def _addstr(self, text): self.pad.addstr(text, curses.A_BOLD) def display(self, data): "3. 根據給定的資料重繪介面" try: self.pad.erase() self._addstr('current: %s\n\n' % data['current']) for key, value in data.items(): if key != 'current': self._addstr('%s:\n' % key) self._addstr(value) self._addstr('\n') self.pad.refresh() except curses.error: pass main() |
現在可以用./gti.py
來替換掉nc -l 9876
,再重新執行gdb。你應該能看到,每當有新的斷點觸發時,./gti.py
就會應用新的資料繪製介面。
順便一提,使用curses模組純粹是為了方便示範。curses提供的介面過於底層,許多細節方面都需要自己去摳。如果真的要開發實際可用的終端介面程式,建議使用諸如urwid這樣的第三方包。
小結
如上面的例子所示,我們成功地用python實現了內嵌於gdb的客戶端。該客戶端可以向外界暴露出gdb除錯時的資訊。依據同樣的思路,我們也可以在gdb內實現內嵌的服務端,這樣外界就能動態修改gdb除錯的方式。當然,這一切離不開python這把“瑞士軍刀”。
《用python擴充gdb》系列到此就結束了。如果你正準備編寫一個擴充,希望本教程可以教會相關的知識。如果你是一位C/C++開發者,希望本教程能夠讓你的工具箱增添新道具。如果你是想了解更多關於gdb除錯的資訊,希望今後遇到相關問題時能想起編寫python擴充予以解決。