决斗:gdb 对战链表、树和哈希表
我第一次接触 gdb 命令 duel
是在大约 15 年前,在一个旧的 IRIX 系统上。我立刻喜欢上了它在 MySQL 调试期间显示各种数据结构的便捷性,并希望 Linux 上也有类似的东西。后来我发现 Duel 并非 IRIX 特有的东西,而是 Michael Golan 在 93 年为 gdb 4.6 编写的一个公共领域补丁。不幸的是,它从未被纳入 gdb 中(我听说是因为许可原因)。现在 gdb 8 已经发布,这个补丁显然不再适用。我没有去修复这个补丁,而是使用 gdb Python API 和 Arpeggio 解析器,用 Python 重新实现了 Duel。现在它可以运行时加载到你的 gdb 中,无需打补丁或编译。而且,希望能兼容多个 gdb 版本,因为它不再依赖于 gdb 未公开的内部细节。所以,请允许我介绍… gdb 版的 Duel.py!
示例
让我们从几个例子开始,尝尝它的滋味。Michael Golan 成功地创造了一种简洁但非常自然易懂的语言。例如,虽然 a->b
表示结构体指针 a
的元素 b
,但在 Duel 中,a-->b
表示遍历链表,生成一系列值 a
、a->b
、a->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..b
、a-->b
和 a,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
生成多个值:x
、x->y
、x->y->y
、x->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 是新的,请预期可能存在错误。
干得好,让这个非常有用的调试工具重获新生!
我以前从未使用过 Dual,但我下次启动 gdb 时(几天后)肯定会开始使用它
文章中有两个问题不太清楚。
你在文章中提到的
=>
和,,
构造是什么意思?(最好能澄清一下)。
谢谢!
所有操作符都在手册中解释,在这里:https://github.com/vuvova/gdb-tools/blob/arpeggio/duel/help.md
自这篇博文发布以来,我已将 duel 放到了 PyPI 上,所以现在你只需一个命令即可安装它:“pip install gdb-tools”(Python 3 使用“pip3 install gdb-tools”)。无需克隆仓库或下载 tarball – pip 将会下载并安装它,还会处理好依赖关系。
感谢分享。