转载

用 Python 拓展 GDB(一)

之前写的 《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 。感兴趣的话可以参考下里面的实现。

原文  https://segmentfault.com/a/1190000005718889
正文到此结束
Loading...