GNU 调试器实用技巧

学习如何使用 gdb 的一些不太为人所知的功能来检查和修复您的代码。
92 位读者喜欢这篇文章。
Bug tracking magnifying glass on computer screen

Pixabay, testbytes, CC0

GNU 调试器 (gdb) 是一个非常有价值的工具,可以在您开发程序时检查正在运行的进程并修复问题。

您可以设置特定位置的断点(通过函数名称、行号等),启用和禁用这些断点,显示和更改变量值,以及执行您期望任何调试器执行的所有标准操作。但它还有许多您可能没有尝试过的其他功能。以下是五个供您尝试。

条件断点

设置断点是您将要学习使用 GNU 调试器的第一件事。程序在到达断点时停止,您可以运行 gdb 命令来检查它或更改变量,然后允许程序继续。

例如,您可能知道一个经常调用的函数有时会崩溃,但仅当它获得某个参数值时才会崩溃。您可以在该函数的开头设置一个断点并运行程序。每次命中该断点时都会显示函数参数,如果未提供触发崩溃的参数值,您可以继续直到再次调用该函数。当有问题的参数触发崩溃时,您可以单步执行代码以查看问题所在。

(gdb) break sometimes_crashes
Breakpoint 1 at 0x40110e: file prog.c, line 5.
(gdb) run
[...]
Breakpoint 1, sometimes_crashes (f=0x7fffffffd1bc) at prog.c:5
5      fprintf(stderr,
(gdb) continue
Breakpoint 1, sometimes_crashes (f=0x7fffffffd1bc) at prog.c:5
5      fprintf(stderr,
(gdb) continue

为了使此操作更具可重复性,您可以计算在您感兴趣的特定调用之前函数被调用的次数,并在该断点上设置一个计数器(例如,“continue 30”使其忽略接下来 29 次到达断点)。

但断点真正强大之处在于它们能够在运行时评估表达式,这使您可以自动执行此类测试。输入:条件断点。

break [LOCATION] if CONDITION

(gdb) break sometimes_crashes if !f
Breakpoint 1 at 0x401132: file prog.c, line 5.
(gdb) run
[...]
Breakpoint 1, sometimes_crashes (f=0x0) at prog.c:5
5      fprintf(stderr,
(gdb)

条件断点不是让 gdb 每次调用该函数时都询问要执行的操作,而是允许您使 gdb 仅在该特定表达式的计算结果为 true 时才在该位置停止。如果执行到达条件断点位置,但表达式的计算结果为 false,则

调试器自动让程序继续运行,而无需询问用户要执行的操作。

断点命令

GNU 调试器中断点的更复杂功能是能够编写脚本来响应到达断点。断点命令允许您编写一个 GNU 调试器命令列表,以便在每次到达断点时运行。

我们可以使用它来解决我们已经知道的 sometimes_crashes 函数中的错误,并在它提供空指针时使其无害地从该函数返回。

我们可以使用 silent 作为第一行来更好地控制输出。如果没有这个,每次命中断点时都会显示堆栈帧,甚至在我们的断点命令运行之前。

(gdb) break sometimes_crashes
Breakpoint 1 at 0x401132: file prog.c, line 5.
(gdb) commands 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>silent
>if !f
 >frame
 >printf "Skipping call\n"
 >return 0
 >continue
 >end
>printf "Continuing\n"
>continue
>end
(gdb) run
Starting program: /home/twaugh/Documents/GDB/prog
warning: Loadable section ".note.gnu.property" outside of ELF segments
Continuing
Continuing
Continuing
#0  sometimes_crashes (f=0x0) at prog.c:5
5      fprintf(stderr,
Skipping call
[Inferior 1 (process 9373) exited normally]
(gdb) 

转储二进制内存

GNU 调试器内置了使用 x 命令以各种格式(包括八进制、十六进制等)检查内存的支持。但我喜欢并排查看两种格式:左侧是十六进制字节,右侧是这些相同字节表示的 ASCII 字符。

当我想逐字节查看文件的内容时,我经常使用 hexdump -C(hexdump 来自 util-linux 包)。这是 gdb 的 x 命令显示十六进制字节

(gdb) x/33xb mydata
0x404040 <mydata>:    0x02    0x01    0x00    0x02    0x00    0x00    0x00    0x01
0x404048 <mydata+8>:    0x01    0x47    0x00    0x12    0x61    0x74    0x74    0x72
0x404050 <mydata+16>:    0x69    0x62    0x75    0x74    0x65    0x73    0x2d    0x63
0x404058 <mydata+24>:    0x68    0x61    0x72    0x73    0x65    0x75    0x00    0x05
0x404060 <mydata+32>:    0x00

如果您可以教 gdb 像 hexdump 一样显示内存,该怎么办?您可以,事实上,您可以将此方法用于您喜欢的任何格式。

通过组合 dump 命令将字节存储在文件中,使用 shell 命令在文件上运行 hexdump,以及使用 define 命令,我们可以制作我们自己的新 hexdump 命令来使用 hexdump 显示内存内容。

(gdb) define hexdump
Type commands for definition of "hexdump".
End with a line saying just "end".
>dump binary memory /tmp/dump.bin $arg0 $arg0+$arg1
>shell hexdump -C /tmp/dump.bin
>end

这些命令甚至可以放入 ~/.gdbinit 文件中以永久定义 hexdump 命令。这是它的实际应用

(gdb) hexdump mydata sizeof(mydata)
00000000  02 01 00 02 00 00 00 01  01 47 00 12 61 74 74 72  |.........G..attr|
00000010  69 62 75 74 65 73 2d 63  68 61 72 73 65 75 00 05  |ibutes-charseu..|
00000020  00                                                |.|
00000021

内联反汇编

有时您想更多地了解导致崩溃的原因,而源代码是不够的。您想了解 CPU 指令级别上发生的事情。

disassemble 命令让您可以查看实现函数的 CPU 指令。但有时输出可能难以理解。通常,我想查看哪些指令对应于函数中特定部分的源代码。为此,请使用 /s 修饰符将源代码行包含在反汇编中。

(gdb) disassemble/s main
Dump of assembler code for function main:
prog.c:
11    {
   0x0000000000401158 <+0>:    push   %rbp
   0x0000000000401159 <+1>:    mov	%rsp,%rbp
   0x000000000040115c <+4>:    sub	$0x10,%rsp

12    	int n = 0;
   0x0000000000401160 <+8>:    movl   $0x0,-0x4(%rbp)

13    	sometimes_crashes(&n);
   0x0000000000401167 <+15>:    lea	-0x4(%rbp),%rax
   0x000000000040116b <+19>:    mov	%rax,%rdi
   0x000000000040116e <+22>:    callq  0x401126 <sometimes_crashes>
[...snipped...]

这与 info registers 一起查看所有 CPU 寄存器的当前值,以及像 stepi 这样一次单步执行一条指令的命令,可以让您对程序有更详细的了解。

反向调试

有时您希望可以回到过去。想象一下,您已经命中了变量上的观察点。观察点就像一个断点,但它不是设置在程序中的某个位置,而是设置在一个表达式上(使用 watch 命令)。每当表达式的值发生变化时,执行都会停止,调试器会接管控制。

所以想象一下,您已经命中了这个观察点,并且变量使用的内存值已经改变。这可能是由较早发生的事情引起的;例如,内存被释放并且现在正在被重复使用。但是它是什么时候以及为什么被释放的?

GNU 调试器甚至可以解决这个问题,因为您可以反向运行您的程序!

它通过仔细记录程序在每个步骤中的状态来实现这一点,以便它可以恢复先前记录的状态,从而给人以时间倒流的错觉。

要启用此状态记录,请使用 target record-full 命令。然后您可以使用听起来不可能的命令,例如

  • reverse-step,它会倒回到上一行源代码
  • reverse-next,它会倒回到上一行源代码,向后单步执行函数调用
  • reverse-finish,它会倒回到即将调用当前函数的时间点
  • reverse-continue,它会倒回到程序中以前的状态,该状态(现在)会触发断点(或导致其停止的任何其他内容)

这是一个反向调试的示例

(gdb) b main
Breakpoint 1 at 0x401160: file prog.c, line 12.
(gdb) r
Starting program: /home/twaugh/Documents/GDB/prog
[...]

Breakpoint 1, main () at prog.c:12
12    	int n = 0;
(gdb) target record-full
(gdb) c
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x0000000000401154 in sometimes_crashes (f=0x0) at prog.c:7
7      return *f;
(gdb) reverse-finish
Run back to call of #0  0x0000000000401154 in sometimes_crashes (f=0x0)
	at prog.c:7
0x0000000000401190 in main () at prog.c:16
16    	sometimes_crashes(0);

这些只是 GNU 调试器可以执行的少量有用操作。还有更多需要发现。您最喜欢的 gdb 的哪些隐藏的、鲜为人知的或只是令人惊叹的功能?请在评论中分享。

接下来阅读
标签
User profile image.
Tim Waugh 领导着管理用于构建 Red Hat 产品容器映像的工具的团队。 他于 1999 年加入 Red Hat,二十多年来一直编写免费软件。 他喜欢学习魔术和速记之类的东西。

1 条评论

一种快速而肮脏但非常有效的调试内存泄漏的方法是让泄漏增长,直到它消耗了程序的大部分内存,然后附加 gdb 并简单地 x 随机内存片段。由于泄漏的数据正在占用大部分内存,您通常会很快命中它,并且可以尝试解释它一定来自哪里。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.