用 Python 拓展 GDB(一)

spacewander

之前写的《GDB 自动化操作的技术》一文介绍了可在gdb内部使用的DSL(领域特定语言)来自动化gdb的操作。借助该DSL,我们分别实现了一个名为mv的自定义命令,和“对账”用的调试脚本。在末尾,我提到了也可以用python来实现拓展脚本。从本篇开始,我会介绍如何使用python来给gdb编写脚本。由于篇幅所限,该教程会分成四篇,争取在本周内更完。

作为开始的热身,让我们用python重新实现前文(《GDB 自动化操作的技术》)的mv命令。

实现自定义命令

引用前文的mv命令实现如下:

# ~/.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实现如下:

# 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命令:

(gdb) so ~/move.py
(gdb) mv 1 binary_search.cpp:18

在“gdb自动化一的技术”一文中,我们最后把自定义命令的实现放到~/.gdbinit里面。这样gdb每次启动时就会运行它,而无需手动source。直接把python代码放进~/.gdbinit当然是不行的。需要变通一下,在~/.gdbinit加入source ~/move.py。这样gdb每次启动时都会替我们source一下。

有两点需要注意的是:

  1. gdb会用python 3来解释你的python脚本,除非你用的gdb还处于版本感人的上古时代。

  2. 跟一般情况不同,gdb环境中的sys.path是不包括当前目录的。这意味着,如果你的脚本依赖于当前目录下的其他模块,你需要手工修改sys.path。比如(gdb) python import sys; sys.path.append('')

gdb的python接口

gdb通过gdb模块提供了不少python接口。其中最为常用的是gdb.executegdb.parse_and_eval

如前所示,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 。感兴趣的话可以参考下里面的实现。

阅读 14.3k

spacewander
这个专栏什么都有,大部分都是关于Linux或后端开发的

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.

5.4k 声望
481 粉丝
0 条评论

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.

5.4k 声望
481 粉丝
宣传栏