【问题标题】:gdb pretty printing with python a recursive structuregdb用python漂亮打印递归结构
【发布时间】:2014-06-28 00:03:19
【问题描述】:

我对 Python 不是很熟悉,我只是在探索 GDB python 脚本功能;我的问题的动机是增强 MELT monitor 中值的 GDB 打印,稍后将连接到 GCC MELT。但这里有一个更简单的变体。

我的系统是 Linux/Debian/Sid/x86-64。 GCC 编译器是 4.8.2; GDB 调试器是 7.6.2;它的python是3.3

我想调试一个带有“可区分联合”类型的 C 程序:

// file tiny.c in the public domain by Basile Starynkevitch
// compile with gcc -g3 -Wall -std=c99 tiny.c -o tiny
// debug with gdb tiny
// under gdb: python tiny-gdb.py
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef union my_un myval_t;
enum tag_en {
  tag_none,
  tag_int,
  tag_string,
  tag_sequence
};
struct boxint_st;
struct boxstring_st;
struct boxsequence_st;
union my_un {
  void* ptr;
  enum tag_en *ptag;
  struct boxint_st *pint;
  struct boxstring_st *pstr;
  struct boxsequence_st *pseq;
};

struct boxint_st {
  enum tag_en tag;      // for tag_int
  int ival;
};
struct boxstring_st {
  enum tag_en tag;      // for tag_string
  char strval[];        // a zero-terminated C string 
};
struct boxsequence_st {
  enum tag_en tag;      // for tag_sequence
  unsigned slen;
  myval_t valtab[];     // of length slen
};


int main (int argc, char **argv) {
  printf ("start %s, argc=%d", argv[0], argc);
  struct boxint_st *iv42 = malloc (sizeof (struct boxint_st));
  iv42->tag = tag_int;
  iv42->ival = 42;
  struct boxstring_st *istrhello =
    malloc (sizeof (struct boxstring_st) + sizeof ("hello") + 1);
  istrhello->tag = tag_string;
  strcpy (istrhello->strval, "hello");
  struct boxsequence_st *iseq3 =
    malloc (sizeof (struct boxsequence_st) + 3 * sizeof (myval_t));
  iseq3->tag = tag_sequence;
  iseq3->slen = 3;
  iseq3->valtab[0] = (myval_t)iv42;
  iseq3->valtab[1] = (myval_t)istrhello;
  iseq3->valtab[2] = (myval_t)NULL;
  printf ("before %s:%d gdb print iseq3\n", __FILE__, __LINE__);
}

这是我要在 gdb 下读取的 Python 文件

 # file tiny-gdb.py in the public domain by Basile Starynkevitch
 ## see also tiny.c file
class my_val_Printer:
    """pretty prints a my_val"""
    def __init__ (self, val):
        self.val = val
    def to_string (self):
        outs = "my_val@" + self.val['ptr']
        mytag = self.val['ptag'].dereference();
        if (mytag):
            outs = outs + mytag.to_string()
    def display_hint (self):
        return 'my_val'

def my_val_lookup(val):
    lookup = val.type.tag
    if (lookup == None):
        return None
    if lookup == "my_val":
        return my_val_Printer(val)
    return None

我被以下基本问题困扰。

  1. 如何在 GDB 下的 python 中安装我的漂亮打印机? (我在文档中看到了几种方法,但我无法选择合适的一种)。
  2. 如何确保 GDB 以相同的方式漂亮地打印 union my_un 及其类型定义的同义词 myval_t
  3. 漂亮的打印机应该如何检测 NULL 指针?
  4. 我的漂亮打印机如何为struct boxsequence_st 递归?这意味着检测指针非零,然后取消引用它的ptag,将该标记与tag_sequence 进行比较,漂亮地打印valtab 灵活数组成员。
  5. 如何避免对漂亮打印进行过深的递归?

【问题讨论】:

    标签: python c linux gdb


    【解决方案1】:

    我没有足够的 gdb Python api 经验,无法将其称为答案;我认为这只是来自一位开发人员的一些研究笔记。下面附上的我的代码也非常粗糙和丑陋。但是,这确实适用于 gdb-7.4 和 python-2.7.3。调试运行示例:

    $ gcc -Wall -g3 tiny.c -o tiny
    $ gdb tiny
    (gdb) b 58
    (gdb) run
    (gdb) print iseq3
    $1 = (struct boxsequence_st *) 0x602050
    (gdb) print iv42
    $2 = (struct boxint_st *) 0x602010
    (gdb) print istrhello
    $3 = (struct boxstring_st *) 0x602030
    

    以上所有都是沼泽标准的漂亮打印输出——我的理由是我经常想看看 指针 是什么,所以我不想覆盖它们。但是,引用指针使用下面进一步显示的漂亮打印机:

    (gdb) print *iseq3
    $4 = (struct boxsequence_st)(3) = {(struct boxint_st)42, (struct boxstring_st)"hello"(5), NULL}
    (gdb) print *iv42
    $5 = (struct boxint_st)42
    (gdb) print *istrhello
    $6 = (struct boxstring_st)"hello"(5)
    (gdb) set print array
    (gdb) print *iseq3
    $7 = (struct boxsequence_st)(3) = {
      (struct boxint_st)42,
      (struct boxstring_st)"hello"(5),
      NULL
    }
    (gdb) info auto-load
    Loaded  Script                                                                 
    Yes     /home/.../tiny-gdb.py
    

    最后一行显示在调试tiny时,同一目录中的tiny-gdb.py会自动加载(虽然您可以禁用此功能,但我相信这是默认行为)。

    上面使用的tiny-gdb.py文件:

    def deref(reference):
        target = reference.dereference()
        if str(target.address) == '0x0':
            return 'NULL'
        else:
            return target
    
    class cstringprinter:
        def __init__(self, value, maxlen=4096):
            try:
                ends = gdb.selected_inferior().search_memory(value.address, maxlen, b'\0')
                if ends is not None:
                    maxlen = ends - int(str(value.address), 16)
                    self.size = str(maxlen)
                else:
                    self.size = '%s+' % str(maxlen)
                self.data = bytearray(gdb.selected_inferior().read_memory(value.address, maxlen))
            except:
                self.data = None
        def to_string(self):
            if self.data is None:
                return 'NULL'
            else:
                return '\"%s\"(%s)' % (str(self.data).encode('string_escape').replace('"', '\\"').replace("'", "\\\\'"), self.size)
    
    class boxintprinter:
        def __init__(self, value):
            self.value = value.cast(gdb.lookup_type('struct boxint_st'))
        def to_string(self):
            return '(struct boxint_st)%s' % str(self.value['ival'])
    
    class boxstringprinter:
        def __init__(self, value):
            self.value = value.cast(gdb.lookup_type('struct boxstring_st'))
        def to_string(self):
            return '(struct boxstring_st)%s' % (self.value['strval'])
    
    class boxsequenceprinter:
        def __init__(self, value):
            self.value = value.cast(gdb.lookup_type('struct boxsequence_st'))
        def display_hint(self):
            return 'array'
        def to_string(self):
            return '(struct boxsequence_st)(%s)' % str(self.value['slen'])
        def children(self):
            value = self.value
            tag = str(value['tag'])
            count = int(str(value['slen']))
            result = []
            if tag == 'tag_none':
                for i in xrange(0, count):
                    result.append( ( '#%d' % i, deref(value['valtab'][i]['ptag']) ))
            elif tag == 'tag_int':
                for i in xrange(0, count):
                    result.append( ( '#%d' % i, deref(value['valtab'][i]['pint']) ))
            elif tag == 'tag_string':
                for i in xrange(0, count):
                    result.append( ( '#%d' % i, deref(value['valtab'][i]['pstr']) ))
            elif tag == 'tag_sequence':
                for i in xrange(0, count):
                    result.append( ( '#%d' % i, deref(value['valtab'][i]['pseq']) ))
            return result
    
    def typefilter(value):
        "Pick a pretty-printer for 'value'."
        typename = str(value.type.strip_typedefs().unqualified())
    
        if typename == 'char []':
            return cstringprinter(value)
    
        if (typename == 'struct boxint_st' or
            typename == 'struct boxstring_st' or
            typename == 'struct boxsequence_st'):
            tag = str(value['tag'])
            if tag == 'tag_int':
                return boxintprinter(value)
            if tag == 'tag_string':
                return boxstringprinter(value)
            if tag == 'tag_sequence':
                return boxsequenceprinter(value)
    
        return None
    
    gdb.pretty_printers.append(typefilter)
    

    我的选择背后的原因如下:

    1. 如何在 gdb 中安装漂亮的打印机

      这个问题有两个部分:在哪里安装 Python 文件,以及如何将漂亮的打印机连接到 gdb。

      由于漂亮的打印机选择不能仅依赖于推断类型,而是必须窥视实际的数据字段,所以不能使用正则表达式匹配函数。相反,我选择将我自己的漂亮打印机选择器函数typefilter() 添加到全局漂亮打印机列表中,如in the documentation 所述。我没有实现启用/禁用功能,因为我相信只加载/不加载相关的 Python 脚本会更容易。

      typefilter() 每个变量引用都会调用一次,除非其他漂亮打印机已经接受它。)

      文件位置问题是一个更复杂的问题。对于特定于应用程序的漂亮打印机,将它们放入单个 Python 脚本文件听起来很明智,但对于一个库,一些拆分似乎是有序的。文档 recommends 将函数打包到 Python 模块中,以便简单的 python import module 启用漂亮打印机。幸运的是,Python 打包非常简单。如果您要将import gdb 放在顶部并保存到/usr/lib/pythonX.Y/tiny.py,其中X.Y 是使用的python 版本,您只需在gdb 中运行python import tiny 即可启用漂亮打印机。

      当然,正确地packaging 漂亮的打印机是一个非常好的主意,特别是如果你打算分发它,但它确实可以归结为在脚本的开头添加一些变量等等,假设你将其保存为单个文件。对于更复杂的漂亮打印机,使用目录布局可能是个好主意。

    2. 如果你有一个值val,那么val.type就是描述其类型的gdb.Type对象;将其转换为字符串会产生人类可读的类型名称。

      val.type.strip_typedefs() 产生所有类型定义被剥离的实际类型。我什至添加了.unqualified(),以便所有 const/volatile/etc.类型限定符已删除。

    3. 空指针检测有点棘手。

      我发现的最好方法是检查目标 gdb.Value 对象的字符串化 .address 成员,看看它是否是 "0x0"

      为了让生活更轻松,我编写了一个简单的deref() 函数,它试图取消对指针的引用。如果目标指向(void *)0,则返回字符串"NULL",否则返回目标gdb.Value对象。

      我使用deref() 的方式是基于"array" type pretty-printers 产生一个2元组列表,其中第一项是名称字符串,第二项是gdb.Value对象或字符串。此列表由 pretty-printer 对象的 children() 方法返回。

    4. 如果通用实体有单独的类型,则处理“可区分联合”类型会容易得多。也就是说,如果你有

      struct box_st {
          enum tag_en tag;
      };
      

      tag 的值仍然不确定时,它无处不在;并且特定结构类型仅在其tag 值固定的情况下使用。这将允许更简单的类型推断。

      事实上,在tiny.c 中,struct box*_st 类型可以互换使用。 (或者,更具体地说,我们不能仅根据类型依赖特定的标记值。)

      序列的情况其实很简单,因为valtab[] 可以被简单地视为一个空指针数组。序列标签用于选择正确的联合成员。事实上,如果 valtab[] 只是一个 void 指针数组,那么 gdb.Value.cast(gdb.lookup_type()) 或 gdb.Value.reinterpret_cast(gdb.lookup_type()) 可用于根据需要更改每个指针类型,就像我对盒装结构类型所做的那样。

    5. 递归限制?

      您可以在print 命令中使用@ 运算符来指定打印多少元素,但这对嵌套没有帮助。

      如果将iseq3-&gt;valtab[2] = (myval_t)iseq3; 添加到tiny.c,则会得到一个无限递归序列。 gdb 确实可以很好地打印它,尤其是使用 set print array,但它不会注意到或关心递归。

    在我看来,除了用于深度嵌套或递归数据结构的漂亮打印机之外,您可能还希望编写 gdb 命令。在我的测试过程中,我编写了一个使用 Graphviz 直接从 gdb 中绘制二叉树结构的命令;我绝对相信它胜过纯文本输出。

    补充:如果将以下内容另存为/usr/lib/pythonX.Y/tree.py

    import subprocess
    import gdb
    
    def pretty(value, field, otherwise=''):
        try:
            if str(value[field].type) == 'char []':
                data = str(gdb.selected_inferior().read_memory(value[field].address, 64))
                try:
                    size = data.index("\0")
                    return '\\"%s\\"' % data[0:size].encode('string_escape').replace('"', '\\"').replace("'", "\\'")
                except:
                    return '\\"%s\\"..' % data.encode('string_escape').replace('"', '\\"').replace("'", "\\'")
            else:
                return str(value[field])
        except:
            return otherwise
    
    class tee:
        def __init__(self, cmd, filename):
            self.file = open(filename, 'wb')
            gdb.write("Saving DOT to '%s'.\n" % filename)
            self.cmd = cmd
        def __del__(self):
            if self.file is not None:
                self.file.flush()
                self.file.close()
                self.file = None
        def __call__(self, arg):
            self.cmd(arg)
            if self.file is not None:
                self.file.write(arg)
    
    def do_dot(value, output, visited, source, leg, label, left, right):
        if value.type.code != gdb.TYPE_CODE_PTR:
            return
        target = value.dereference()
    
        target_addr = int(str(target.address), 16)
        if target_addr == 0:
            return
    
        if target_addr in visited:
            if source is not None:
                path='%s.%s' % (source, target_addr)
                if path not in visited:
                    visited.add(path)
                    output('\t"%s" -> "%s" [ taillabel="%s" ];\n' % (source, target_addr, leg))
            return
    
        visited.add(target_addr)
    
        if source is not None:
            path='%s.%s' % (source, target_addr)
            if path not in visited:
                visited.add(path)
                output('\t"%s" -> "%s" [ taillabel="%s" ];\n' % (source, target_addr, leg))
    
        if label is None:
            output('\t"%s" [ label="%s" ];\n' % (target_addr, target_addr))
        elif "," in label:
            lab = ''
            for one in label.split(","):
                cur = pretty(target, one, '')
                if len(cur) > 0:
                    if len(lab) > 0:
                        lab = '|'.join((lab,cur))
                    else:
                        lab = cur
            output('\t"%s" [ shape=record, label="{%s}" ];\n' % (target_addr, lab))
        else:
            output('\t"%s" [ label="%s" ];\n' % (target_addr, pretty(target, label, target_addr)))
    
        if left is not None:
            try:
                target_left = target[left]
                do_dot(target_left, output, visited, target_addr, left, label, left, right)
            except:
                pass
    
        if right is not None:
            try:
                target_right = target[right]
                do_dot(target_right, output, visited, target_addr, right, label, left, right)
            except:
                pass
    
    class Tree(gdb.Command):
    
        def __init__(self):
            super(Tree, self).__init__('tree', gdb.COMMAND_DATA, gdb.COMPLETE_SYMBOL, False)
    
        def do_invoke(self, name, filename, left, right, label, cmd, arg):
            try:
                node = gdb.selected_frame().read_var(name)
            except:
                gdb.write('No symbol "%s" in current context.\n' % str(name))
                return
            if len(arg) < 1:
                cmdlist = [ cmd ]
            else:
                cmdlist = [ cmd, arg ]
            sub = subprocess.Popen(cmdlist, bufsize=16384, stdin=subprocess.PIPE, stdout=None, stderr=None)
            if filename is None:
                output = sub.stdin.write
            else:
                output = tee(sub.stdin.write, filename)
            output('digraph {\n')
            output('\ttitle = "%s";\n' % name)
            if len(label) < 1: label = None
            if len(left)  < 1: left  = None
            if len(right) < 1: right = None
            visited = set((0,))
            do_dot(node, output, visited, None, None, label, left, right)
            output('}\n')
            sub.communicate()
            sub.wait()
    
        def help(self):
            gdb.write('Usage: tree [OPTIONS] variable\n')
            gdb.write('Options:\n')
            gdb.write('   left=name          Name member pointing to left child\n')
            gdb.write('   right=name         Name right child pointer\n')
            gdb.write('   label=name[,name]  Define node fields\n')
            gdb.write('   cmd=dot arg=-Tx11  Specify the command (and one option)\n')
            gdb.write('   dot=filename.dot   Save .dot to a file\n')
            gdb.write('Suggestions:\n')
            gdb.write('   tree cmd=neato variable\n')
    
        def invoke(self, argument, from_tty):
            args = argument.split()
            if len(args) < 1:
                self.help()
                return
            num = 0
            cfg = { 'left':'left', 'right':'right', 'label':'value', 'cmd':'dot', 'arg':'-Tx11', 'dot':None }
            for arg in args[0:]:
                if '=' in arg:
                    key, val = arg.split('=', 1)
                    cfg[key] = val
                else:
                    num += 1
                    self.do_invoke(arg, cfg['dot'], cfg['left'], cfg['right'], cfg['label'], cfg['cmd'], cfg['arg'])
            if num < 1:
                self.help()
    
    Tree()
    

    你可以在 gdb 中使用它:

    (gdb) python import tree
    (gdb) tree
    Usage: tree [OPTIONS] variable
    Options:
       left=name          Name member pointing to left child
       right=name         Name right child pointer
       label=name[,name]  Define node fields
       cmd=dot arg=-Tx11  Specify the command (and one option)
       dot=filename.dot   Save .dot to a file
    Suggestions:
       tree cmd=neato variable
    

    如果你有例如

    struct node {
        struct node *le;
        struct node *gt;
        long         key;
        char         val[];
    }
    
    struct node *sometree;
    

    并且您已经安装了 X11(本地或远程)连接和 Graphviz,您可以使用

    (gdb) tree left=le right=gt label=key,val sometree
    

    查看树形结构。因为它保留了一个已经访问过的节点列表(作为一个 Python 集),所以它不会对递归结构感到担心。

    我可能应该在发布之前清理我的 Python sn-ps,但没关系。请务必考虑这些只是初始测试版本;使用风险自负。 :)

    【讨论】:

    • 非常感谢您的回复。我还没有测试过(因为目前我不需要gdb 来调试),但是信息量很大!
    猜你喜欢
    • 2015-08-27
    • 1970-01-01
    • 2012-09-16
    • 2019-09-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多