用 Python 拓展 GDB(四)

发布时间:2019-10-10 09:17:43编辑:auto阅读(1789)

    欢迎来到《用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: %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拓展予以解决。

关键字