决斗:gdb 对战链表、树和哈希表

我第一次接触 gdb 命令 duel 是在大约 15 年前,在一个旧的 IRIX 系统上。我立刻喜欢上了它在 MySQL 调试期间显示各种数据结构的便捷性,并希望 Linux 上也有类似的东西。后来我发现 Duel 并非 IRIX 特有的东西,而是 Michael Golan 在 93 年为 gdb 4.6 编写的一个公共领域补丁。不幸的是,它从未被纳入 gdb 中(我听说是因为许可原因)。现在 gdb 8 已经发布,这个补丁显然不再适用。我没有去修复这个补丁,而是使用 gdb Python APIArpeggio 解析器,用 Python 重新实现了 Duel。现在它可以运行时加载到你的 gdb 中,无需打补丁或编译。而且,希望能兼容多个 gdb 版本,因为它不再依赖于 gdb 未公开的内部细节。所以,请允许我介绍… gdb 版的 Duel.py

示例

让我们从几个例子开始,尝尝它的滋味。Michael Golan 成功地创造了一种简洁但非常自然易懂的语言。例如,虽然 a->b 表示结构体指针 a 的元素 b,但在 Duel 中,a-->b 表示遍历链表,生成一系列值 aa->ba->b->b 等等。在 MariaDB 中,查询的所有表都存储在一个链表中,只需一个命令即可打印出来

(gdb) dl table_list-->next_local->table_name
tables->table_name = 0x7fffc40126b8 "t2"
tables->next_local->table_name = 0x7fffc4012d18 "t1"
tables-->next_local[[2]]->table_name = 0x7fffc4013388 "t1"

同样非常自然地,你可以不使用数字,而是指定一个范围,这对于打印数组非常有用

(gdb) dl longopts[0..405].name
longopts[0].name = "help"
longopts[1].name = "allow-suspicious-udfs"
<... cut ...>
longopts[404].name = "session_track_transaction_info"
longopts[405].name = "session_track_state_change"

你也可以不使用任何表达式,而是指定一组表达式

(gdb) dl my_long_options[1..3].(name,def_value)
my_long_options[1].(name) = "allow-suspicious-udfs"
my_long_options[1].(def_value) = 0
my_long_options[2].(name) = "ansi"
my_long_options[2].(def_value) = 0
my_long_options[3].(name) = "autocommit"
my_long_options[3].(def_value) = 1

概念

Duel 的构建理念是每个表达式都可以返回多个值。你已经看到了 a..ba-->ba,b 作为这类表达式的例子。任何此类表达式都可以与其他操作符一起正常使用。例如

(gdb) dl (1..3)+(100,200)
(1) + (100) = 101
(1) + (200) = 201
(2) + (100) = 102
(2) + (200) = 202
(3) + (100) = 103
(3) + (200) = 203

除了生成多个值的操作符外,还有停止生成的操作符以及类似 grep 的过滤已生成值的操作符。让我们仔细看看。

操作符

Duel 的语法类似 C 语言,C 操作符按预期工作,可以使用 C 风格的类型转换,引用变量并调用被调试程序(inferior)的函数。但此外,还可以使用处理多个值的特殊 Duel 操作符。

范围和列表,..,

你已经看到了上面的例子。这些操作符看起来相当熟悉,它们也存在于其他语言中。一个范围可以指定两端或只指定一端,..x 表示包含 x 个元素的范围,等同于 0..x-1。开放范围 x.. 将无限生成值(或者直到计数器溢出,以先发生的为准),通常与 @ “直到”操作符一起使用。

直到,@

x@y 表达式中,x 将持续生成值直到 y 变为真。例如,

(gdb) dl arr[0..]@(count > 10)

将打印 arr 数组的元素,直到 arr[i].count 大于十。作为一个方便的快捷方式,第二个参数可以是常量,在这种情况下,生成的值一旦等于此常量就会停止。例如,打印所有命令行参数

(gdb) dl argv[0..]@0
argv[0] = "./mysqld"
argv[1] = "--log-output=file"
argv[2] = "--gdb"
argv[3] = "--core-file"

虽然也可以用以下方式实现

(gdb) dl argv[..argc]
argv[0] = "./mysqld"
argv[1] = "--log-output=file"
argv[2] = "--gdb"
argv[3] = "--core-file"

遍历链表,-->

操作符 x-->y 生成多个值:xx->yx->y->yx->y->y->y 等等,直到遇到 NULL。这显然可以用来在一行命令中打印整个链表。但它对树同样适用

(gdb) dl tree->(left,right)->data

评估花括号,{ }

花括号的作用就像圆括号,但它们在输出中用表达式的值替换表达式本身。用例子解释更容易,假设 i = 5

(gdb) dl i+6
i+6 = 11
(gdb) dl {i}+6
5+6 = 11
(gdb) dl {i+6}
11 = 11

这在涉及数组索引的复杂表达式中非常有用

(gdb) dl if (my_long_options[i:=1..20].name[0] == 'd') my_long_options[i].name
if(my_long_options[i].name[0] == 'd') my_long_options[i].name = "debug-abort-slave-event-count"
if(my_long_options[i].name[0] == 'd') my_long_options[i].name = "debug-assert-on-error"
if(my_long_options[i].name[0] == 'd') my_long_options[i].name = "debug-assert-if-crashed-table"
if(my_long_options[i].name[0] == 'd') my_long_options[i].name = "debug-disconnect-slave-event-count"
if(my_long_options[i].name[0] == 'd') my_long_options[i].name = "debug-exit-info"
(gdb) dl if (my_long_options[i:=1..20].name[0] == 'd') my_long_options[{i}].name 
if(my_long_options[i].name[0] == 'd') my_long_options[16].name = "debug-abort-slave-event-count"
if(my_long_options[i].name[0] == 'd') my_long_options[17].name = "debug-assert-on-error"
if(my_long_options[i].name[0] == 'd') my_long_options[18].name = "debug-assert-if-crashed-table"
if(my_long_options[i].name[0] == 'd') my_long_options[19].name = "debug-disconnect-slave-event-count"
if(my_long_options[i].name[0] == 'd') my_long_options[20].name = "debug-exit-info"

注意,在第二个例子中,花括号如何让我们看到哪些数组元素满足条件。

过滤器,<?, >?, <=?, >=?, ==?, !=?

这些操作符有点像 grep,它们选择满足条件的左侧参数的值。换句话说,x ==? y 的结果是 x(如果 x 等于 y),否则没有任何结果。前面那个相当复杂的例子可以使用过滤器来简化

(gdb) dl my_long_options[1..20].(name[0] ==? 'd' => name)
my_long_options[16].(name) = "debug-abort-slave-event-count"
my_long_options[17].(name) = "debug-assert-on-error"
my_long_options[18].(name) = "debug-assert-if-crashed-table"
my_long_options[19].(name) = "debug-disconnect-slave-event-count"
my_long_options[20].(name) = "debug-exit-info"

其他操作符

还有别名(:=)、分组操作符(#/ 等)、条件 if 操作符,以及其他一些不常用的东西。

进阶

手册中的更多示例。

打印循环链表(从 head 开始,沿着 next 指针直到再次看到 head

dl head-->(next!=?head)

找到数组中的第三个正数元素(使用 [[ ]] 操作符,它从序列中选择一个值)

(gdb) dl (x[0..] >? 0)[[2]]

找到链表中的最后一个元素(使用计数器 #/ 操作符,它返回序列中的值数量)

(gdb) head-->next[[#/head-->next - 1]]

检查数组是否已排序

(gdb) dl x[i:=..100] >? x[i+1]

总结 (即太长不看版)

gdb 中的 Duel 又可以用了(24 年后)。它在调试比“Hello, world”更复杂的程序时非常有用。实际上,你只需要记住四个构造:..、`,`、-->@0

你可以从我的仓库获取它:https://github.com/vuvova/gdb-tools
免责声明:尽管 Duel 本身是一种非常稳定且经过时间检验的语言,但 Duel.py 是新的,请预期可能存在错误。