前言

gdbGNU 下的一款调试器,支持常见的语言,例如 C/C++GoRust 等等。

调试器对我们开发程序的时候有非常大的帮助,是每一个开发都需要学会的调试技能。

今天来了解一下 gdb 的使用。

启动调试

使用 gdb 调试有两种方式,分别是直接调试还没运行的程序和调试已经运行的程序。

我们先来了解如何调试编译好的程序,这里需要注意一点,编译的时候需要加入 -g 保留调试信息,否则不能被调试。

假如我们要调试的程序名为 main ,先用 gcc -g -o main main.c 进行编译,然后中使用 gdb main 就可以对 main 进行调试。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
➜  ~ gdb main
GNU gdb (GDB) 10.2
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
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-pc-linux-gnu".
Type "show configuration" for configuration details.
--Type <RET> for more, q to quit, c to continue without paging--c
For bug reporting instructions, please see:
<https://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 main...
(gdb)

这时候程序还没开始执行。要想程序开始执行可以使用 run ,这样程序就直接运行起来了。 run 可以简写为 r

1
2
3
4
5
6
Reading symbols from main...
(gdb) run
Starting program: /home/test/main
hello,world
[Inferior 1 (process 1215) exited normally]
(gdb)

但是通过 run 运行的程序会直接执行,这时候还没来得及设置断点,程序已经运行结束了。

为了解决这个问题可以使用 start 来运行程序,它会停在 main 函数处,等待下一步的调试指令。

1
2
3
4
5
6
7
8
Reading symbols from main...
(gdb) start
Temporary breakpoint 1 at 0x1148: file main.c, line 5.
Starting program: /home/test/main

Temporary breakpoint 1, main (argc=1, argv=0x7fffffffde48) at main.c:5
5           printf("%s\n","hello,world");
(gdb)

对于已经启动的程序,需要通过进程号来启动调试,获取进程号可以通过 ps -ef |grep 进程名 ,获取到进程号之后就可以使用 gdb attach 进程号 来启动调试。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
➜  ~ ps -ef |grep main
test     1352    1202  0 21:16 pts/2    00:00:00 ./main
➜  ~ gdb attach 1352
...
Attaching to process 1352
Reading symbols from /home/test/main...
Reading symbols from /usr/lib/libc.so.6...
(No debugging symbols found in /usr/lib/libc.so.6)
Reading symbols from /lib64/ld-linux-x86-64.so.2...
(No debugging symbols found in /lib64/ld-linux-x86-64.so.2)
0x00007f3179b51a4a in clock_nanosleep@GLIBC_2.2.5 () from /usr/lib/libc.so.6
(gdb)

如果你有执行文件还可以使用 gdb 程序 -p 进程号 来进行调试,效果和上面的一样的。

如果你已经进入 gdb 环境,可以直接使用 attach 进程号 来指定要调试的进程。

在使用 attch 的时候会碰到一个 not permitted 的错误,如下所示

1
2
3
attach: No such file or directory.
Attaching to process 1285
ptrace: Operation not permitted.

这时候你可以使用 sudo echo 0 > /proc/sys/kernel/yama/ptrace_scope 来解决这个问题。

设置断点

使用 gdb 运行程序后,我们需要对程序设置断点,当程序执行到这里程序就会暂停往下执行,这对我们调试程序非常有帮助。

设置断点可以通过 break 关键字,后面可以跟函数名、行号, break 可以简写为 b ,在多个文件的情况下可以使用 文件名: 行号

1
2
3
4
5
(gdb) break main
Breakpoint 1 at 0x1148: file main.c, line 5.
(gdb) b 6
Breakpoint 2 at 0x1157: file main.c, line 6.
(gdb) b main.c:7

上面的设置会在程序执行到断点的位置时停止执行,每次都是。我们希望在某些情况下才停下来,其他情况继续执行,这时候就需要用到条件断点。

条件断点的设置只需要在断点后面加上需要的条件,例如 b 8 if a==0 ,这样当程序执行到第8行的时候发现 a==0 就会暂停执行。

查看断点

设置完断点之后,可以通过 info break 来查看设置过的断点,简写 i b

1
2
3
4
5
(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000001148 in main at main.c:5
2       breakpoint     keep y   0x0000000000001157 in main at main.c:6
3       breakpoint     keep y   0x000000000000115c in main at main.c:7

第一列的是断点号,清除断点的时候会用到,下一小节会讲到。

清除断点

清除断点使用 clear ,单独它会清除当前行的断点。

clear 函数名 会清除函数上的断点。

clear 文件名:行号 会清除指定文件对于行号上的断点, clear 文件名:函数名 会清除对应文件函数上的断点。

delete 删除所有断点。

delete 断点号 删除指定断点号的断点,断点号就是上一小节使用 info break 查看中的第一列。

单步调试

我们在设置好断点之后,希望一步一步执行代码,在 gdb 中可以使用 next 来一行一行执行, next 可以简写为 n

在单步执行的过程中如果碰到函数,使用 n 不会进入函数内部执行,这时候需要用到 stepstep 简写为 s 。使用 s 可以进入到函数内部进行调试。

如果你不想再一步一步的调试,想要程序继续一直往下执行可以使用 continue ,简写为 c ,这样程序会在碰到下一个断点的时候停下来。

设置启动参数

在一些程序中,启动的时候需要传递参数给程序进行处理,而在 gdb 中可以通过 set args 参数 来设置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Reading symbols from main...
(gdb) set args value1 value2
(gdb) start
Temporary breakpoint 1 at 0x1148: file main.c, line 5.
Starting program: /home/test/main value1 value2

Temporary breakpoint 1, main (argc=3, argv=0x7fffffffde28) at main.c:5
5           printf("%s\n", "hello, world");
(gdb) p argc
$1 = 3
(gdb) p argv
$2 = (char **) 0x7fffffffde28
(gdb) p argv[0]
$3 = 0x7fffffffe1c9 "/home/test/main"
(gdb) p argv[1]
$4 = 0x7fffffffe1dc "value1"
(gdb) p argv[2]
$5 = 0x7fffffffe1e3 "value2"

打印变量的值

在使用 gdb 调试的过程中,我们经常需要查看变量的值,打印使用 print 简写为 p

例如我要打印变量 a 的值,可以使用 p a

print 也支持格式打印,和 c 语言中的 print 类似

x 按十六进制格式打印
d 按十进制格式打印
o 按八进制格式打印
1
2
3
4
5
6
7
8
(gdb) p a
$1 = 15
(gdb) p/x a
$2 = 0xf
(gdb) p/d a
$3 = 15
(gdb) p/o a
$4 = 017

调试core dump

Linux 下程序崩溃后会产生一个 core dump 文件,通过这个文件可以分析程序的崩溃问题

在使用 systemd 的系统中可以使用 coredumpctl list 来查看所有的 core dump 记录

这时候可以通过 coredumpctl gdb 进程号 来对崩溃的进程进行调试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
coredumpctl gdb 1248
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /home/test/main...
[New LWP 1248]
Core was generated by `./main'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000056091440e15b in main (argc=1, argv=0x7ffdf32b2d28) at main.c:7
7           *p = 10;
(gdb) bt
#0  0x000056091440e15b in main (argc=1, argv=0x7ffdf32b2d28) at main.c:6

从上面的输出可以看到在第7行发生了段错误,这里崩溃的原因是使用了一个没有初始化的指针。

对于 core dump 我们还可以把它保存下来,使用 coredumpctl -o core dump 1248 这样就会把 1248 这个进程的崩溃保存到 core 文件中,方便下次进行查看。

有了 core dump 文件也可以直接从 gdb 中查看,通过 gdb main core 来进行调试。输出结果跟上面一样。

在调试 core dump 的时候可以通过 backtrace 来查看函数的调用栈。 backtrace 可以简写为 bt

总结

调试指令 解释
r 直接运行程序
start 运行程序,会停在 main 方法上
n 单步执行
s 单步执行,会进入函数内部
p 打印变量
set args 设置程序运行参数
b 设置断点,可以跟行号、函数名
clear 清除断点,可以跟行号、函数名
delete 删除所有断点
bt 查看调用栈

参考