使用Python在gdb中美化打印递归结构
我对Python不是很熟悉,现在正在探索GDB的Python脚本功能。我问这个问题的目的是想改善GDB在打印值时的表现,特别是在使用MELT监控器时,之后这个监控器会和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
我现在遇到了一些基本问题。
- 我该如何在GDB中安装我的美化打印工具?(我在文档中看到好几种方法,但不知道该选哪一个)。
- 如何确保GDB以相同的方式美化打印
union my_un
和它的类型别名myval_t
? - 我的美化打印工具应该如何检测NULL指针?
- 我的美化打印工具如何处理
struct boxsequence_st
的递归?这意味着要检测指针是否有效,然后解引用它的ptag
,将这个标签与tag_sequence
进行比较,并美化打印valtab
这个灵活数组成员。 - 如何避免在美化打印时递归得太深?
1 个回答
我对gdb的Python接口经验不够,不能算是一个正式的答案;我觉得这只是一个开发者的研究笔记。下面附上的代码也很粗糙和丑陋。不过,这段代码在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)
我选择的理由如下:
如何将漂亮打印器安装到gdb中?
这个问题有两个部分:一是Python文件放在哪里,二是如何将漂亮打印器连接到gdb。
因为选择漂亮打印器不能仅依赖推断的类型,还需要查看实际的数据字段,所以不能使用常规的正则表达式匹配函数。因此,我选择将自己的漂亮打印器选择函数
typefilter()
添加到全局的漂亮打印器列表中,具体描述可以在文档中找到。我没有实现启用/禁用功能,因为我觉得直接加载或不加载相关的Python脚本更简单。(
typefilter()
会在每次引用变量时被调用,除非其他漂亮打印器已经接受了它。)文件位置的问题比较复杂。对于特定应用的漂亮打印器,把它们放在一个Python脚本文件中听起来很合理,但对于库来说,似乎需要一些拆分。文档建议将函数打包成一个Python模块,这样简单的
python import module
就能启用漂亮打印器。幸运的是,Python打包相对简单。如果你在顶部import gdb
并将其保存到/usr/lib/pythonX.Y/tiny.py
,其中X.Y
是使用的Python版本,那么只需在gdb中运行python import tiny
就能启用漂亮打印器。当然,正确地打包漂亮打印器是个好主意,尤其是如果你打算分发它,但如果你保持为单个文件,基本上只需在脚本开头添加一些变量等。对于更复杂的漂亮打印器,使用目录结构可能是个好主意。
如果你有一个值
val
,那么val.type
是一个描述其类型的gdb.Type对象;将其转换为字符串会得到一个人类可读的类型名称。val.type.strip_typedefs()
会返回实际类型,去掉所有typedefs。我甚至添加了.unqualified()
,这样所有的const/volatile等类型限定符都会被移除。检测NULL指针有点棘手。
我找到的最好方法是检查目标gdb.Value对象的字符串化
.address
成员,看看它是否是"0x0"
。为了简化操作,我写了一个简单的
deref()
函数,尝试解引用一个指针。如果目标指向(void *)0,它返回字符串"NULL"
,否则返回目标gdb.Value对象。我使用
deref()
的方式是基于这样的事实:"array"
类型的漂亮打印器返回一个包含2元组的列表,第一个项目是名称字符串,第二个项目是gdb.Value对象或字符串。这个列表由漂亮打印器对象的children()
方法返回。处理“带标识的联合体”类型会简单得多,如果你有一个单独的类型来表示通用实体。也就是说,如果你有
struct box_st { enum tag_en tag; };
并且在
tag
值仍不确定时到处使用;而具体的结构类型只在其tag
值固定时使用。这将允许更简单的类型推断。实际上,在
tiny.c
中,struct box*_st
类型可以互换使用。(更具体地说,我们不能仅仅依靠类型来推断特定的标识值。)序列情况实际上很简单,因为
valtab[]
可以简单地视为一个void指针数组。序列标识用于选择正确的联合体成员。事实上,如果valtab[]只是一个void指针数组,那么可以使用gdb.Value.cast(gdb.lookup_type())或gdb.Value.reinterpret_cast(gdb.lookup_type())根据需要更改每个指针类型,就像我对盒装结构类型所做的那样。递归限制?
你可以在
print
命令中使用@
运算符来指定打印多少个元素,但这对嵌套没有帮助。如果你在
tiny.c
中添加iseq3->valtab[2] = (myval_t)iseq3;
,你会得到一个无限递归的序列。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代码片段,但没关系。请将这些视为初步测试版本;使用时请自行承担风险。:)