使用 gdb PrettyPrinting API 让生活更美好
任何翻阅过 gdb 手册的人都知道 gdb 有某种 Python API。而任何粗略浏览过的人都会看到一些叫做“Pretty Printing”的东西,它据说能告诉 gdb 如何以一种漂亮且易读的方式打印复杂的数据结构。至少我是看到过这个,但我从未多想。不过,有一天,当我输入了
(gdb) p/t table->read_set->bitmap[0] @ (table->read_set->n_bits+7)/8
无数次之后,我问自己:“为什么不呢?”,于是就开始了……
现在,当然,Python 中的 Pretty Printer 是一个类。要打印的值被传递给构造函数,而且不仅仅是简单的标量值,而是一个 gdb.Value
对象。之后 gdb 调用这个类的 to_string()
方法来获取实际的字符串,以便展示给用户。例如,几个 MariaDB 内部类的 pretty printers 可能看起来像这样:
class BitmapPrinter: def __init__(self, val): self.val = val def to_string(self): s='' for i in range((self.val['n_bits']+7)//8): s = format(int(self.val['bitmap'][i]), '032b') + s return "b'" + s[-int(self.val['n_bits']):] + "'" class StringPrinter: def __init__(self, val): self.val = val def to_string(self): return '_' + self.val['str_charset']['name'].string() + \ ' "' + self.val['Ptr'].string('ascii', 'strict', self.val['str_length']) + '"'
这还不全。还应该有一个可调用类,用来告诉 gdb 使用哪个 pretty printer。但无需从头编写;gdb.printing
提供了一个 RegexpCollectionPrettyPrinter
类,它通过正则表达式匹配类型来实现这一点。对于上面的 pretty printers,它可以像这样使用:
import gdb.printing def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter( "my_library") pp.add_printer('String', '^String$', StringPrinter) pp.add_printer('bitmap', '^st_bitmap$', BitmapPrinter) return pp gdb.printing.register_pretty_printer( gdb.current_objfile(), my_library.build_pretty_printer()
这样可以。但这对我来说太冗长了。我讨厌样板代码。而这里的 pretty printer 类是多余的;它们唯一有用的部分是 to_string()
方法。注册也需要输入太多。甚至要 pretty print 的类型在那里重复了三次!
而且它不支持指针。需要为 String *
创建一个单独的 pretty printer,尽管它只会添加打印地址,其余代码是相同的。复制粘贴太多了。这还只是两个 pretty printers,我预计会有很多。
它也不处理 typedef;需要在 pp.add_printer
中使用所有 typedef 解析后的基本类型。这对于以下情况帮助不大:
typedef unsigned long long sql_mode_t
我想要指针,我想要 typedef,而且我不想写样板代码。理想情况下,我想像这样编写 pretty printers:
@PrettyPrinter def String(val): return '_' + val['str_charset']['name'].string() + \ ' "' + val['Ptr'].string('ascii', 'strict', val['str_length']) + '"' @PrettyPrinter def st_bitmap(val): s='' for i in range((val['n_bits']+7)//8): s = format(int(val['bitmap'][i]), '032b') + s return "b'" + s[-int(val['n_bits']):] + "'"
就是这样,只有打印函数和装饰器。没有废话。这就是我所做的。它是这样工作的:
装饰器将类型名称设为与函数名称相同。并为该类型注册一个 pretty printer。这个 pretty printer 类将在构造函数中接受函数(当然还有值)作为参数,并在其 to_string()
方法中,它将简单地对该值调用所述函数。因为这个类仅仅封装了实际的 pretty printing 函数,我称之为 PrettyPrinterWrapper
。
现在,我不喜欢 gdb.printing.RegexpCollectionPrettyPrinter
,所以我自己也创建了一个可调用类。它检查值的 typedef 类型和基本类型,因此 sql_mode_t
可以正确打印。而且它支持指针——如果该值是指向某个可以 pretty print 的东西的指针,它将打印地址,gdb 风格,然后对解引用的值调用 pretty printer。因为它封装了 PrettyPrinterWrapper
,我觉得称这个类为 PrettyPrinterWrapperWrapper
会很有趣。
那时我没有想到的一点是,我无法创建一个名为 Alter_inplace_info::HA_ALTER_FLAGS
的 Python 函数。我必须实现一个变通方法:
@PrettyPrinter('Alter_inplace_info::HA_ALTER_FLAGS') def Aii_HA_ALTER_FLAGS(val): s='' ...
而在 Python 中实现这一点的途径是,将装饰器封装在另一个函数中,该函数将接受字符串参数并应返回实际的装饰器,用于装饰函数。那时我意识到了自己的错误,但为时已晚。那个封装 PrettyPrinterWrapperWrapper
的函数不得不被命名为 PrettyPrinterWrapperWrapperWrapper
。幸运的是,之后一切都开始顺利进行,并且 wrapper-crazyness 没有进一步发展。
现在我可以轻松编写 pretty printers,无需复制粘贴或样板代码:
@PrettyPrinter def sql_mode_t(val): s='' modes=['STRICT_TRANS_TABLES', 'STRICT_ALL_TABLES', 'NO_ZERO_IN_DATE', 'TRADITIONAL', 'NO_AUTO_CREATE_USER', 'HIGH_NOT_PRECEDENCE', 'NO_ENGINE_SUBSTITUTION', 'PAD_CHAR_TO_FULL_LENGTH'] for i in range(0,len(modes)): if val & (1 << i): s += ',' + modes[i] return s[1:]
并享受 pretty printed 结构的优雅,这当然在调试 MariaDB 中非常重要。
(gdb) p table->alias $1 = _binary "t3" (gdb) p &table->alias $2 = (String *) 0x7fffd409c250 _binary "t3" (gdb) p table->read_set[0] $3 = b'10011' (gdb) p thd->variables.sql_mode $4 = STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION (gdb) p ha_alter_info.handler_flags $5 = ADD_INDEX,DROP_INDEX,ADD_PK_INDEX,ALTER_STORED_COLUMN_ORDER
完整的实现在此。免责声明:如果您想将此装饰器用于您的 pretty printers,请注意,它主要是 Python 2。可能需要一些调整才能在 Python 3 中工作。欢迎提交补丁。