用 Python 拓展 GDB(四)

歡迎來到《用python拓展gdb》的最後一篇。第一篇結尾,我提到了通用語言相對於領域特定語言的一項優勢,即在處理數據上更加靈活。其實通用語言還有著另一樣優勢,領域特定語言只能局限在宿主程式中使用,而通用語言則無此限制。對於通用語言來說,gdb暴露的介面不過是又一個庫而已。

在本篇中,我們會把python當作一門「膠水語言」,A面是gdb的介面,B面是一個終端介面的程式。姑且把這個終端介面程式稱之為gti(gdb's terminal interface)吧。我們會實現從gdb到gti的單向數據傳輸。每當gdb觸發斷點時,就在gti上自動輸出各項相關資訊。這兩者間的通訊使用UDP協議。換言之,接下來要完成的是一個位於gdb內部UDP客戶端,和監聽指定埠的帶終端介面的UDP服務端。

gdb 端實現

gdb端功能如下:

  1. 每當斷點被觸發時,通過gdb介面獲取info breakpointsinfo args,以及info locals三者的值
  2. 把上述三者的值轉換成json格式
  3. 通過UDP協議發送到埠9876

功能要求看上去很多,不過實現成程式碼其實也就二三十行:

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的輸出中:

$ nc -l 9876  {"locals": "pointers = ...

gti 端實現

gti 端功能如下:

  1. 監聽埠9876
  2. 每當收到數據包時,提取出json格式的數據
  3. 根據收到的數據,重繪當前介面

在繪製終端介面時,我用的是自帶的curses模組。在監聽埠方面,我用的是python3.4之後才有的async模組。當然蘿蔔白菜,各有所愛,大可改用你自己喜歡的庫。

#!/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: %snn' % 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拓展予以解決。