资讯专栏INFORMATION COLUMN

用 Python 拓展 GDB(一)

Cheng_Gang / 3736人阅读

摘要:在末尾,我提到了也可以用来实现拓展脚本。其中最为常用的是和。接受一个字符串作为表达式,并以的形式返回表达式求值的结果。当触发断点或收到信号时,就会调用事先注册的回调函数。对应的,撤销回调函数的接口是。本教程剩余部分会提及这一点。

之前写的《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一下。

有两点需要注意的是:

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

跟一般情况不同,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	Addr: %s	Size: %d" % (
                        e.timestamp, e.addr, e.size))
                f.write("
%s
" % 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://www.ucloud.cn/yun/38000.html

相关文章

  • Python 拓展 GDB(三)

    摘要:欢迎来到用拓展的第三篇。它们必须以开头,以此区别于来自于上下文的函数。提供的基类名为。不过有一个区别是,的方法通常会返回一个对象,表示调用该函数后的返回值。它不能像通常意义上的函数独立使用,只能跟某个命令搭配。具体实现参见用拓展第一篇。 欢迎来到《用python拓展gdb》的第三篇。上一篇我们谈到了pretty printer,一个需要python支持的特性。这一篇我们谈论另一个需要p...

    quietin 评论0 收藏0
  • Python 拓展 GDB(四)

    摘要:欢迎来到用拓展的最后一篇。对于通用语言来说,暴露的接口不过是又一个库而已。这两者间的通讯使用协议。该客户端可以向外界暴露出调试时的信息。用拓展系列到此就结束了。 欢迎来到《用python拓展gdb》的最后一篇。第一篇结尾,我提到了通用语言相对于领域特定语言的一项优势,即在处理数据上更加灵活。其实通用语言还有着另一样优势,领域特定语言只能局限在宿主程序中使用,而通用语言则无此限制。对于通...

    Seay 评论0 收藏0
  • Python 拓展 GDB(二)

    摘要:欢迎来到用拓展的第二篇。到目前为止,我们都是在用实现内置领域特定语言也能实现的效果。这就是的全部要求了。构造函数接收一个表示被打印的的必选。在后被调用,可用于打印复杂的成员。能通过来自定义打印方式,无疑为的使用打开新的大门。 欢迎来到《用python拓展gdb》的第二篇。在上一篇,我们学习了gdb提供的常用python接口,并用python实现了自定义命令和调试脚本。 到目前为止,我们...

    AbnerMing 评论0 收藏0
  • Python 调试方法

    摘要:背景这几天一直在查一个线上程序住的问题这个程序总是在运行分钟后住通过以下的一些调试手段发现是打日志的时候因为满被了日志是默认打到的无论日志级别而我这个程序是被另一个程序调起的父进程没有接收子进程的导致了被打满在调试的过程中用到了以下几种调试 FROM http://kamushin.github.io/debug/python.html 背景 这几天一直在查一个线上程序 hang 住的...

    klivitamJ 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<