前言

GDB (全称为GNU Debuger) 是一款由 GNU 开源组织发布、基于 UNIX/LINUX 操作系统的命令行程序调试工具。对于一名 Linux 下工作的 C++ 程序员,GDB 是必不可少的工具。

GDB常用调试命令

为了便于讲解,将通过以下 demo 进行举例说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;
int add(int a,int b){
int sum = 0;
sum = a + b;
return sum;
}
int main(int argc,char *argv[]){
int sum = 0;
sum = add(1, 2);
cout << sum << endl;
return 0;
}

1.启动 GDB

对于 C/C++ 程序调试,需要在编译前加入 -g 参数选项,表示加入一些调试信息。这样在编译后生成的可执行文件才能使用 GDB 进行调试

1
g++ -g main.cpp -o demo  // 生成可执行文件

启动 GDB 命令:

1
gdb demo  // 启动 GDB 调试

效果如图 1 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(base) sv@sv-NF5280M5:/home/sv/桌面$ gdb main
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
Copyright (C) 2020 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-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 main...
(gdb)

2. 终端列出源代码

我们可以在终端列出要调试的代码,这样可以不用打开源文件查看。

1
list  // 执行一次列出 10 行代码,再执行一次列出后面 10 行代码

效果如图 2 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from main...
(gdb) list
1 #include <iostream>
2 using namespace std;
3 int add(int a,int b){
4 int sum = 0;
5 sum = a + b;
6 return sum;
7 }
8 int main(int argc,char *argv[]){
9 int sum = 0;
10 sum = add(1, 2);
(gdb)
11 cout << sum << endl;
12 return 0;
13 }

3. 设置断点

断点是程序调试的关键所在,随心所欲地设置断点使我们调试程序变得高效。

断点打在源程序第 n 行:

1
break n  // 在源程序第 n 行设置一个断点,break 可以用 b 缩写替代

断点打在源程序 main() 函数入口:

1
break main  // 在 main() 函数入口打断点,也就是第 9 行,break 可以用 b 缩写替代

上面是打在源文件的函数入口,若要将断点打在其他文件的函数入口或者其他文件的特定行号应该怎么做呢?

下面的命令可以解决:

1
2
3
break otherfile:func  // 断点打在其他文件的函数入口
break otherfile:n // 断点打在其他文件的第 n 行
// 注:otherfile 为其他文件,例如 sort.cpp,而 n 代表行号

我们还可以查看我们所有打的断点:

1
info break  // 列出我们所有打的断点的信息,break 可以用 b 缩写替代

除了设置断点外,我们还能删除指定断点或者删除全部断点:

1
2
delete    // 删除全部断点
delete n // 删除指定断点,n 表示断点号

还有使能和失能断点,失能表示这个断点不会删除,只是暂时失作用,可以理解为屏蔽。若想再次启用可以直接使能,断点功能恢复,不用去重新打断点。

1
2
disable n  // 失能第 n 个断点
enable n // 使能第 n 个断点

效果如图 3 所示:

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) b main
Breakpoint 1 at 0x11ce: file main.cpp, line 8.
(gdb) b 10
Breakpoint 2 at 0x11e8: file main.cpp, line 10.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000000011ce in main(int, char**) at main.cpp:8
2 breakpoint keep y 0x00000000000011e8 in main(int, char**) at main.cpp:10
(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) info b
No breakpoints or watchpoints.

4. 运行程序

运行程序命令与我们 IDE 调试程序类似,两者可以作对比。

1
2
3
4
5
6
7
8
run  // 类比于 IDE 的全速运行,遇到断点会停下,run 可以用 r 缩写替代
next // 类比于 IDE 不进入函数的单步调试,next 可以用 n 缩写替代
step // 类比于 IDE 进入函数的单步调试,step 可以用 s 缩写替代
continue // 继续执行直到遇到下一个断点停下,没有断点直接结束,可以用 c 缩写替代
until n // 直接跳转到指定行号程序,n 表示行号
finish // 当程序运行在函数中时,直接执行完当前函数并打印函数返回信息
kill // 直接结束当前进程,程序可以从头运行
quit // 退出 GDB,quit 可以用 q 缩写替代

5.打印信息

1
2
print x  // 可以打印变量、地址、表达式值等,x 表示需要打印的东西,print 可以用 p 替代
bt // 打印当前调用堆栈的信息,后面会继续讲

GDB 的常用调试命令基本上就这些,而且都有缩写表示,非常好记忆的!

GDB调试core文件

Linux应用程序发生Segmentation fault段错误时,需要利用core dump文件定位错误。

段错误segmentation fault,信号SIGSEGV,是由于访问内存管理单元MMU异常所致,通常由于无效内存引用,如指针引用了一个不属于当前进程地址空间中的地址,操作系统便会进行干涉引发SIGSEGV信号产生段错误。

段错误产生的原因

  • 空指针
  • 野指针
  • 堆栈越界

核心转储

在 Linux 系统中,常将“主内存”称为核心(core),核心映像(core image) 就是 “进程”(process)执行当时的内存内容。

当进程发生错误或收到“信号”(signal) 而终止执行时,系统会将核心映像写入一个文件,以作为调试之用,这就是所谓的核心转储(core dump)。

【简单解释】当在一个程序崩溃时,系统会在指定目录下生成一个core文件,我们就可以通过 core文件来对造成程序崩贵的原因进行调试定位。

1. 产生 Segmentation fault 程序

下面是一个没有递归终止条件的程序,显然会发生 Segmentation fault 错误。

1
2
3
4
5
6
7
(base) sv@sv-NF5280M5:/home/sv/桌面$ g++ -g -o main main.cpp -lpthread -fpermissive
main.cpp: In function ‘int recursion(int)’:
main.cpp:5:1: warning: no return statement in function returning non-void [-Wreturn-type]
5 | }
| ^
(base) sv@sv-NF5280M5:/home/sv/桌面$ ./main
段错误 (核心已转储)

此时编译运行的时候是没有 core 文件的,是因为我们没有配置生成 core 文件。

2. 生成 core 文件

**Core 文件是一种特殊的文件类型,通常在程序崩溃时由操作系统生成。它包含了程序崩溃时的内存镜像和程序状态信息,**比如程序计数器、堆栈指针、寄存器状态等。这些信息可以用来调试程序,找出导致程序崩溃的原因。

Core 文件的名字通常为 “core”,但也可以通过操作系统的设置来改变。在 Unix 和类 Unix 系统(比如 Linux)中,你可以使用 ulimit 命令来控制是否生成 core 文件,以及 core 文件的大小。

1
2
3
我们先使用如下命令查看 core 文件大小,若结果为 0 表示还没有生成 core 文件。

ulimit -c // 查看 core 文件大小

查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(base) sv@sv-NF5280M5:/home/sv/桌面$ ulimit -c
0
(base) sv@sv-NF5280M5:/home/sv/桌面$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 255799
max locked memory (kbytes, -l) 65536
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 255799
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited

使用如下命令修改 core 文件大小不受限制:

1
ulimit -c unlimited  // 修改 core 文件大小不受限制

但是以下并未查找到core文件。

1
2
3
4
5
6
7
8
9
10
(base) sv@sv-NF5280M5:/home/sv/桌面$ g++ -g -o main main.cpp -lpthread -fpermissive
main.cpp: In function ‘int recursion(int)’:
main.cpp:5:1: warning: no return statement in function returning non-void [-Wreturn-type]
5 | }
| ^
(base) sv@sv-NF5280M5:/home/sv/桌面$ ./main
段错误 (核心已转储)
(base) sv@sv-NF5280M5:/home/sv/桌面$ ls
'未命名 1.ods' '未命名 2.odt' lu3018gtmnzo.tmp main.c params.odt
'未命名 1.odt' index.html main main.cpp proxy.txt

但是当前目录下没有看到core文件的生成

这是因为core文件的默认生成路径不对,只要发生段错误时,括号里出现了core dumped就代表core文件已生成。

可以通过以下命令查看core文件的存放路径:

1
cat /proc/sys/kernel/core_pattern
1
(base) sv@sv-NF5280M5:/home/sv/桌面$ cat /proc/sys/kernel/core_pattern

修改core文件生成路径为当前目录下,输入命令:

1
sudo bash -c "echo core > /proc/sys/kernel/core_pattern "

注意使用root用户权限

修改后,core文件就会在当前目录下生成。

查看

1
2
3
base) sv@sv-NF5280M5:/home/sv/桌面$ ls
'未命名 1.ods' '未命名 2.odt' index.html main main.cpp proxy.txt
'未命名 1.odt' core lu3018gtmnzo.tmp main.c params.odt

3.调试 core 文件

1
(base) sv@sv-NF5280M5:/home/sv/桌面$ gdb main core

接下来可以打印当前堆栈信息分析问题:

1
backtrace  // 打印当前堆栈信息,backtrace 可以用 bt 的缩写替代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
4           recursion(n - 1);
(gdb) bt
#0 0x000055fb7cc74180 in recursion (n=-261907) at main.cpp:4
#1 0x000055fb7cc74185 in recursion (n=-261906) at main.cpp:4
#2 0x000055fb7cc74185 in recursion (n=-261905) at main.cpp:4
#3 0x000055fb7cc74185 in recursion (n=-261904) at main.cpp:4
#4 0x000055fb7cc74185 in recursion (n=-261903) at main.cpp:4
#5 0x000055fb7cc74185 in recursion (n=-261902) at main.cpp:4
#6 0x000055fb7cc74185 in recursion (n=-261901) at main.cpp:4
#7 0x000055fb7cc74185 in recursion (n=-261900) at main.cpp:4
#8 0x000055fb7cc74185 in recursion (n=-261899) at main.cpp:4
#9 0x000055fb7cc74185 in recursion (n=-261898) at main.cpp:4
#10 0x000055fb7cc74185 in recursion (n=-261897) at main.cpp:4
#11 0x000055fb7cc74185 in recursion (n=-261896) at main.cpp:4
#12 0x000055fb7cc74185 in recursion (n=-261895) at main.cpp:4
#13 0x000055fb7cc74185 in recursion (n=-261894) at main.cpp:4
#14 0x000055fb7cc74185 in recursion (n=-261893) at main.cpp:4
#15 0x000055fb7cc74185 in recursion (n=-261892) at main.cpp:4
#16 0x000055fb7cc74185 in recursion (n=-261891) at main.cpp:4
#17 0x000055fb7cc74185 in recursion (n=-261890) at main.cpp:4
#18 0x000055fb7cc74185 in recursion (n=-261889) at main.cpp:4
#19 0x000055fb7cc74185 in recursion (n=-261888) at main.cpp:4
#20 0x000055fb7cc74185 in recursion (n=-261887) at main.cpp:4

常使用的命令

1
2
3
4
5
6
7
bt 或 backtrace: 显示当前线程的调用栈跟踪。
info threads: 列出所有线程。
thread n: 切换到指定的线程号,n为指定的线程号
list: 显示当前执行的源代码。
print x: 打印变量或表达式的值,x为变量或表达式
frame f: 切换到特定的栈帧。f为指定的栈帧号
thread apply all bt: 打印所有堆栈信息

参考文献