GDB 入门教程:调试 ncurses 相关 bug 的完整范例

这是一篇由 Liam Huang 翻译的译文,原文是 Brendan Gregg 所作的 gdb Debugging Full Example (Tutorial): ncurses。转载请保留本段文字,尊重作者和译者的权益。
The author of this work is Brendan Gregg and this work was firstly posted on gdb Debugging Full Example (Tutorial): ncurses. This is the translation of the original work, by Liam Huang. Please keep this information at the very top of your reprint, for the rights of the author and the translator.

当我尝试在网上寻找「GDB 范例」时,我发现大多数文章只是贴出了命令,而没有讲解相关输出。GDB 是 GNU 调试器(GNU Debugger),亦是 Linux 系统上的标准调试器。在听 Greg Law 在 CppCon 2015 上关于 GDB 的演讲时(Give me 15 minutes and I’ll change your view of GDB),我发现 Law 给出了相关输出,从而意识到了上述不足。

Law 的演讲,也让我意识到应该分享一个使用 GDB 解决问题的完整范例:包括命令输出、各个步骤,以及一些死胡同。也就是说,这篇文章将分享使用 GDB 调试查错的一般步骤,而不是其他特别的东西。这篇文章介绍了 GDB 的基本使用方法,因此可以作为教程使用。不过,还有很多东西没有介绍,请谨记在心。

以下命令,均是以 root 权限执行的。这是因为,我调试的程序,目前需要以 root 权限执行。在实际使用中,并不是所有所需的命令都需要 root 权限。此外,本文罗列了解决问题的每一个步骤,你可以只浏览你感兴趣的部分。

一 · 问题来了

bcc 工具集是 BPF 工具箱的一部分。有人提起了一个 PR (Pull Request),修改了其中的 cachetop 以使用 top-like display 显示 page cache 的统计。当我对这个 PR 进行测试时,程序提示段错误(segfault)

1
2
# ./cachetop.py
Segmentation fault

需要注意的是,Linux 提示程序遇到「段错误 Segmentation fault」,而不是
「段错误(核心已转储)Segmentation fault (Core Dumped)」。而此处,我希望能通过核心转储文件,对错误进行调试。(核心转储文件是进程内存空间的拷贝,因此可以用于调试;这个术语源自早年的磁性核心记忆体)

分析核心转储文件,是调试查错的手段之一。不过,也有其他的调试办法。比如,在程序执行时,使用 GDB 检查问题;又或者使用外部工具,收集段错误发生时的数据和堆栈信息。不过,此处我们从核心转储文件的分析开始。

二 · 获得核心转储文件

首先,我需要检查一下关于核心转储的设置。

1
2
3
4
# ulimit -c
0
# cat /proc/sys/kernel/core_pattern
core

ulimit -c 是 Linux 的系统设置之一,用于限制生成核心转储文件的最大体积。此处,提示不允许转储进程内存。

另一方面,/proc/sys/kernel/core_pattern 的内容是 core。这意味着,发生核心转储时,Linux 会将进程内存空间转储到当前目录下名为 core 的文件当中。对于解决当前问题,这样的设置没什么问题。不过,我会希望将其存在某个固定的目录中。

1
2
3
# ulimit -c unlimited
# mkdir /var/cores
# echo "/var/cores/core.%e.%p" > /proc/sys/kernel/core_pattern

你也可以进一步调整 core_pattern。比如说,%h 表示机器名称,%t 表示转储发生的时间。这些 Linux 内核参数的文档在这里

如果想要永久更改 core_pattern 的设置,你可以修改 /etc/sysctl.conf 文件中的 kernel.core_pattern

如此,再试试看。

1
2
3
4
5
6
7
8
# ./cachetop.py
Segmentation fault (core dumped)
# ls -lh /var/cores
total 19M
-rw------- 1 root root 20M Aug 7 22:15 core.python.30520
# file /var/cores/core.python.30520
/var/cores/core.python.30520: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from 'python ./cachetop.py'
That's better: we have our core dump.

这样一来,我们就有核心转储文件了。

三 · 启动 GDB

现在,我可以启动 GDB,调试目标程序和核心转储文件了。(这里使用了 `which python` 的方式获取系统中 python 的绝对路径)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# gdb `which python` /var/cores/core.python.30520
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
.
Find the GDB manual and other documentation resources online at:
.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /usr/bin/python...(no debugging symbols found)...done.

warning: core file may not match specified executable file.
[New LWP 30520]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

warning: JITed object file architecture unknown is not compatible with target architecture i386:x86-64.
Core was generated by `python ./cachetop.py'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00007f0a37aac40d in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5

最后两行包含有趣的信息:它告诉我们,段错误发生在 libncursesw 库的 doupdate() 函数中。这类问题,可以考虑在网上检索,看看是否是已知问题。当然,我遇到的这个问题,没有其他人报告过。

对我来说,我大概能猜到 libncursesw 是做什么的。不过,如果对你来说这很陌生的话,首先你应该知道 /lib 开头并以 *.so 结尾说明它是一个共享对象(动态库)。接下来,你可以尝试通过 man 命令、网页、软件包说明等方式,弄清楚它是做什么的。

1
2
3
# dpkg -l | grep libncursesw
ii libncursesw5:amd64 6.0+20160213-1ubuntu1 amd64
shared libraries for terminal handling (wide character support)

我这里是在 Ubuntu 上调试,不过,在其他 Linux 发行上调试也基本没差。

四 · 逆向追溯

逆向追溯调用栈,能让我们知道程序是如何一步步调用直到出现问题的。对于一般的问题,逆向追溯调用栈,就足够定位出问题了。因此,我在 GDB 中都会首先尝试使用这一命令:btbacktrace 的缩写)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(gdb) bt
#0 0x00007f0a37aac40d in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5
#1 0x00007f0a37aa07e6 in wrefresh () from /lib/x86_64-linux-gnu/libncursesw.so.5
#2 0x00007f0a37a99616 in ?? () from /lib/x86_64-linux-gnu/libncursesw.so.5
#3 0x00007f0a37a9a325 in wgetch () from /lib/x86_64-linux-gnu/libncursesw.so.5
#4 0x00007f0a37cc6ec3 in ?? () from /usr/lib/python2.7/lib-dynload/_curses.x86_64-linux-gnu.so
#5 0x00000000004c4d5a in PyEval_EvalFrameEx ()
#6 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#7 0x00000000004def08 in ?? ()
#8 0x00000000004b1153 in PyObject_Call ()
#9 0x00000000004c73ec in PyEval_EvalFrameEx ()
#10 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#11 0x00000000004caf42 in PyEval_EvalFrameEx ()
#12 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#13 0x00000000004c2ba9 in PyEval_EvalCode ()
#14 0x00000000004f20ef in ?? ()
#15 0x00000000004eca72 in PyRun_FileExFlags ()
#16 0x00000000004eb1f1 in PyRun_SimpleFileExFlags ()
#17 0x000000000049e18a in Py_Main ()
#18 0x00007f0a3be10830 in __libc_start_main (main=0x49daf0 <main>, argc=2, argv=0x7ffd33d94838, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>,
stack_end=0x7ffd33d94828) at ../csu/libc-start.c:291
#19 0x000000000049da19 in _start ()

这里,自底向上的方向即是调用者 -> 被调用的方向。其中 ?? 表示符号转义失败。遍历调用栈生成栈回溯记录的过程也有可能失败。这种情况下,你可能会看到一个地址值很小的栈帧,当然,这个地址通常是错误的。如果这些问题很严重,那么就要考虑修复这些问题:安装相应软件包的调试信息包(为 GDB 提供更多符号,且允许 GDB 做基于 DWARF 的遍历),或是在编译程序时禁止编译器优化栈帧指针并包含调试信息(-fno-omit-frame-pointer -g)。这里的 ?? 在安装 python-dbg 软件包之后,大都都能解决。

仅从调用栈来看,信息似乎不太够:#5#17 是在 Python 内部,而后经由 _curses 库调用进入 libncursesw。在 libncursesw 中,wgetch()->wrefresh()->doupdate() 调用顺序看起来进行了一次窗口刷新。但是,为什么这会引发核心转储呢?

五 · 反汇编

接下来,我要反汇编导致段错误的函数 doupdate()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(gdb) disas doupdate
Dump of assembler code for function doupdate:
0x00007f0a37aac2e0 <+0>: push %r15
0x00007f0a37aac2e2 <+2>: push %r14
0x00007f0a37aac2e4 <+4>: push %r13
0x00007f0a37aac2e6 <+6>: push %r12
0x00007f0a37aac2e8 <+8>: push %rbp
0x00007f0a37aac2e9 <+9>: push %rbx
0x00007f0a37aac2ea <+10>: sub $0xc8,%rsp
[...]
---Type <return> to continue, or q <return> to quit---
[...]
0x00007f0a37aac3f7 <+279>: cmpb $0x0,0x21(%rcx)
0x00007f0a37aac3fb <+283>: je 0x7f0a37aacc3b <doupdate+2395>
0x00007f0a37aac401 <+289>: mov 0x20cb68(%rip),%rax # 0x7f0a37cb8f70
0x00007f0a37aac408 <+296>: mov (%rax),%rsi
0x00007f0a37aac40b <+299>: xor %eax,%eax
=> 0x00007f0a37aac40d <+301>: mov 0x10(%rsi),%rdi
0x00007f0a37aac411 <+305>: cmpb $0x0,0x1c(%rdi)
0x00007f0a37aac415 <+309>: jne 0x7f0a37aac6f7 <doupdate+1047>
0x00007f0a37aac41b <+315>: movswl 0x4(%rcx),%ecx
0x00007f0a37aac41f <+319>: movswl 0x74(%rdx),%edi
0x00007f0a37aac423 <+323>: mov %rax,0x40(%rsp)
[...]

这里的输出做了适当的截断。(我也可以直接输入 disas,以查看当前函数的汇编代码)

=> 箭头标注的位置,即是产生段错误的指令地址。该指令中(mov 0x10(%rsi),%rdi),%rsi 寄存器保存了一个内存地址,之前的 0x10 表示在这个内存地址的基础上做 0x10 的偏移;%rdi 则是另一个寄存器。该指令的作用,是将上述偏移过的内存地址中保存的内容,拷贝到 %rdi 寄存器当中。接下来,我们要看看寄存器的状态。

六 · 查看寄存器状态

使用命令 i rinfo registers 的缩写)可以打印寄存器状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(gdb) i r
rax 0x0 0
rbx 0x1993060 26816608
rcx 0x19902a0 26804896
rdx 0x19ce7d0 27060176
rsi 0x0 0
rdi 0x19ce7d0 27060176
rbp 0x7f0a3848eb10 0x7f0a3848eb10 <SP>
rsp 0x7ffd33d93c00 0x7ffd33d93c00
r8 0x7f0a37cb93e0 139681862489056
r9 0x0 0
r10 0x8 8
r11 0x202 514
r12 0x0 0
r13 0x0 0
r14 0x7f0a3848eb10 139681870703376
r15 0x19ce7d0 27060176
rip 0x7f0a37aac40d 0x7f0a37aac40d <doupdate+301>
eflags 0x10246 [ PF ZF IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0

好吧,%rsi 这个寄存器中,保存的是一个空指针(0x0)。这就怪不得要出问题了。解引用一个未初始化的指针,或是解引用空指针,是段错误的常见原因。

七 · 内存映射

你可以再确认一下此时 0x0 是否是有效的地址。在 GDB 中使用 i proc minfo proc mappings 的缩写)可以查看内存映射状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) i proc m
Mapped address spaces:

Start Addr End Addr Size Offset objfile
0x400000 0x6e7000 0x2e7000 0x0 /usr/bin/python2.7
0x8e6000 0x8e8000 0x2000 0x2e6000 /usr/bin/python2.7
0x8e8000 0x95f000 0x77000 0x2e8000 /usr/bin/python2.7
0x7f0a37a8b000 0x7f0a37ab8000 0x2d000 0x0 /lib/x86_64-linux-gnu/libncursesw.so.5.9
0x7f0a37ab8000 0x7f0a37cb8000 0x200000 0x2d000 /lib/x86_64-linux-gnu/libncursesw.so.5.9
0x7f0a37cb8000 0x7f0a37cb9000 0x1000 0x2d000 /lib/x86_64-linux-gnu/libncursesw.so.5.9
0x7f0a37cb9000 0x7f0a37cba000 0x1000 0x2e000 /lib/x86_64-linux-gnu/libncursesw.so.5.9
0x7f0a37cba000 0x7f0a37ccd000 0x13000 0x0 /usr/lib/python2.7/lib-dynload/_curses.x86_64-linux-gnu.so
0x7f0a37ccd000 0x7f0a37ecc000 0x1ff000 0x13000 /usr/lib/python2.7/lib-dynload/_curses.x86_64-linux-gnu.so
0x7f0a37ecc000 0x7f0a37ecd000 0x1000 0x12000 /usr/lib/python2.7/lib-dynload/_curses.x86_64-linux-gnu.so
0x7f0a37ecd000 0x7f0a37ecf000 0x2000 0x13000 /usr/lib/python2.7/lib-dynload/_curses.x86_64-linux-gnu.so
0x7f0a38050000 0x7f0a38066000 0x16000 0x0 /lib/x86_64-linux-gnu/libgcc_s.so.1
0x7f0a38066000 0x7f0a38265000 0x1ff000 0x16000 /lib/x86_64-linux-gnu/libgcc_s.so.1
0x7f0a38265000 0x7f0a38266000 0x1000 0x15000 /lib/x86_64-linux-gnu/libgcc_s.so.1
0x7f0a38266000 0x7f0a3828b000 0x25000 0x0 /lib/x86_64-linux-gnu/libtinfo.so.5.9
0x7f0a3828b000 0x7f0a3848a000 0x1ff000 0x25000 /lib/x86_64-linux-gnu/libtinfo.so.5.9
[...]

如此可以看到,虚存空间有效地址的最低位是 0x400000。因此,显而易见 0x0 此时是一个非法的地址,所以使用它会导致段错误。

至此为止,继续深入进行调试的方法有多重。我将从指令的角度继续深入。

八 · 断点

回到刚才的反汇编的结果。

1
2
3
4
   0x00007f0a37aac401 <+289>:   mov    0x20cb68(%rip),%rax        # 0x7f0a37cb8f70
0x00007f0a37aac408 <+296>: mov (%rax),%rsi
0x00007f0a37aac40b <+299>: xor %eax,%eax
=> 0x00007f0a37aac40d <+301>: mov 0x10(%rsi),%rdi

从汇编指令来看,代码似乎首先从栈上将某个信息拷贝到 %rax 寄存器当中。而后解引用 %rax 寄存器里的内容,并拷贝到 %rsi 寄存器。而后通过 xor%eax 寄存器中的内容置零。最后执行到产生段错误的代码。在这里,%eax 有可能提供更多的信息。(译注:原作者写的是 %rax,这应该是手误)但是在段错误之前,它已经被置零了,因此我们看不到更多的信息。

我可以将断点设置在 doupdate+298 处,而后顺着指令单步调试,看看各个寄存器的值是如何变化的。为此,首先我得启动 GDB,以便在其中实时执行程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# gdb `which python`
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86\_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
.
Find the GDB manual and other documentation resources online at:
.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /usr/bin/python...(no debugging symbols found)...done.

而后,使用 b 命令(break 的缩写)设置断点。

1
2
(gdb) b *doupdate + 289
No symbol table is loaded. Use the "file" command.

啊,出错了……这是我有意呈现的错误。通常,我们会将断点首先设置在 main 处,因为彼时符号应该都加载完毕了。而后,在程序遇到断点并暂停时,我们可以设置其他断点。此处,我需要关注 doupdate 函数,因此,我将断点首先设置在这个函数被调用的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) b doupdate
Function "doupdate" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (doupdate) pending.
(gdb) r cachetop.py
Starting program: /usr/bin/python cachetop.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
warning: JITed object file architecture unknown is not compatible with target architecture i386:x86-64.

Breakpoint 1, 0x00007ffff34ad2e0 in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5
(gdb) b *doupdate + 289
Breakpoint 2 at 0x7ffff34ad401
(gdb) c
Continuing.

Breakpoint 2, 0x00007ffff34ad401 in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5

如此,我们就来到了目标断点位置。

这里,r (run) 命令将参数传给被 GDB 跟踪的程序(这里是 python),以便执行目标 Python 脚本。因此,这里相当于执行了 python cachetop.py

⑨ · 单步调试

至此,我使用 si (stepi) 命令,让程序单步地向前执行一条指令,而后检查寄存器状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(gdb) si
0x00007ffff34ad408 in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5
(gdb) i r
rax 0x7ffff3e8f948 140737285519688
rbx 0xaea060 11444320
rcx 0xae72a0 11432608
rdx 0xa403d0 10748880
rsi 0x7ffff7ea8e10 140737352732176
rdi 0xa403d0 10748880
rbp 0x7ffff3e8fb10 0x7ffff3e8fb10 <SP>
rsp 0x7fffffffd390 0x7fffffffd390
r8 0x7ffff36ba3e0 140737277305824
r9 0x0 0
r10 0x8 8
r11 0x202 514
r12 0x0 0
r13 0x0 0
r14 0x7ffff3e8fb10 140737285520144
r15 0xa403d0 10748880
rip 0x7ffff34ad408 0x7ffff34ad408 <doupdate+296>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb) p/a 0x7ffff3e8f948
$1 = 0x7ffff3e8f948 <cur_term>

新的线索出现了。看起来,我们对空指针解引用的操作,发生在一个名为 cur_term 的符号当中。(p/aprint/a 的缩写,其中 /a 表示随后传入的是一个内存地址)考虑到这是在调试 ncurses,那么,是不是我们的 TERM 环境设置有问题呢?

1
2
# echo $TERM
xterm-256color

我尝试将其设为 vt100,但是执行程序依旧得到了相同的段错误。

注意,此处我检查的是 doupdate 函数第一次被调用的情形,但实际上它可能被多次调用,而且问题可能出在后续的调用中。为此,我们可以让程序持续执行(c 命令),直到撞见我们期待的那次调用。然而,调用次数少的话,可能还好。若是调用多次,这就不好办了。(第 15 节将讨论这个问题)

十 · 单步回退

单步回退是 GDB 的重要特性之一,Law 在它的演讲中也有提到。我们看一个例子。

我会重启整个 GDB 会话,从头开始演示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# gdb `which python`
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86\_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /usr/bin/python...(no debugging symbols found)...done.

而后,我将断点设置在 doupdate 函数上;当遇到之后,我会打开记录功能,直到进程崩溃。这种记录功能,对系统影响是很大的。所以,最好不要从 main 函数就开始记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) b doupdate
Function "doupdate" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (doupdate) pending.
(gdb) r cachetop.py
Starting program: /usr/bin/python cachetop.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
warning: JITed object file architecture unknown is not compatible with target architecture i386:x86-64.

Breakpoint 1, 0x00007ffff34ad2e0 in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5
(gdb) record
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff34ad40d in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5

至此,我就可以在代码行或者指令的意义上单步回退了。单步回退的原理,是从记录中,回放寄存器的状态。此处,我回退两个指令,而后打印寄存器状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
(gdb) reverse-stepi
0x00007ffff34ad40d in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5
(gdb) reverse-stepi
0x00007ffff34ad40b in doupdate () from /lib/x86_64-linux-gnu/libncursesw.so.5
(gdb) i r
rax 0x7ffff3e8f948 140737285519688
rbx 0xaea060 11444320
rcx 0xae72a0 11432608
rdx 0xa403d0 10748880
rsi 0x0 0
rdi 0xa403d0 10748880
rbp 0x7ffff3e8fb10 0x7ffff3e8fb10 <SP>
rsp 0x7fffffffd390 0x7fffffffd390
r8 0x7ffff36ba3e0 140737277305824
r9 0x0 0
r10 0x8 8
r11 0x302 770
r12 0x0 0
r13 0x0 0
r14 0x7ffff3e8fb10 140737285520144
r15 0xa403d0 10748880
rip 0x7ffff34ad40b 0x7ffff34ad40b <doupdate+299>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb) p/a 0x7ffff3e8f948
$1 = 0x7ffff3e8f948 <cur_term>

这次确定无疑,我们又一次定位到了 cur_term 这条线索。此时,我很想读读源码,但是首先我会试着看到更多的调试信息。

十一 · 调试信息

这里,我们需要(在 Ubuntu 上)安装 libncursesw 的调试信息包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# apt-cache search libncursesw
libncursesw5 - shared libraries for terminal handling (wide character support)
libncursesw5-dbg - debugging/profiling libraries for ncursesw
libncursesw5-dev - developer's libraries for ncursesw
# dpkg -l | grep libncursesw
ii libncursesw5:amd64 6.0+20160213-1ubuntu1 amd64 shared libraries for terminal handling (wide character support)
# apt-get install -y libncursesw5-dbg
Reading package lists... Done
Building dependency tree
Reading state information... Done
[...]
After this operation, 2,488 kB of additional disk space will be used.
Get:1 http://us-west-1.ec2.archive.ubuntu.com/ubuntu xenial/main amd64 libncursesw5-dbg amd64 6.0+20160213-1ubuntu1 [729 kB]
Fetched 729 kB in 0s (865 kB/s)
Selecting previously unselected package libncursesw5-dbg.
(Reading database ... 200094 files and directories currently installed.)
Preparing to unpack .../libncursesw5-dbg_6.0+20160213-1ubuntu1_amd64.deb ...
Unpacking libncursesw5-dbg (6.0+20160213-1ubuntu1) ...
Setting up libncursesw5-dbg (6.0+20160213-1ubuntu1) ...
# dpkg -l | grep libncursesw
ii libncursesw5:amd64 6.0+20160213-1ubuntu1 amd64 shared libraries for terminal handling (wide character support)
ii libncursesw5-dbg 6.0+20160213-1ubuntu1 amd64 debugging/profiling libraries for ncursesw

赞~!此处的调试信息包与动态库的版本是一致的。现在的段错误看起来是怎样的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# gdb `which python` /var/cores/core.python.30520
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
[...]
warning: JITed object file architecture unknown is not compatible with target architecture i386:x86-64.
Core was generated by `python ./cachetop.py'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 ClrBlank (win=0x1993060) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c:1129
1129 if (back_color_erase)
(gdb) bt
#0 ClrBlank (win=0x1993060) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c:1129
#1 ClrUpdate () at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c:1147
#2 doupdate () at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c:1010
#3 0x00007f0a37aa07e6 in wrefresh (win=win@entry=0x1993060) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/base/lib_refresh.c:65
#4 0x00007f0a37a99499 in recur_wrefresh (win=win@entry=0x1993060) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/base/lib_getch.c:384
#5 0x00007f0a37a99616 in _nc_wgetch (win=win@entry=0x1993060, result=result@entry=0x7ffd33d93e24, use_meta=1)
at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/base/lib_getch.c:491
#6 0x00007f0a37a9a325 in wgetch (win=0x1993060) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/base/lib_getch.c:672
#7 0x00007f0a37cc6ec3 in ?? () from /usr/lib/python2.7/lib-dynload/_curses.x86_64-linux-gnu.so
#8 0x00000000004c4d5a in PyEval_EvalFrameEx ()
#9 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#10 0x00000000004def08 in ?? ()
#11 0x00000000004b1153 in PyObject_Call ()
#12 0x00000000004c73ec in PyEval_EvalFrameEx ()
#13 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#14 0x00000000004caf42 in PyEval_EvalFrameEx ()
#15 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#16 0x00000000004c2ba9 in PyEval_EvalCode ()
#17 0x00000000004f20ef in ?? ()
#18 0x00000000004eca72 in PyRun_FileExFlags ()
#19 0x00000000004eb1f1 in PyRun_SimpleFileExFlags ()
#20 0x000000000049e18a in Py_Main ()
#21 0x00007f0a3be10830 in __libc_start_main (main=0x49daf0 <main>, argc=2, argv=0x7ffd33d94838, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>,
stack_end=0x7ffd33d94828) at ../csu/libc-start.c:291
#22 0x000000000049da19 in _start ()

调用栈看起来稍微有些不一样了。发生段错误的地方,实际是在 ClrBlank() 函数当中。它首先被内联到 ClrUpdate() 当中,最后内联到 doupdate() 当中。

至此,我真的要读读代码了。

十二 · 源代码

有了调试信息包,GDB 可以将源代码和汇编码打印在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(gdb) disas/s
Dump of assembler code for function doupdate:
/build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c:
759 {
0x00007f0a37aac2e0 <+0>: push %r15
0x00007f0a37aac2e2 <+2>: push %r14
0x00007f0a37aac2e4 <+4>: push %r13
0x00007f0a37aac2e6 <+6>: push %r12
[...]
0x00007f0a37aac3dd <+253>: jne 0x7f0a37aac6ca <doupdate+1002>

1009 if (CurScreen(SP_PARM)->_clear || NewScreen(SP_PARM)->_clear) { /* force refresh ? */
0x00007f0a37aac3e3 <+259>: mov 0x80(%rdx),%rax
0x00007f0a37aac3ea <+266>: mov 0x88(%rdx),%rcx
0x00007f0a37aac3f1 <+273>: cmpb $0x0,0x21(%rax)
0x00007f0a37aac3f5 <+277>: jne 0x7f0a37aac401 <doupdate+289>
0x00007f0a37aac3f7 <+279>: cmpb $0x0,0x21(%rcx)
0x00007f0a37aac3fb <+283>: je 0x7f0a37aacc3b <doupdate+2395>

1129 if (back_color_erase)
0x00007f0a37aac401 <+289>: mov 0x20cb68(%rip),%rax # 0x7f0a37cb8f70
0x00007f0a37aac408 <+296>: mov (%rax),%rsi

1128 NCURSES_CH_T blank = blankchar;
0x00007f0a37aac40b <+299>: xor %eax,%eax

1129 if (back_color_erase)
=> 0x00007f0a37aac40d <+301>: mov 0x10(%rsi),%rdi
0x00007f0a37aac411 <+305>: cmpb $0x0,0x1c(%rdi)
0x00007f0a37aac415 <+309>: jne 0x7f0a37aac6f7 <doupdate+1047>

好,依旧有 => 作为出问题指令的标记,以及指令对应的代码打印在其上。那么,程序产生段错误,是因为 if(back_color_erase) 这一行代码吗?看起来似乎不太可能。段错误产生的原因是对指向非法地址的指针进行解引用,例如 a->b 或者 *a。但在此处,back_color_erase 仅只是普通地访问变量,没有解引用的动作,是不会引起段错误的。

为此,我反复检查了调试信息包的版本是否匹配,而后重新在 GDB 里执行程序,但无有收获——段错误发生在同一位置。

那么,是不是 back_color_erase 本身有什么特别之处呢?现在我们在 ClrBlank 函数中,我试着列出它的源代码。

1
2
3
4
5
6
7
8
9
10
11
(gdb) list ClrBlank
1124
1125 static NCURSES_INLINE NCURSES_CH_T
1126 ClrBlank(NCURSES_SP_DCLx WINDOW *win)
1127 {
1128 NCURSES_CH_T blank = blankchar;
1129 if (back_color_erase)
1130 AddAttr(blank, (AttrOf(BCE_BKGD(SP_PARM, win)) & BCE_ATTRS));
1131 return blank;
1132 }
1133

呃……back_color_erase 在函数里是未定义的,看起来是一个全局变量?

十三 · TUI

TUI 是 text user interface(文本用户界面)的缩写。这一界面我甚少使用,我也是听过 Law 的讲座之后受到的启发。

为此,你需要在 GDB 启动的时候,传入 --tui 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# gdb --tui `which python` /var/cores/core.python.30520
┌───────────────────────────────────────────────────────────────────────────┐
│ │
│ │
│ │
│ │
│ │
│ │
│ [ No Source Available ] │
│ │
│ │
│ │
│ │
│ │
│ │
└───────────────────────────────────────────────────────────────────────────┘
None No process In: L?? PC: ??
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
---Type to continue, or q to quit---

GDB 抱怨说没能找到 Python 的源代码。诚然,我可以去解决这个问题,然而,现在进程崩溃的位置是在 libncursesw,费劲去解决这个问题就没必要了。因此,按下回车,使其继续加载,并读入 libncursesw 的调试信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   ┌──/build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c──────┐
1124
1125 static NCURSES_INLINE NCURSES_CH_T │
1126 ClrBlank(NCURSES_SP_DCLx WINDOW *win) │
1127 { │
1128 NCURSES_CH_T blank = blankchar; │
>│1129 if (back_color_erase) │
1130 AddAttr(blank, (AttrOf(BCE_BKGD(SP_PARM, win)) & BCE_ATTRS)│
1131 return blank; │
1132 } │
1133
1134 /* │
1135 ** ClrUpdate() │
1136 ** │
└───────────────────────────────────────────────────────────────────────────┘
multi-thre Thread 0x7f0a3c5e87 In: doupdate L1129 PC: 0x7f0a37aac40d
warning: JITed object file architecture unknown is not compatible with target ar
chitecture i386:x86-64.
---Type <return> to continue, or q <return> to quit---
Core was generated by `python ./cachetop.py'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 ClrBlank (win=0x1993060)
at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c:1129
(gdb)

棒呆!

箭头 > 指向的代码就是程序崩溃的位置。使用 layout split 命令,可以让汇编指令和源代码分开显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
   ┌──/build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c──────┐
>│1129 if (back_color_erase) │
1130 AddAttr(blank, (AttrOf(BCE_BKGD(SP_PARM, win)) & BCE_ATTRS)│
1131 return blank; │
1132 } │
1133
1134 /* │
1135 ** ClrUpdate() │
└───────────────────────────────────────────────────────────────────────────┘
>│0x7f0a37aac40d <doupdate+301> mov 0x10(%rsi),%rdi │
0x7f0a37aac411 <doupdate+305> cmpb $0x0,0x1c(%rdi) │
0x7f0a37aac415 <doupdate+309> jne 0x7f0a37aac6f7 <doupdate+1047> │
0x7f0a37aac41b <doupdate+315> movswl 0x4(%rcx),%ecx │
0x7f0a37aac41f <doupdate+319> movswl 0x74(%rdx),%edi │
0x7f0a37aac423 <doupdate+323> mov %rax,0x40(%rsp) │
0x7f0a37aac428 <doupdate+328> movl $0x20,0x48(%rsp) │
0x7f0a37aac430 <doupdate+336> movl $0x0,0x4c(%rsp) │
└───────────────────────────────────────────────────────────────────────────┘
multi-thre Thread 0x7f0a3c5e87 In: doupdate L1129 PC: 0x7f0a37aac40d

chitecture i386:x86-64.
Core was generated by `python ./cachetop.py'.
Program terminated with signal SIGSEGV, Segmentation fault.
---Type <return> to continue, or q <return> to quit---
#0 ClrBlank (win=0x1993060)
at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c:1129
(gdb) layout split

Law 在单步回退中展示了如何使用 TUI。你可以想象一下,在代码执行的过程中,同时呈现汇编码和源代码是怎样的光景。

十四 · 外部工具:cscope

我依然需要搞清楚 back_color_erase 到底发生了什么。当然,我可以在 GDB 中使用 search 命令,查找 back_color_erase 是在哪里定义的。不过,我更偏好使用名为 cscope 的外部工具。这是一个贝尔实验室在上世纪 80 年代发明的基于文本的代码浏览器。如果你有喜欢的现代 IDE 工具,那么就用你喜欢的就好。

首先,设置 cscope

1
2
3
4
5
6
# apt-get install -y cscope
# wget http://archive.ubuntu.com/ubuntu/pool/main/n/ncurses/ncurses_6.0+20160213.orig.tar.gz
# tar xvf ncurses_6.0+20160213.orig.tar.gz
# cd ncurses-6.0-20160213
# cscope -bqR
# cscope -dq

此处 cscope -bqR 创建了查找数据库,而 cscope -dq 则启动之。

搜索 back_color_erase 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Cscope version 15.8b                                   Press the ? key for help












Find this C symbol:
Find this global definition: back_color_erase
Find functions called by this function:
Find functions calling this function:
Find this text string:
Change this text string:
Find this egrep pattern:
Find this file:
Find files #including this file:
Find assignments to this symbol:

敲回车。

1
2
3
4
5
6
7
8
[...]
#define non_dest_scroll_region CUR Booleans[26]
#define can_change CUR Booleans[27]
#define back_color_erase CUR Booleans[28]
#define hue_lightness_saturation CUR Booleans[29]
#define col_addr_glitch CUR Booleans[30]
#define cr_cancels_micro_mode CUR Booleans[31]
[...]

好吧……这是一个用 #define 定义的宏变量。(就不能大写吗?摔!)

那么,CUR 又是什么呢?我们继续搜索。

1
#define CUR cur_term->type.

好嘛,至少这个 #define 定义的宏是大写的。

碰见 cur_term 了——之前我们在单步调试汇编指令和检查寄存器状态的时候有看到过它。那么它是啥咧?

1
2
3
4
5
6
7
8
#if 0 && !0
extern NCURSES_EXPORT_VAR(TERMINAL *) cur_term;
#elif 0
NCURSES_WRAPPED_VAR(TERMINAL *, cur_term);
#define cur_term NCURSES_PUBLIC_VAR(cur_term())
#else
extern NCURSES_EXPORT_VAR(TERMINAL *) cur_term; // <- here
#endif

cscope/usr/include/term.h 中找到了它。我在我认为真正起作用的定义处,用注释做了标记。为什么会有 if 0 && !0 ... elif 0 这种那奇怪的写法,我也不知道……(但总之要阅读更多的代码)有时,程序员会使用 #if 0 使调试用代码在生产环境中不生效。但这部分代码,看上去是自动生成的。

继续搜索 NCURSES_EXPORT_VAR,会有新的发现。

1
#  define NCURSES_EXPORT_VAR(type) NCURSES_IMPEXP type

以及 NCURSES_IMPEXP……

1
2
3
4
5
6
7
8
typedef struct term {       /* describe an actual terminal */
TERMTYPE type; /* terminal type description */
short Filedes; /* file description being written to */
TTY Ottyb, /* original state of the terminal */
Nttyb; /* current state of the terminal */
int _baudrate; /* used to compute padding */
char * _termname; /* used for termname() */
} TERMINAL;

哈!这回 TERMINAL 是大写的了。在这坨宏当中,这算是容易追踪的了。

好了,现在到底是谁设置了 cur_term 呢?想想看,我们遇到的问题,是因为 cur_term 被设置为 0x0,这可能是没有初始化导致的,也有可能是显式设置导致的。顺着代码检查,可能会有新的线索。

1
2
3
4
5
Find this C symbol: cur_term
Find this global definition:
Find functions called by this function:
Find functions calling this function:
[...]

按下回车,得到结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NCURSES_EXPORT(TERMINAL *)
NCURSES_SP_NAME(set_curterm) (NCURSES_SP_DCLx TERMINAL * termp)
{
TERMINAL *oldterm;

T((T_CALLED("set_curterm(%p)"), (void *) termp));

_nc_lock_global(curses);
oldterm = cur_term;
if (SP_PARM)
SP_PARM->_term = termp;
#if USE_REENTRANT
CurTerm = termp;
#else
cur_term = termp; // <- here
#endif

同样,我对实际生效的部分做了标记。尽管函数名字被包在宏变量当中,但是我们至少知道 cur_term 是如何设置的了——通过 set_curterm() 函数。这个函数是没有被调用吗?

十五 · 外部工具:perf-tools/ftrace/uprobes

我稍后将介绍如何使用 GDB 解决这个问题,但我想试着用我的 perf-tools 工具集当中的 uprobe 工具来解决问题。它使用了 Linux 提供的 ftrace 以及 uprobes 两个工具。使用跟踪器的好处是不需要像 GDB 那样暂停目标进程——当然,在这个例子里这没所谓就是了。此外,使用跟踪器追踪一个事件和追踪成百上千个事件是一样的。

为此,我需要追踪 set_curterm 的调用,并且打印其首个参数。

1
2
# /apps/perf-tools/bin/uprobe 'p:/lib/x86_64-linux-gnu/libncursesw.so.5:set_curterm %di'
ERROR: missing symbol "set_curterm" in /lib/x86_64-linux-gnu/libncursesw.so.5

好吧,在 libncursesw 里没见着有 set_curterm。那么它在哪里呢?我们可以用 GDB 或者 objdump 查找一下。

1
2
3
4
5
6
7
(gdb) info symbol set_curterm
set_curterm in section .text of /lib/x86_64-linux-gnu/libtinfo.so.5

# objdump -tT /lib/x86_64-linux-gnu/libncursesw.so.5 | grep cur_term
0000000000000000 DO *UND* 0000000000000000 NCURSES_TINFO_5.0.19991023 cur_term
# objdump -tT /lib/x86_64-linux-gnu/libtinfo.so.5 | grep cur_term
0000000000228948 g DO .bss 0000000000000008 NCURSES_TINFO_5.0.19991023 cur_term

显而易见,在此处 GDB 更好使些。如果你足够仔细的话,你会发现,这个函数定义在 libtinfo 当中。那么,让我们尝试在 libtinfo 中去追踪 set_curterm

1
2
3
4
5
6
7
# /apps/perf-tools/bin/uprobe 'p:/lib/x86_64-linux-gnu/libtinfo.so.5:set_curterm %di'
Tracing uprobe set_curterm (p:set_curterm /lib/x86_64-linux-gnu/libtinfo.so.5:0xfa80 %di). Ctrl-C to end.
python-31617 [007] d... 24236402.719959: set_curterm: (0x7f116fcc2a80) arg1=0x1345d70
python-31617 [007] d... 24236402.720033: set_curterm: (0x7f116fcc2a80) arg1=0x13a22e0
python-31617 [007] d... 24236402.723804: set_curterm: (0x7f116fcc2a80) arg1=0x14cdfa0
python-31617 [007] d... 24236402.723838: set_curterm: (0x7f116fcc2a80) arg1=0x0
^C

这会没问题了。显而易见,set_curterm 确实是有被调用的,并且被调用了 4 次。在最后一次调用时(之后进程就崩溃了),第一个参数是 0x0,看起来似乎就是问题所在了。

至于为什么要打印 %di 这个寄存器中的值,是因为我们的程序运行在 x86_64 平台。使用 man syscall 可以看到有用的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# man syscall
[...]
arch/ABI arg1 arg2 arg3 arg4 arg5 arg6 arg7 Notes
──────────────────────────────────────────────────────────────────
arm/OABI a1 a2 a3 a4 v1 v2 v3
arm/EABI r0 r1 r2 r3 r4 r5 r6
arm64 x0 x1 x2 x3 x4 x5 -
blackfin R0 R1 R2 R3 R4 R5 -
i386 ebx ecx edx esi edi ebp -
ia64 out0 out1 out2 out3 out4 out5 -
mips/o32 a0 a1 a2 a3 - - - See below
mips/n32,64 a0 a1 a2 a3 a4 a5 -
parisc r26 r25 r24 r23 r22 r21 -
s390 r2 r3 r4 r5 r6 r7 -
s390x r2 r3 r4 r5 r6 r7 -
sparc/32 o0 o1 o2 o3 o4 o5 -
sparc/64 o0 o1 o2 o3 o4 o5 -
x86_64 rdi rsi rdx r10 r8 r9 -
[...]

我还想去看看为什么会在调用 set_curterm 时传入 0x0 作为参数,不过当前 ftrace 不支持调用栈的查询。

十六 · 外部工具:bcc/BPF

考虑到我们正在调试 bcc 工具 cachetop.py,那么使用 bcc 提供的 trace.py 来追踪函数调用是个不错的选择。它和刚才的 uprobe 工具有类似的功能。

1
2
3
4
5
6
# ./trace.py 'p:tinfo:set_curterm "%d", arg1'
TIME PID COMM FUNC -
01:00:20 31698 python set_curterm 38018416
01:00:20 31698 python set_curterm 38396640
01:00:20 31698 python set_curterm 39624608
01:00:20 31698 python set_curterm 0

没错!我们在用 bcc 来调试 bcc

如果你之前未曾使用过 bcc,那么我推荐你试试看。它有面向 Python 和 Lua 的接口,提供了在 Linux 4.x 系列中的 BPF 跟踪特性。简单来说,它让很多以前无法或者难以实现的性能工具变得可行。在 Ubuntu Xenial 上有我关于此的介绍。

十七 · 更多的断点

我本该用 GDB 给 set_curterm 设置断点的,但是绕道去 ftrace 和 BPF 也时很有趣的事情。

回到 GDB。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# gdb `which python`
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
[...]
(gdb) b set_curterm
Function "set_curterm" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (set_curterm) pending.
(gdb) r cachetop.py
Starting program: /usr/bin/python cachetop.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, set_curterm (termp=termp@entry=0xa43150) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
80 {
(gdb) c
Continuing.

Breakpoint 1, set_curterm (termp=termp@entry=0xab5870) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
80 {
(gdb) c
Continuing.

Breakpoint 1, set_curterm (termp=termp@entry=0xbecb90) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
80 {
(gdb) c
Continuing.

Breakpoint 1, set_curterm (termp=0x0) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
80 {

好了,在断点处,我们看到 set_curterm 被传入了参数 termp=0x0。幸好能有这些调试信息,否则,我就不得不在各个断点去打印寄存器状态了。

现在,查看逆向追溯调用栈,应当可以看到是哪个函数将 0x0 传给了 set_curterm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
(gdb) bt
#0 set_curterm (termp=0x0) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
#1 0x00007ffff5a44e75 in llvm::sys::Process::FileDescriptorHasColors(int) () from /usr/lib/x86_64-linux-gnu/libbcc.so.0
#2 0x00007ffff45cabb8 in clang::driver::tools::Clang::ConstructJob(clang::driver::Compilation&, clang::driver::JobAction const&, clang::driver::InputInfo const&, llvm::SmallVector<clang::driver::InputInfo, 4u> const&, llvm::opt::ArgList const&, char const*) const () from /usr/lib/x86_64-linux-gnu/libbcc.so.0
#3 0x00007ffff456ffa5 in clang::driver::Driver::BuildJobsForAction(clang::driver::Compilation&, clang::driver::Action const*, clang::driver::ToolChain const*, char const*, bool, bool, char const*, clang::driver::InputInfo&) const () from /usr/lib/x86_64-linux-gnu/libbcc.so.0
#4 0x00007ffff4570501 in clang::driver::Driver::BuildJobs(clang::driver::Compilation&) const () from /usr/lib/x86_64-linux-gnu/libbcc.so.0
#5 0x00007ffff457224a in clang::driver::Driver::BuildCompilation(llvm::ArrayRef<char const*>) () from /usr/lib/x86_64-linux-gnu/libbcc.so.0
#6 0x00007ffff4396cda in ebpf::ClangLoader::parse(std::unique_ptr<llvm::Module, std::default_delete<llvm::Module> >*, std::unique_ptr<std::vector<ebpf::TableDesc, std::allocator<ebpf::TableDesc> >, std::default_delete<std::vector<ebpf::TableDesc, std::allocator<ebpf::TableDesc> > > >*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, bool, char const**, int) () from /usr/lib/x86_64-linux-gnu/libbcc.so.0
#7 0x00007ffff4344314 in ebpf::BPFModule::load_cfile(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, bool, char const**, int) ()
from /usr/lib/x86_64-linux-gnu/libbcc.so.0
#8 0x00007ffff4349e5e in ebpf::BPFModule::load_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, char const**, int) ()
from /usr/lib/x86_64-linux-gnu/libbcc.so.0
#9 0x00007ffff43430c8 in bpf_module_create_c_from_string () from /usr/lib/x86_64-linux-gnu/libbcc.so.0
#10 0x00007ffff690ae40 in ffi_call_unix64 () from /usr/lib/x86_64-linux-gnu/libffi.so.6
#11 0x00007ffff690a8ab in ffi_call () from /usr/lib/x86_64-linux-gnu/libffi.so.6
#12 0x00007ffff6b1a68c in _ctypes_callproc () from /usr/lib/python2.7/lib-dynload/_ctypes.x86_64-linux-gnu.so
#13 0x00007ffff6b1ed82 in ?? () from /usr/lib/python2.7/lib-dynload/_ctypes.x86_64-linux-gnu.so
#14 0x00000000004b1153 in PyObject_Call ()
#15 0x00000000004ca5ca in PyEval_EvalFrameEx ()
#16 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#17 0x00000000004def08 in ?? ()
#18 0x00000000004b1153 in PyObject_Call ()
#19 0x00000000004f4c3e in ?? ()
#20 0x00000000004b1153 in PyObject_Call ()
#21 0x00000000004f49b7 in ?? ()
#22 0x00000000004b6e2c in ?? ()
#23 0x00000000004b1153 in PyObject_Call ()
#24 0x00000000004ca5ca in PyEval_EvalFrameEx ()
#25 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#26 0x00000000004def08 in ?? ()
#27 0x00000000004b1153 in PyObject_Call ()
#28 0x00000000004c73ec in PyEval_EvalFrameEx ()
#29 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#30 0x00000000004caf42 in PyEval_EvalFrameEx ()
#31 0x00000000004c2e05 in PyEval_EvalCodeEx ()
#32 0x00000000004c2ba9 in PyEval_EvalCode ()
#33 0x00000000004f20ef in ?? ()
#34 0x00000000004eca72 in PyRun_FileExFlags ()
#35 0x00000000004eb1f1 in PyRun_SimpleFileExFlags ()
#36 0x000000000049e18a in Py_Main ()
#37 0x00007ffff7811830 in __libc_start_main (main=0x49daf0 <main>, argc=2, argv=0x7fffffffdfb8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>,
stack_end=0x7fffffffdfa8) at ../csu/libc-start.c:291
#38 0x000000000049da19 in _start ()

唔……这回有更多信息了。set_curterm 是被 llvm::sys::Process::FileDescriptorHasColors() 调用的,llvm 编译器有问题?

十八 · 外部工具:再次使用 cscope

这回,我们需要使用 cscope 看看 llvm 的代码。FileDescriptorHasColors 函数是酱婶的。

1
2
3
4
5
6
static bool terminalHasColors(int fd) {
[...]
// Now extract the structure allocated by setupterm and free its memory
// through a really silly dance.
struct term *termp = set_curterm((struct term *)nullptr);
(void)del_curterm(termp); // Drop any errors here.

在早先的版本里,该函数是这样定义的。

1
2
3
4
5
6
7
8
9
10
11
static bool terminalHasColors() {
if (const char *term = std::getenv("TERM")) {
// Most modern terminals support ANSI escape sequences for colors.
// We could check terminfo, or have a list of known terms that support
// colors, but that would be overkill.
// The user can always ask for no colors by setting TERM to dumb, or
// using a commandline flag.
return strcmp(term, "dumb") != 0;
}
return false;
}

set_curterm 传入 nullptr 着实是个坏主意。

十九 · 复写内存

为了确定嫌疑,同时试着看看有没有可能的临时解决方案,我会尝试在异常调用 set_curterm 时复写修改内存。

首先,我们在 GDB 里跟踪程序,并在 set_curterm 处设置断点,直到调用它时传入了 0x0 作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# gdb `which python`
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
[...]
(gdb) b set_curterm
Function "set_curterm" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (set_curterm) pending.
(gdb) r cachetop.py
Starting program: /usr/bin/python cachetop.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, set_curterm (termp=termp@entry=0xa43150) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
80 {
(gdb) c
Continuing.

Breakpoint 1, set_curterm (termp=termp@entry=0xab5870) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
80 {
(gdb) c
Continuing.

Breakpoint 1, set_curterm (termp=termp@entry=0xbecb90) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
80 {
(gdb) c
Continuing.

Breakpoint 1, set_curterm (termp=0x0) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
80 {

此时,我要使用 set 命令,复写相关的内存:使用之前传入 set_curterm 并正确执行的参数 0xbecb90 替代 0x0——当然,希望这一地址仍旧是合法的。

警告,复写内存是十分危险的!GDB 不会向你确认,「你确定吗」。如果你搞错了,或者输入时手滑了,那么进程就会崩溃。好一点的情况,进程当时就崩溃了。糟糕的情况,可能在如果按时间后才崩溃,而谁也不知道是怎么了。

此处,我是在一台试验用机器上进行调试。因为没有敏感数据,所以我冒险一试。此处,我将用 p/x 以十六进制的形式打印寄存器 %rdi 中的内容,并用 set 命令将其设置为之前可用的值,并检查寄存器状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
(gdb) p/x $rdi
$1 = 0x0
(gdb) set $rdi=0xbecb90
(gdb) p/x $rdi
$2 = 0xbecb90
(gdb) i r
rax 0x100 256
rbx 0x1 1
rcx 0xe71 3697
rdx 0x0 0
rsi 0x7ffff5dd45d3 140737318307283
rdi 0xbecb90 12503952
rbp 0x100 0x100
rsp 0x7fffffffa5b8 0x7fffffffa5b8
r8 0xbf0050 12517456
r9 0x1999999999999999 1844674407370955161
r10 0xbf0040 12517440
r11 0x7ffff7bb4b78 140737349634936
r12 0xbecb70 12503920
r13 0xbeaea0 12496544
r14 0x7fffffffa9a0 140737488333216
r15 0x7fffffffa8a0 140737488332960
rip 0x7ffff3c76a80 0x7ffff3c76a80 <set_curterm>
eflags 0x246 [ PF ZF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0

(当然,因为有调试信息,所以我本没必要直接操作寄存器 %rdi 的值,我可以直接设置函数参数 termp 的值)

现在 %rdi 的值已经更新,其他寄存器看上去也没什么问题。于是我们让程序继续执行。

1
2
3
4
5
(gdb) c
Continuing.

Breakpoint 1, set_curterm (termp=termp@entry=0x0) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
80 {

赞!当前对 set_curterm 的调用通过了,没有引发段错误。不过,下一次调用 set_curterm 又传入了 0x0,我们故技重施。

1
2
3
4
5
6
7
8
(gdb) set $rdi=0xbecb90
(gdb) c
Continuing.
warning: JITed object file architecture unknown is not compatible with target architecture i386:x86-64.

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff34ad411 in ClrBlank (win=0xaea060) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c:1129
1129 if (back_color_erase)

啊哈!这次修改内存得到了另一个段错误。不过,虽然如此,当前的问题至少是解决了。

二十 · 条件断点

在之前的章节中,我不得不连续使用 3 次 continue 以便到达我真正感兴趣的那次函数调用。如果相关函数被调用了上百次,那么我会考虑使用条件断点。以下是一个示例。

首先,我会如常启动程序并未 set_curterm 设置断点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# gdb `which python`
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
[...]
(gdb) b set_curterm
Function "set_curterm" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (set_curterm) pending.
(gdb) r cachetop.py
Starting program: /usr/bin/python cachetop.py
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, set_curterm (termp=termp@entry=0xa43150) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
80 {

现在,我要将断点 1 转换为条件断点,使其只在 %rdi 的值为 0x0 时生效。

1
2
3
4
5
6
7
8
9
10
11
(gdb) cond 1 $rdi==0x0
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00007ffff3c76a80 in set_curterm at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
stop only if $rdi==0x0
breakpoint already hit 1 time
(gdb) c
Continuing.

Breakpoint 1, set_curterm (termp=0x0) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
(gdb)

吼啊!通过使用 cond (conditional),我将断点 1 设置为条件断点。因此,当断点 1 下一次生效时,就是 set_curterm 接受的第一个参数值为 0x0 的时候。此外,我用了 i b (info breakpoints) 列出了所有断点的信息。

这里需要考虑,为什么我不在设置断点后的第一时间就将其设置为条件断点。这是因为,我发现对于程序尚未执行时设置的被延迟的断点来说,这些条件不会生效——至少当前的 GDB 版本是这样。(也有可能是我搞错了)

二一 · 直接返回

我也曾尝试了另一个和复写内存类似的方案。不过,这次我不打算修改内存中的数据,而是修改指令。

警告:先前的警告在此处同样适用。

我将如先前一样,停在 set_curterm0x0 调用处,而后适用 GDB 的 ret (return) 命令跳过函数的执行,直接返回。此处的考量是,如果函数未执行,则全局变量 curterm 不会被设置为 0x0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[...]
(gdb) c
Continuing.

Breakpoint 1, set_curterm (termp=0x0) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80

(gdb) ret
Make set_curterm return now? (y or n) y
#0 0x00007ffff5a44e75 in llvm::sys::Process::FileDescriptorHasColors(int) () from /usr/lib/x86_64-linux-gnu/libbcc.so.0
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
_nc_free_termtype (ptr=ptr@entry=0x100) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/free_ttype.c:52
52 FreeIfNeeded(ptr->str_table);

好嘛,进程又挂了……这是进程崩掉之后的状态。

度过更多代码之后,我决定再试试看。我想试着连续执行两次 ret,以免 set_curterm 的调用者被不完整的调用搞坏了。再次强调:这只是一个非常 hacky 的实验,在生产环境中请慎重执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
[...]
(gdb) c
Continuing.

Breakpoint 1, set_curterm (termp=0x0) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tinfo/lib_cur_term.c:80
80 {
(gdb) ret
Make set_curterm return now? (y or n) y
#0 0x00007ffff5a44e75 in llvm::sys::Process::FileDescriptorHasColors(int) () from /usr/lib/x86_64-linux-gnu/libbcc.so.0
(gdb) ret
Make selected stack frame return now? (y or n) y
#0 0x00007ffff45cabb8 in clang::driver::tools::Clang::ConstructJob(clang::driver::Compilation&, clang::driver::JobAction const&, clang::driver::InputInfo const&, llvm::SmallVector const&, llvm::opt::ArgList const&, char const*) const () from /usr/lib/x86_64-linux-gnu/libbcc.so.0
(gdb) c

这回,整个屏幕都白了,然后停住……之后显示出了如下结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
07:44:22 Buffers MB: 61 / Cached MB: 1246
PID UID CMD HITS MISSES DIRTIES READ_HIT% WRITE_HIT%
2742 root systemd-logind 3 66 2 1.4% 95.7%
15836 root kworker/u30:1 7 0 1 85.7% 0.0%
2736 messageb dbus-daemon 8 66 2 8.1% 89.2%
1 root systemd 15 0 0 100.0% 0.0%
2812 syslog rs:main Q:Reg 16 66 8 9.8% 80.5%
435 root systemd-journal 32 66 8 24.5% 67.3%
2740 root accounts-daemon 113 66 2 62.0% 36.9%
15847 root bash 160 0 1 99.4% 0.0%
15864 root lesspipe 306 0 2 99.3% 0.0%
15854 root bash 309 0 2 99.4% 0.0%
15856 root bash 309 0 2 99.4% 0.0%
15866 root bash 309 0 2 99.4% 0.0%
15867 root bash 309 0 2 99.4% 0.0%
15860 root bash 313 0 2 99.4% 0.0%
15868 root bash 341 0 2 99.4% 0.0%
15858 root uname 452 0 2 99.6% 0.0%
15858 root bash 453 0 2 99.6% 0.0%
15866 root dircolors 464 0 2 99.6% 0.0%
15861 root basename 465 0 2 99.6% 0.0%
15864 root dirname 468 0 2 99.6% 0.0%
15856 root ls 476 0 2 99.6% 0.0%
[...]

帅!炸!搞定啦!

二二 · 更好的方案

我将调试过程的结果提交在 github 上。这是因为,一方面 BPF 的首席工程师 Alexei Starovoitov 也是 llvm 方面的专家,另一方面这个问题的根源似乎是 llvm 的一个 bug。当我在把指令和数据搞得乱七八糟时,他建议我在编译 bcc 时加入 llvm 的参数 -fno-color-diagnostics 即可绕过这一问题。搞定!这确实能解决问题。因此我将其加入了 bcc 的代码库作为暂时的解决方案,并期待 llvm 解决这个 bug。

二三 · 关于 Python 的环境

至此,我们已经解决了问题。不过,(译注:有强迫症的)你可能还想要让整个调用栈看起来完好。

为此,你需要安装 python-dbg 软件包。

1
2
3
4
5
6
7
8
9
10
11
12
13
# apt-get install -y python-dbg
Reading package lists... Done
[...]
The following additional packages will be installed:
libpython-dbg libpython2.7-dbg python2.7-dbg
Suggested packages:
python2.7-gdbm-dbg python2.7-tk-dbg python-gdbm-dbg python-tk-dbg
The following NEW packages will be installed:
libpython-dbg libpython2.7-dbg python-dbg python2.7-dbg
0 upgraded, 4 newly installed, 0 to remove and 20 not upgraded.
Need to get 11.9 MB of archives.
After this operation, 36.4 MB of additional disk space will be used.
[...]

此时,再打开 GDB 看看调用栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# gdb `which python` /var/cores/core.python.30520
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
[...]
Reading symbols from /usr/bin/python...Reading symbols from /usr/lib/debug/.build-id/4e/a0539215b2a9e32602f81c90240874132c1a54.debug...done.
[...]
(gdb) bt
#0 ClrBlank (win=0x1993060) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c:1129
#1 ClrUpdate () at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c:1147
#2 doupdate () at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/tty/tty_update.c:1010
#3 0x00007f0a37aa07e6 in wrefresh (win=win@entry=0x1993060) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/base/lib_refresh.c:65
#4 0x00007f0a37a99499 in recur_wrefresh (win=win@entry=0x1993060) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/base/lib_getch.c:384
#5 0x00007f0a37a99616 in _nc_wgetch (win=win@entry=0x1993060, result=result@entry=0x7ffd33d93e24, use_meta=1)
at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/base/lib_getch.c:491
#6 0x00007f0a37a9a325 in wgetch (win=0x1993060) at /build/ncurses-pKZ1BN/ncurses-6.0+20160213/ncurses/base/lib_getch.c:672
#7 0x00007f0a37cc6ec3 in PyCursesWindow_GetCh.lto_priv.109 (self=0x7f0a3c57b198, args=()) at /build/python2.7-HpIZBG/python2.7-2.7.11/Modules/_cursesmodule.c:853
#8 0x00000000004c4d5a in call_function (oparg=<optimized out>, pp_stack=0x7ffd33d93f30) at ../Python/ceval.c:4350
#9 PyEval_EvalFrameEx () at ../Python/ceval.c:2987
#10 0x00000000004c2e05 in PyEval_EvalCodeEx () at ../Python/ceval.c:3582
#11 0x00000000004def08 in function_call.lto_priv () at ../Objects/funcobject.c:523
#12 0x00000000004b1153 in PyObject_Call () at ../Objects/abstract.c:2546
#13 0x00000000004c73ec in ext_do_call (nk=0, na=<optimized out>, flags=<optimized out>, pp_stack=0x7ffd33d941e8, func=<function at remote 0x7f0a37edcc80>)
at ../Python/ceval.c:4662
#14 PyEval_EvalFrameEx () at ../Python/ceval.c:3026
#15 0x00000000004c2e05 in PyEval_EvalCodeEx () at ../Python/ceval.c:3582
#16 0x00000000004caf42 in fast_function (nk=0, na=<optimized out>, n=<optimized out>, pp_stack=0x7ffd33d943f0, func=<function at remote 0x7f0a38039140>)
at ../Python/ceval.c:4445
#17 call_function (oparg=<optimized out>, pp_stack=0x7ffd33d943f0) at ../Python/ceval.c:4370
#18 PyEval_EvalFrameEx () at ../Python/ceval.c:2987
#19 0x00000000004c2e05 in PyEval_EvalCodeEx () at ../Python/ceval.c:3582
#20 0x00000000004c2ba9 in PyEval_EvalCode (co=<optimized out>, globals=<optimized out>, locals=<optimized out>) at ../Python/ceval.c:669
#21 0x00000000004f20ef in run_mod.lto_priv () at ../Python/pythonrun.c:1376
#22 0x00000000004eca72 in PyRun_FileExFlags () at ../Python/pythonrun.c:1362
#23 0x00000000004eb1f1 in PyRun_SimpleFileExFlags () at ../Python/pythonrun.c:948
#24 0x000000000049e18a in Py_Main () at ../Modules/main.c:640
#25 0x00007f0a3be10830 in __libc_start_main (main=0x49daf0 <main>, argc=2, argv=0x7ffd33d94838, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>,
stack_end=0x7ffd33d94828) at ../csu/libc-start.c:291
#26 0x000000000049da19 in _start ()

此时,所有的 ?? 都不见了,然而对解决我们的问题并没有什么 X 用……

此外,Python 调试软件包还加强了 GDB 的功能。我们现在可以看看 Python 的调用栈。

1
2
3
4
5
6
7
8
(gdb) py-bt
Traceback (most recent call first):
File "./cachetop.py", line 188, in handle_loop
s = stdscr.getch()
File "/usr/lib/python2.7/curses/wrapper.py", line 43, in wrapper
return func(stdscr, *args, **kwds)
File "./cachetop.py", line 260, in
curses.wrapper(handle_loop, args)

以及,我们可以看看 Python 代码是怎样的。

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) py-list
183 b.attach_kprobe(event="mark_buffer_dirty", fn_name="do_count")
184
185 exiting = 0
186
187 while 1:
>188 s = stdscr.getch()
189 if s == ord('q'):
190 exiting = 1
191 elif s == ord('r'):
192 sort_reverse = not sort_reverse
193 elif s == ord('<'):

这能指出,在我们的 Python 代码中,究竟是那一行触发了段错误。这看起来非常棒!

之前我们在回溯调用栈时遇到的问题,其原因在于我们看到了 Python 内部在执行的方法(methods),但却看不到对应方法的符号。如果你在调试别的语言,类似的问题取决于它的编译选项和运行环境,以及执行代码是如何结束的。如果你在网上检索「语言名 GDB」,你可能会找到类似 python-dbg 这样的扩展。如果没有的话,坏消息是你必须自己写这样的软件包,但好消息是这样做是可行的。如果你是在调试 Python,那么请在网上检索「add new GDB commands in Pyhon」。

二四 · 更多……

看起来,我似乎是要写一个完整的 GDB 指南,但显然我不是:GDB 还有很多我没讲到的东西。你可以在 GDB 中使用 help 命令,分门别类查看 GDB 的其他功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) help
List of classes of commands:

aliases -- Aliases of other commands
breakpoints -- Making program stop at certain points
data -- Examining data
files -- Specifying and examining files
internals -- Maintenance commands
obscure -- Obscure features
running -- Running the program
stack -- Examining the stack
status -- Status inquiries
support -- Support facilities
tracepoints -- Tracing of program execution without stopping the program
user-defined -- User-defined commands

Type "help" followed by a class name for a list of commands in that class.
Type "help all" for the list of all commands.
Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.

对于具体某个命令来说,你也可以用 help 命令查看具体用法。例如说,你可以查看所有和 breakpoints 相关的命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
(gdb) help breakpoints
Making program stop at certain points.

List of commands:

awatch -- Set a watchpoint for an expression
break -- Set breakpoint at specified location
break-range -- Set a breakpoint for an address range
catch -- Set catchpoints to catch events
catch assert -- Catch failed Ada assertions
catch catch -- Catch an exception
catch exception -- Catch Ada exceptions
catch exec -- Catch calls to exec
catch fork -- Catch calls to fork
catch load -- Catch loads of shared libraries
catch rethrow -- Catch an exception
catch signal -- Catch signals by their names and/or numbers
catch syscall -- Catch system calls by their names and/or numbers
catch throw -- Catch an exception
catch unload -- Catch unloads of shared libraries
catch vfork -- Catch calls to vfork
clear -- Clear breakpoint at specified location
commands -- Set commands to be executed when a breakpoint is hit
condition -- Specify breakpoint number N to break only if COND is true
delete -- Delete some breakpoints or auto-display expressions
delete bookmark -- Delete a bookmark from the bookmark list
delete breakpoints -- Delete some breakpoints or auto-display expressions
delete checkpoint -- Delete a checkpoint (experimental)
delete display -- Cancel some expressions to be displayed when program stops
delete mem -- Delete memory region
delete tracepoints -- Delete specified tracepoints
delete tvariable -- Delete one or more trace state variables
disable -- Disable some breakpoints
disable breakpoints -- Disable some breakpoints
disable display -- Disable some expressions to be displayed when program stops
disable frame-filter -- GDB command to disable the specified frame-filter
disable mem -- Disable memory region
disable pretty-printer -- GDB command to disable the specified pretty-printer
disable probes -- Disable probes
disable tracepoints -- Disable specified tracepoints
disable type-printer -- GDB command to disable the specified type-printer
disable unwinder -- GDB command to disable the specified unwinder
disable xmethod -- GDB command to disable a specified (group of) xmethod(s)
dprintf -- Set a dynamic printf at specified location
enable -- Enable some breakpoints
enable breakpoints -- Enable some breakpoints
enable breakpoints count -- Enable breakpoints for COUNT hits
enable breakpoints delete -- Enable breakpoints and delete when hit
enable breakpoints once -- Enable breakpoints for one hit
enable count -- Enable breakpoints for COUNT hits
enable delete -- Enable breakpoints and delete when hit
enable display -- Enable some expressions to be displayed when program stops
enable frame-filter -- GDB command to disable the specified frame-filter
enable mem -- Enable memory region
enable once -- Enable breakpoints for one hit
enable pretty-printer -- GDB command to enable the specified pretty-printer
enable probes -- Enable probes
enable tracepoints -- Enable specified tracepoints
enable type-printer -- GDB command to enable the specified type printer
enable unwinder -- GDB command to enable unwinders
enable xmethod -- GDB command to enable a specified (group of) xmethod(s)
ftrace -- Set a fast tracepoint at specified location
hbreak -- Set a hardware assisted breakpoint
ignore -- Set ignore-count of breakpoint number N to COUNT
rbreak -- Set a breakpoint for all functions matching REGEXP
rwatch -- Set a read watchpoint for an expression
save -- Save breakpoint definitions as a script
save breakpoints -- Save current breakpoint definitions as a script
save gdb-index -- Save a gdb-index file
save tracepoints -- Save current tracepoint definitions as a script
skip -- Ignore a function while stepping
skip delete -- Delete skip entries
skip disable -- Disable skip entries
skip enable -- Enable skip entries
skip file -- Ignore a file while stepping
skip function -- Ignore a function while stepping
strace -- Set a static tracepoint at location or marker
tbreak -- Set a temporary breakpoint
tcatch -- Set temporary catchpoints to catch events
tcatch assert -- Catch failed Ada assertions
tcatch catch -- Catch an exception
tcatch exception -- Catch Ada exceptions
tcatch exec -- Catch calls to exec
tcatch fork -- Catch calls to fork
tcatch load -- Catch loads of shared libraries
tcatch rethrow -- Catch an exception
tcatch signal -- Catch signals by their names and/or numbers
tcatch syscall -- Catch system calls by their names and/or numbers
tcatch throw -- Catch an exception
tcatch unload -- Catch unloads of shared libraries
tcatch vfork -- Catch calls to vfork
thbreak -- Set a temporary hardware assisted breakpoint
trace -- Set a tracepoint at specified location
watch -- Set a watchpoint for an expression

Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.

显而易见,GDB 还有很多功能,而我仅只是在解决问题的过程中使用了其中很小一部分。

二五 · 结语

好吧,这里我遇到的问题比较恶心:这是一个由 LLVM 的 bug 经由 ncurses 最终导致 Python 程序崩溃的例子。不过,解决问题过程中,我用到的命令和使用他们的流程基本就是调试查错的基本流程:查看调用栈、检查寄存器、设置断点、单步调试、浏览代码。

当我在几年前第一次使用 GDB 时,我真的不怎么喜欢它。它看起来又笨拙又难用。然而,GDB 已经改进了很多,同时也有了一些 GDB 调试查错的技巧之后,现在我认为它是一个强大的现代调试器。不同调试器有不同的技能,但是 GDB 必是其中最强大的文本调试器——当然 lldb 正在迎头赶上。

在此,我希望所有查找 GDB 调试范例的人会从我的例子和警示中学到东西。当然,以后有机会我也会分享更多关于 GDB 的经验——尤其是诸如 JAVA 的运行时问题。

哦对了,退出 GDB 的方法是 q (quit) 命令。(译者注:你也可以使用 Ctrl + D 来退出 GDB 调试器。)


您的鼓励是我写作最大的动力

俗话说,投资效率是最好的投资。 如果您感觉我的文章质量不错,读后收获很大,预计能为您提高 10% 的工作效率,不妨小额捐助我一下,让我有动力继续写出更多好文章。


撰写评论

写了这么多年博客,收到的优秀评论少之又少。在这个属于 SNS 的时代也并不缺少向作者反馈的渠道。因此,如果你希望撰写评论,请发邮件至我的邮箱并注明文章标题,我会挑选对读者有价值的评论附加到文章末尾。