GDB 调试工具入门教程:从零开始手把手教你调试 C/C++ 程序
一、前言
在学习 C/C++、Linux 编程、嵌入式开发、ROS 开发或者操作系统相关课程时,很多同学都会遇到一个问题:
程序能编译,但是运行结果不对;
程序运行一半突然崩溃;
出现 Segmentation fault,却不知道错在哪里。
如果只靠 printf 一行一行打印变量,虽然也能排查问题,但效率很低。尤其当程序结构复杂、函数调用层级很多时,单纯依赖打印会变得非常麻烦。
这时就需要使用专业的调试工具:GDB。
GDB 是 Linux 环境下非常常用的调试器,它可以让程序在指定位置暂停,查看变量值,单步执行代码,进入函数内部,查看函数调用栈,还能定位段错误等问题。
本文将从零开始,带你一步一步学习 GDB 的基本使用方法。
二、GDB 是什么?
GDB 的全称是 GNU Debugger,中文通常叫做 GNU 调试器。
它主要用于调试 C、C++ 等程序。
我们可以这样理解:
平时运行程序时,程序是从头到尾连续执行的,中间发生了什么我们看不到。而使用 GDB 后,我们可以控制程序的执行过程,让它:
- 在某一行代码处停下来;
- 一行一行地执行;
- 查看变量当前的值;
- 查看函数是如何调用的;
- 查看程序崩溃在哪一行;
- 修改程序运行过程中的变量值;
- 分析段错误、死循环、数组越界等问题。
简单来说:
GDB 的作用就是让程序“慢下来”,让我们看清楚程序每一步到底做了什么。
三、安装 GDB
如果你使用的是 Ubuntu 或 Debian 系统,可以使用下面命令安装:
sudo apt update
sudo apt install gdb
安装完成后,查看版本:
gdb --version
如果能看到类似下面的信息,说明安装成功:
GNU gdb (Ubuntu 版本号)
Copyright (C) Free Software Foundation, Inc.
四、准备一个测试程序
为了方便学习,我们先写一个简单的 C 程序。
创建一个文件夹:
mkdir gdb_demo
cd gdb_demo
新建源文件:
vim main.c
如果不会使用 vim,也可以用 nano:
nano main.c
输入下面代码:
#include <stdio.h>
int add(int a, int b)
{
int result = a + b;
return result;
}
int main()
{
int x = 10;
int y = 20;
int sum = add(x, y);
printf("sum = %d\n", sum);
return 0;
}
这段代码很简单,主要功能是:
- 在
main函数中定义两个整数x和y; - 调用
add函数计算两个数的和; - 将结果保存到
sum; - 最后打印结果。
五、编译程序:一定要加 -g
普通编译命令是:
gcc main.c -o main
这样虽然可以生成可执行文件,但不适合调试。
如果要使用 GDB 调试,编译时必须加上 -g 参数:
gcc -g main.c -o main
这里的 -g 表示加入调试信息。
调试信息中包含:
- 源代码文件名;
- 代码行号;
- 函数名;
- 变量名;
- 类型信息。
如果没有 -g,GDB 仍然可以打开程序,但是调试体验会很差,可能看不到源码,也不能方便地查看变量。
为了让调试结果更准确,建议初学时使用:
gcc -g -O0 main.c -o main
其中:
-g 表示加入调试信息
-O0 表示关闭编译优化
为什么要关闭优化?
因为编译器优化后,代码的实际执行顺序可能和源代码不完全一致,某些变量也可能被优化掉。对于初学者来说,关闭优化更容易理解程序执行过程。
运行程序:
./main
输出结果:
sum = 30
六、启动 GDB
使用下面命令启动 GDB:
gdb ./main
进入 GDB 后,会看到类似下面的提示:
(gdb)
这说明你已经进入了 GDB 的命令环境。
在这个环境中,我们输入的不再是普通 Linux 命令,而是 GDB 的调试命令。
七、GDB 调试的基本流程
GDB 的基本调试流程一般是:
1. 启动 GDB
2. 设置断点
3. 运行程序
4. 程序停在断点处
5. 单步执行代码
6. 查看变量
7. 继续运行或退出
下面我们一步一步来操作。
八、设置断点
断点的作用是:
让程序运行到指定位置时自动暂停。
例如我们想让程序一进入 main 函数就暂停,可以输入:
break main
也可以使用简写:
b main
GDB 可能会显示:
Breakpoint 1 at 0x0000000000001149: file main.c, line 10.
这句话表示:
- 成功设置了一个断点;
- 断点编号是 1;
- 断点位置在
main.c文件; - 对应源代码第 10 行。
除了按函数名设置断点,也可以按行号设置断点。
例如:
break 14
或者:
b main.c:14
表示在 main.c 的第 14 行设置断点。
九、查看源码
在 GDB 中可以使用 list 命令查看源码:
list
简写为:
l
如果想查看 main 函数附近的代码:
list main
如果想查看第 12 行附近的代码:
list 12
GDB 会显示类似:
8
9 int main()
10 {
11 int x = 10;
12 int y = 20;
13
14 int sum = add(x, y);
15
16 printf("sum = %d\n", sum);
17
这样就可以确认断点位置是否正确。
十、运行程序
设置好断点后,输入:
run
也可以简写:
r
程序开始运行,并在 main 函数处停下来。
你可能会看到:
Breakpoint 1, main () at main.c:11
11 int x = 10;
这句话很重要,意思是:
程序现在停在
main.c第 11 行,这一行代码即将执行,但还没有执行。
也就是说,此时 int x = 10; 还没有真正执行。
十一、单步执行:next 命令
如果想让程序执行当前这一行,然后停到下一行,可以使用:
next
简写为:
n
例如当前停在:
int x = 10;
输入:
n
程序会执行这一行,然后停到下一行:
int y = 20;
这个时候,变量 x 已经被赋值为 10。
十二、查看变量:print 命令
查看变量值使用:
print 变量名
简写为:
p 变量名
例如查看 x:
p x
输出可能是:
$1 = 10
这里的 $1 是 GDB 给这次输出结果起的编号,不是变量名。
继续执行一行:
n
然后查看 y:
p y
输出:
$2 = 20
查看 sum:
p sum
如果此时 sum 那一行还没有执行,它的值可能是不确定的。因为变量虽然已经声明,但还没有完成赋值。
所以要记住一个原则:
GDB 显示的当前行通常是“即将执行的行”,不是“已经执行完的行”。
十三、进入函数内部:step 命令
现在程序会执行到这一行:
int sum = add(x, y);
这里调用了 add 函数。
如果使用:
n
GDB 会把这一行整体执行完,不会进入 add 函数内部。
如果想进入 add 函数中查看执行过程,需要使用:
step
简写为:
s
当程序停在:
int sum = add(x, y);
输入:
s
程序会进入 add 函数:
int add(int a, int b)
{
int result = a + b;
return result;
}
此时可以查看函数参数:
p a
p b
输出:
$3 = 10
$4 = 20
说明 main 函数中的 x 和 y 已经传给了 add 函数的参数 a 和 b。
十四、next 和 step 的区别
初学 GDB 时,很多人会分不清 next 和 step。
它们的区别如下:
| 命令 | 简写 | 作用 |
|---|---|---|
| next | n | 执行下一行,遇到函数不会进入 |
| step | s | 执行下一步,遇到函数会进入函数内部 |
举例:
int sum = add(x, y);
如果使用:
n
GDB 会直接执行完整个 add(x, y),然后停到下一行。
如果使用:
s
GDB 会进入 add 函数内部,让你看到函数里面是怎么执行的。
简单记忆:
next:看表面
step:进里面
十五、跳出当前函数:finish 命令
当我们进入 add 函数后,如果不想一行一行执行了,可以使用:
finish
这个命令的作用是:
执行完当前函数,并回到调用这个函数的位置。
例如在 add 函数中输入:
finish
GDB 可能显示:
Run till exit from #0 add (a=10, b=20) at main.c:6
Value returned is $5 = 30
这表示 add 函数执行结束,返回值是 30。
十六、继续运行:continue 命令
如果程序停在某个断点,或者单步执行过程中暂停了,你想让程序继续运行,可以输入:
continue
简写:
c
程序会继续运行,直到:
- 遇到下一个断点;
- 程序正常结束;
- 程序发生错误;
- 用户手动中断。
在当前例子中,如果没有其他断点,输入:
c
程序会继续执行并输出:
sum = 30
然后程序结束。
十七、查看所有断点
查看当前设置了哪些断点,可以使用:
info breakpoints
也可以简写:
info b
显示结果类似:
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000001149 in main at main.c:11
其中:
| 字段 | 含义 |
|---|---|
| Num | 断点编号 |
| Type | 类型 |
| Enb | 是否启用 |
| What | 断点位置 |
十八、删除断点
删除指定断点:
delete 1
也可以简写:
d 1
这里的 1 是断点编号。
如果想删除所有断点:
delete
GDB 会询问是否确认删除。
十九、禁用和启用断点
有时我们不想删除断点,只是暂时不用它,可以禁用断点:
disable 1
重新启用:
enable 1
这样做的好处是,调试复杂程序时不需要频繁删除和重新设置断点。
二十、自动显示变量:display 命令
如果每执行一步都要手动输入:
p x
会比较麻烦。
这时可以使用 display 命令,让 GDB 每次暂停时自动显示变量值。
例如:
display x
display y
display sum
之后每次执行:
n
GDB 都会自动显示这些变量的值。
查看已经设置的自动显示项:
info display
取消某个自动显示项:
undisplay 1
其中 1 是 display 编号。
二十一、查看局部变量
如果当前函数里有很多局部变量,一个一个 print 会比较麻烦。
可以使用:
info locals
这个命令会显示当前函数中的所有局部变量。
例如在 main 函数中,可能会显示:
x = 10
y = 20
sum = 30
查看当前函数参数:
info args
例如在 add 函数中,可能会显示:
a = 10
b = 20
二十二、查看函数调用栈:backtrace 命令
函数调用栈可以帮助我们知道:
当前代码是从哪些函数一步一步调用过来的。
使用命令:
backtrace
简写:
bt
如果当前程序停在 add 函数中,输入:
bt
可能会看到:
#0 add (a=10, b=20) at main.c:5
#1 main () at main.c:14
这表示:
- 当前正在执行的是
add函数; add函数是被main函数调用的;- 调用位置在
main.c第 14 行。
调用栈在调试复杂程序时非常重要,尤其是程序崩溃时,可以通过 bt 快速看到崩溃是从哪里一路调用过来的。
二十三、切换栈帧:frame 命令
当使用 bt 查看调用栈后,可以使用 frame 命令切换到不同的函数调用层级。
例如:
#0 add (a=10, b=20) at main.c:5
#1 main () at main.c:14
如果当前在 #0,也就是 add 函数中。
切换到 main 函数:
frame 1
简写:
f 1
然后可以查看 main 函数中的变量:
p x
p y
p sum
如果想回到 add 函数:
frame 0
二十四、调试段错误 Segmentation fault
GDB 最常用的场景之一,就是排查段错误。
下面写一个会产生段错误的程序。
创建文件:
vim seg.c
输入代码:
#include <stdio.h>
int main()
{
int *p = NULL;
*p = 100;
printf("value = %d\n", *p);
return 0;
}
编译:
gcc -g -O0 seg.c -o seg
直接运行:
./seg
程序会报错:
Segmentation fault
这说明程序访问了非法内存。
接下来用 GDB 调试:
gdb ./seg
运行:
run
GDB 会提示:
Program received signal SIGSEGV, Segmentation fault.
main () at seg.c:7
7 *p = 100;
这说明程序在第 7 行崩溃:
*p = 100;
查看指针 p:
p p
输出:
$1 = (int *) 0x0
0x0 就是空地址,也就是 NULL。
所以错误原因是:
指针 p 是空指针,却对它进行了解引用操作。
错误代码:
int *p = NULL;
*p = 100;
正确写法之一:
int value = 0;
int *p = &value;
*p = 100;
完整修改后:
#include <stdio.h>
int main()
{
int value = 0;
int *p = &value;
*p = 100;
printf("value = %d\n", *p);
return 0;
}
二十五、调试数组越界问题
再看一个常见错误:数组越界。
创建文件:
vim array.c
输入代码:
#include <stdio.h>
int main()
{
int arr[3] = {1, 2, 3};
for (int i = 0; i <= 3; i++)
{
printf("arr[%d] = %d\n", i, arr[i]);
}
return 0;
}
编译:
gcc -g -O0 array.c -o array
使用 GDB:
gdb ./array
设置断点:
b main
运行:
r
单步执行:
n
n
当进入循环后,可以查看 i:
p i
查看数组:
p arr
查看指定元素:
p arr[0]
p arr[1]
p arr[2]
p arr[3]
数组定义是:
int arr[3] = {1, 2, 3};
它只有 3 个元素,下标分别是:
arr[0]
arr[1]
arr[2]
但是循环条件写成了:
i <= 3
当 i 等于 3 时,会访问:
arr[3]
这就是数组越界。
正确写法应该是:
for (int i = 0; i < 3; i++)
所以这类问题的调试思路是:
- 在循环处设置断点;
- 单步执行;
- 查看循环变量;
- 查看数组下标;
- 判断是否访问了非法位置。
二十六、调试死循环
死循环也是常见问题。
创建文件:
vim loop.c
输入代码:
#include <stdio.h>
int main()
{
int i = 0;
while (i < 5)
{
printf("i = %d\n", i);
}
return 0;
}
编译:
gcc -g -O0 loop.c -o loop
运行:
./loop
你会发现程序一直输出:
i = 0
i = 0
i = 0
程序不会停止。
这时用 GDB 调试:
gdb ./loop
运行:
run
如果程序一直执行,可以按:
Ctrl + C
这会让 GDB 暂停正在运行的程序。
暂停后输入:
bt
查看程序当前停在哪里。
再输入:
list
查看附近代码。
可以看到循环部分:
while (i < 5)
{
printf("i = %d\n", i);
}
问题是变量 i 一直没有变化,所以 i < 5 永远成立。
正确写法:
while (i < 5)
{
printf("i = %d\n", i);
i++;
}
二十七、条件断点
如果程序中有一个循环执行很多次,我们不想每次循环都停,只想在某个条件满足时停,可以使用条件断点。
例如:
#include <stdio.h>
int main()
{
for (int i = 0; i < 10; i++)
{
printf("i = %d\n", i);
}
return 0;
}
编译:
gcc -g -O0 condition.c -o condition
进入 GDB:
gdb ./condition
如果想让程序在 i == 5 时停下,可以设置条件断点:
b 7 if i == 5
这里假设第 7 行是:
printf("i = %d\n", i);
运行:
r
程序会在 i 等于 5 的时候暂停。
条件断点适合调试:
- 循环次数很多的程序;
- 某个变量达到特定值才出错的程序;
- 数组处理、链表遍历、数据查找等场景。
二十八、监视变量变化:watch 命令
有时候我们会遇到这种问题:
某个变量的值突然变错了,但不知道是哪一行代码改坏的。
这时可以使用 watch 命令。
例如:
watch sum
表示监视变量 sum。
只要 sum 的值发生变化,程序就会自动暂停。
例如:
int sum = 0;
sum = 10;
sum = 20;
如果设置了:
watch sum
当 sum 被修改时,GDB 会停下来,并告诉你变量旧值和新值。
这对排查变量被意外修改的问题非常有用。
二十九、修改变量值
GDB 不仅能查看变量值,还能在程序运行过程中修改变量。
例如当前程序中:
int x = 10;
int y = 20;
在 GDB 中可以输入:
set var x = 100
然后查看:
p x
输出:
$1 = 100
这说明变量 x 已经被修改了。
这种功能适合测试不同分支,比如:
if (score >= 60)
{
printf("pass\n");
}
else
{
printf("fail\n");
}
你可以在 GDB 中直接修改:
set var score = 59
或者:
set var score = 90
这样不用反复改代码、重新编译,就可以测试不同情况。
三十、调试带命令行参数的程序
有些程序运行时需要参数,例如:
./main hello world
下面写一个示例程序。
创建文件:
vim args.c
输入:
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("argc = %d\n", argc);
for (int i = 0; i < argc; i++)
{
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
编译:
gcc -g -O0 args.c -o args
普通运行:
./args hello world
使用 GDB 调试时,有两种传参方式。
第一种,直接在 run 后面加参数:
run hello world
第二种,先设置参数:
set args hello world
run
查看当前参数:
show args
三十一、查看内存
GDB 还可以查看内存内容,常用命令是:
x
x 是 examine 的意思,表示检查内存。
常见格式:
x/数量格式单位 地址
例如:
x/4xw &x
含义如下:
| 部分 | 含义 |
|---|---|
| x | examine,查看内存 |
| /4 | 查看 4 个单位 |
| x | 用十六进制显示 |
| w | word,4 字节 |
| &x | 变量 x 的地址 |
常见显示格式:
| 格式 | 含义 |
|---|---|
| x | 十六进制 |
| d | 有符号十进制 |
| u | 无符号十进制 |
| c | 字符 |
| s | 字符串 |
| i | 汇编指令 |
例如查看变量地址:
p &x
查看变量附近的内存:
x/4xw &x
查看字符串:
x/s str
查看数组中的 10 个整数:
x/10dw arr
这里:
10 表示查看 10 个
d 表示十进制
w 表示每个单位 4 字节
三十二、查看汇编代码
如果学习操作系统、嵌入式、汇编或底层调试,可以使用 GDB 查看汇编代码。
反汇编当前函数:
disassemble
反汇编指定函数:
disassemble main
显示当前指令:
x/i $pc
其中 $pc 表示当前程序计数器,也就是 CPU 正在执行的位置。
如果想让 GDB 显示源码窗口,可以使用:
layout src
显示汇编窗口:
layout asm
退出这个界面:
Ctrl + X
然后按 A
也就是先按住 Ctrl 再按 X,松开后再按 A。
三十三、常见 GDB 报错和解决方法
1. No debugging symbols found
如果启动 GDB 时看到:
No debugging symbols found
说明编译时没有加 -g。
错误编译方式:
gcc main.c -o main
正确编译方式:
gcc -g -O0 main.c -o main
2. No symbol table is loaded
这个问题通常也是因为没有调试信息。
解决方法:
gcc -g -O0 main.c -o main
gdb ./main
3. 程序没有停在断点
可能原因有:
- 断点所在代码根本没有被执行;
- 没有加
-g; - 编译时开启了优化;
- GDB 加载的不是刚刚编译的程序。
建议重新编译:
gcc -g -O0 main.c -o main
4. Cannot access memory at address 0x0
这个错误通常和空指针有关。
例如:
int *p = NULL;
*p = 10;
解决方法是:在使用指针前,确保它指向有效地址。
5. Segmentation fault
段错误常见原因包括:
- 空指针解引用;
- 数组越界;
- 使用已经释放的内存;
- 栈溢出;
- 字符串越界写入;
- 访问非法地址。
遇到段错误时,最推荐的做法是:
gdb ./程序名
然后:
run
bt
通常可以直接定位到出错位置。
三十四、GDB 常用命令速查表
| 命令 | 简写 | 作用 |
|---|---|---|
| gdb ./main | 无 | 启动 GDB |
| break main | b main | 在 main 函数设置断点 |
| break 10 | b 10 | 在第 10 行设置断点 |
| run | r | 运行程序 |
| next | n | 单步执行,不进入函数 |
| step | s | 单步执行,进入函数 |
| continue | c | 继续运行 |
| print x | p x | 查看变量 x |
| display x | 无 | 每次暂停自动显示 x |
| info locals | 无 | 查看局部变量 |
| info args | 无 | 查看函数参数 |
| backtrace | bt | 查看函数调用栈 |
| frame 1 | f 1 | 切换到第 1 层栈帧 |
| finish | 无 | 执行完当前函数并返回 |
| info breakpoints | info b | 查看所有断点 |
| delete 1 | d 1 | 删除编号为 1 的断点 |
| disable 1 | 无 | 禁用编号为 1 的断点 |
| enable 1 | 无 | 启用编号为 1 的断点 |
| watch x | 无 | 监视变量 x 的变化 |
| set var x=10 | 无 | 修改变量 x 的值 |
| list | l | 查看源码 |
| quit | q | 退出 GDB |
| 最后再记住一句话: |
GDB 调试的核心不是背命令,而是看清楚程序每一步执行到了哪里、变量发生了什么变化、错误是从哪一步开始出现的。
只要掌握了这个思路,后面无论是调试 C/C++ 程序、Linux 程序、嵌入式程序,还是 ROS 工程,GDB 都会成为非常有用的工具。
更多推荐


所有评论(0)