使用 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 中工作。欢迎提交补丁。