GDB 调试工具入门教程:从零开始手把手教你调试 C/C++ 程序

John Doe Lv2

一、前言

在学习 C/C++、Linux 编程、嵌入式开发、ROS 开发或者操作系统相关课程时,很多同学都会遇到一个问题:

程序能编译,但是运行结果不对;
程序运行一半突然崩溃;
出现 Segmentation fault,却不知道错在哪里。

如果只靠 printf 一行一行打印变量,虽然也能排查问题,但效率很低。尤其当程序结构复杂、函数调用层级很多时,单纯依赖打印会变得非常麻烦。

这时就需要使用专业的调试工具:GDB

GDB 是 Linux 环境下非常常用的调试器,它可以让程序在指定位置暂停,查看变量值,单步执行代码,进入函数内部,查看函数调用栈,还能定位段错误等问题。

本文将从零开始,带你一步一步学习 GDB 的基本使用方法。


二、GDB 是什么?

GDB 的全称是 GNU Debugger,中文通常叫做 GNU 调试器。

它主要用于调试 C、C++ 等程序。

我们可以这样理解:

平时运行程序时,程序是从头到尾连续执行的,中间发生了什么我们看不到。而使用 GDB 后,我们可以控制程序的执行过程,让它:

  1. 在某一行代码处停下来;
  2. 一行一行地执行;
  3. 查看变量当前的值;
  4. 查看函数是如何调用的;
  5. 查看程序崩溃在哪一行;
  6. 修改程序运行过程中的变量值;
  7. 分析段错误、死循环、数组越界等问题。

简单来说:

GDB 的作用就是让程序“慢下来”,让我们看清楚程序每一步到底做了什么。


三、安装 GDB

如果你使用的是 Ubuntu 或 Debian 系统,可以使用下面命令安装:

1
2
sudo apt update
sudo apt install gdb

安装完成后,查看版本:

1
gdb --version

如果能看到类似下面的信息,说明安装成功:

1
2
GNU gdb (Ubuntu 版本号)
Copyright (C) Free Software Foundation, Inc.

四、准备一个测试程序

为了方便学习,我们先写一个简单的 C 程序。

创建一个文件夹:

1
2
mkdir gdb_demo
cd gdb_demo

新建源文件:

1
vim main.c

如果不会使用 vim,也可以用 nano

1
nano main.c

输入下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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;
}

这段代码很简单,主要功能是:

  1. main 函数中定义两个整数 xy
  2. 调用 add 函数计算两个数的和;
  3. 将结果保存到 sum
  4. 最后打印结果。

五、编译程序:一定要加 -g

普通编译命令是:

1
gcc main.c -o main

这样虽然可以生成可执行文件,但不适合调试。

如果要使用 GDB 调试,编译时必须加上 -g 参数:

1
gcc -g main.c -o main

这里的 -g 表示加入调试信息。

调试信息中包含:

  1. 源代码文件名;
  2. 代码行号;
  3. 函数名;
  4. 变量名;
  5. 类型信息。

如果没有 -g,GDB 仍然可以打开程序,但是调试体验会很差,可能看不到源码,也不能方便地查看变量。

为了让调试结果更准确,建议初学时使用:

1
gcc -g -O0 main.c -o main

其中:

1
2
-g   表示加入调试信息
-O0 表示关闭编译优化

为什么要关闭优化?

因为编译器优化后,代码的实际执行顺序可能和源代码不完全一致,某些变量也可能被优化掉。对于初学者来说,关闭优化更容易理解程序执行过程。

运行程序:

1
./main

输出结果:

1
sum = 30

六、启动 GDB

使用下面命令启动 GDB:

1
gdb ./main

进入 GDB 后,会看到类似下面的提示:

1
(gdb)

这说明你已经进入了 GDB 的命令环境。

在这个环境中,我们输入的不再是普通 Linux 命令,而是 GDB 的调试命令。


七、GDB 调试的基本流程

GDB 的基本调试流程一般是:

1
2
3
4
5
6
7
1. 启动 GDB
2. 设置断点
3. 运行程序
4. 程序停在断点处
5. 单步执行代码
6. 查看变量
7. 继续运行或退出

下面我们一步一步来操作。


八、设置断点

断点的作用是:

让程序运行到指定位置时自动暂停。

例如我们想让程序一进入 main 函数就暂停,可以输入:

1
break main

也可以使用简写:

1
b main

GDB 可能会显示:

1
Breakpoint 1 at 0x0000000000001149: file main.c, line 10.

这句话表示:

  1. 成功设置了一个断点;
  2. 断点编号是 1;
  3. 断点位置在 main.c 文件;
  4. 对应源代码第 10 行。

除了按函数名设置断点,也可以按行号设置断点。

例如:

1
break 14

或者:

1
b main.c:14

表示在 main.c 的第 14 行设置断点。


九、查看源码

在 GDB 中可以使用 list 命令查看源码:

1
list

简写为:

1
l

如果想查看 main 函数附近的代码:

1
list main

如果想查看第 12 行附近的代码:

1
list 12

GDB 会显示类似:

1
2
3
4
5
6
7
8
9
10
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

这样就可以确认断点位置是否正确。


十、运行程序

设置好断点后,输入:

1
run

也可以简写:

1
r

程序开始运行,并在 main 函数处停下来。

你可能会看到:

1
2
Breakpoint 1, main () at main.c:11
11 int x = 10;

这句话很重要,意思是:

程序现在停在 main.c 第 11 行,这一行代码即将执行,但还没有执行。

也就是说,此时 int x = 10; 还没有真正执行。


十一、单步执行:next 命令

如果想让程序执行当前这一行,然后停到下一行,可以使用:

1
next

简写为:

1
n

例如当前停在:

1
int x = 10;

输入:

1
n

程序会执行这一行,然后停到下一行:

1
int y = 20;

这个时候,变量 x 已经被赋值为 10。


十二、查看变量:print 命令

查看变量值使用:

1
print 变量名

简写为:

1
p 变量名

例如查看 x

1
p x

输出可能是:

1
$1 = 10

这里的 $1 是 GDB 给这次输出结果起的编号,不是变量名。

继续执行一行:

1
n

然后查看 y

1
p y

输出:

1
$2 = 20

查看 sum

1
p sum

如果此时 sum 那一行还没有执行,它的值可能是不确定的。因为变量虽然已经声明,但还没有完成赋值。

所以要记住一个原则:

GDB 显示的当前行通常是“即将执行的行”,不是“已经执行完的行”。


十三、进入函数内部:step 命令

现在程序会执行到这一行:

1
int sum = add(x, y);

这里调用了 add 函数。

如果使用:

1
n

GDB 会把这一行整体执行完,不会进入 add 函数内部。

如果想进入 add 函数中查看执行过程,需要使用:

1
step

简写为:

1
s

当程序停在:

1
int sum = add(x, y);

输入:

1
s

程序会进入 add 函数:

1
2
3
4
5
int add(int a, int b)
{
int result = a + b;
return result;
}

此时可以查看函数参数:

1
2
p a
p b

输出:

1
2
$3 = 10
$4 = 20

说明 main 函数中的 xy 已经传给了 add 函数的参数 ab


十四、next 和 step 的区别

初学 GDB 时,很多人会分不清 nextstep

它们的区别如下:

命令简写作用
nextn执行下一行,遇到函数不会进入
steps执行下一步,遇到函数会进入函数内部

举例:

1
int sum = add(x, y);

如果使用:

1
n

GDB 会直接执行完整个 add(x, y),然后停到下一行。

如果使用:

1
s

GDB 会进入 add 函数内部,让你看到函数里面是怎么执行的。

简单记忆:

1
2
next:看表面
step:进里面

十五、跳出当前函数:finish 命令

当我们进入 add 函数后,如果不想一行一行执行了,可以使用:

1
finish

这个命令的作用是:

执行完当前函数,并回到调用这个函数的位置。

例如在 add 函数中输入:

1
finish

GDB 可能显示:

1
2
Run till exit from #0 add (a=10, b=20) at main.c:6
Value returned is $5 = 30

这表示 add 函数执行结束,返回值是 30。


十六、继续运行:continue 命令

如果程序停在某个断点,或者单步执行过程中暂停了,你想让程序继续运行,可以输入:

1
continue

简写:

1
c

程序会继续运行,直到:

  1. 遇到下一个断点;
  2. 程序正常结束;
  3. 程序发生错误;
  4. 用户手动中断。

在当前例子中,如果没有其他断点,输入:

1
c

程序会继续执行并输出:

1
sum = 30

然后程序结束。


十七、查看所有断点

查看当前设置了哪些断点,可以使用:

1
info breakpoints

也可以简写:

1
info b

显示结果类似:

1
2
Num     Type           Disp Enb Address            What
1 breakpoint keep y 0x0000000000001149 in main at main.c:11

其中:

字段含义
Num断点编号
Type类型
Enb是否启用
What断点位置

十八、删除断点

删除指定断点:

1
delete 1

也可以简写:

1
d 1

这里的 1 是断点编号。

如果想删除所有断点:

1
delete

GDB 会询问是否确认删除。


十九、禁用和启用断点

有时我们不想删除断点,只是暂时不用它,可以禁用断点:

1
disable 1

重新启用:

1
enable 1

这样做的好处是,调试复杂程序时不需要频繁删除和重新设置断点。


二十、自动显示变量:display 命令

如果每执行一步都要手动输入:

1
p x

会比较麻烦。

这时可以使用 display 命令,让 GDB 每次暂停时自动显示变量值。

例如:

1
2
3
display x
display y
display sum

之后每次执行:

1
n

GDB 都会自动显示这些变量的值。

查看已经设置的自动显示项:

1
info display

取消某个自动显示项:

1
undisplay 1

其中 1 是 display 编号。


二十一、查看局部变量

如果当前函数里有很多局部变量,一个一个 print 会比较麻烦。

可以使用:

1
info locals

这个命令会显示当前函数中的所有局部变量。

例如在 main 函数中,可能会显示:

1
2
3
x = 10
y = 20
sum = 30

查看当前函数参数:

1
info args

例如在 add 函数中,可能会显示:

1
2
a = 10
b = 20

二十二、查看函数调用栈:backtrace 命令

函数调用栈可以帮助我们知道:

当前代码是从哪些函数一步一步调用过来的。

使用命令:

1
backtrace

简写:

1
bt

如果当前程序停在 add 函数中,输入:

1
bt

可能会看到:

1
2
#0  add (a=10, b=20) at main.c:5
#1 main () at main.c:14

这表示:

  1. 当前正在执行的是 add 函数;
  2. add 函数是被 main 函数调用的;
  3. 调用位置在 main.c 第 14 行。

调用栈在调试复杂程序时非常重要,尤其是程序崩溃时,可以通过 bt 快速看到崩溃是从哪里一路调用过来的。


二十三、切换栈帧:frame 命令

当使用 bt 查看调用栈后,可以使用 frame 命令切换到不同的函数调用层级。

例如:

1
2
#0  add (a=10, b=20) at main.c:5
#1 main () at main.c:14

如果当前在 #0,也就是 add 函数中。

切换到 main 函数:

1
frame 1

简写:

1
f 1

然后可以查看 main 函数中的变量:

1
2
3
p x
p y
p sum

如果想回到 add 函数:

1
frame 0

二十四、调试段错误 Segmentation fault

GDB 最常用的场景之一,就是排查段错误。

下面写一个会产生段错误的程序。

创建文件:

1
vim seg.c

输入代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main()
{
int *p = NULL;

*p = 100;

printf("value = %d\n", *p);

return 0;
}

编译:

1
gcc -g -O0 seg.c -o seg

直接运行:

1
./seg

程序会报错:

1
Segmentation fault

这说明程序访问了非法内存。

接下来用 GDB 调试:

1
gdb ./seg

运行:

1
run

GDB 会提示:

1
2
3
Program received signal SIGSEGV, Segmentation fault.
main () at seg.c:7
7 *p = 100;

这说明程序在第 7 行崩溃:

1
*p = 100;

查看指针 p

1
p p

输出:

1
$1 = (int *) 0x0

0x0 就是空地址,也就是 NULL

所以错误原因是:

指针 p 是空指针,却对它进行了解引用操作。

错误代码:

1
2
int *p = NULL;
*p = 100;

正确写法之一:

1
2
3
4
int value = 0;
int *p = &value;

*p = 100;

完整修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main()
{
int value = 0;
int *p = &value;

*p = 100;

printf("value = %d\n", *p);

return 0;
}

二十五、调试数组越界问题

再看一个常见错误:数组越界。

创建文件:

1
vim array.c

输入代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#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;
}

编译:

1
gcc -g -O0 array.c -o array

使用 GDB:

1
gdb ./array

设置断点:

1
b main

运行:

1
r

单步执行:

1
2
n
n

当进入循环后,可以查看 i

1
p i

查看数组:

1
p arr

查看指定元素:

1
2
3
4
p arr[0]
p arr[1]
p arr[2]
p arr[3]

数组定义是:

1
int arr[3] = {1, 2, 3};

它只有 3 个元素,下标分别是:

1
2
3
arr[0]
arr[1]
arr[2]

但是循环条件写成了:

1
i <= 3

i 等于 3 时,会访问:

1
arr[3]

这就是数组越界。

正确写法应该是:

1
for (int i = 0; i < 3; i++)

所以这类问题的调试思路是:

  1. 在循环处设置断点;
  2. 单步执行;
  3. 查看循环变量;
  4. 查看数组下标;
  5. 判断是否访问了非法位置。

二十六、调试死循环

死循环也是常见问题。

创建文件:

1
vim loop.c

输入代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main()
{
int i = 0;

while (i < 5)
{
printf("i = %d\n", i);
}

return 0;
}

编译:

1
gcc -g -O0 loop.c -o loop

运行:

1
./loop

你会发现程序一直输出:

1
2
3
i = 0
i = 0
i = 0

程序不会停止。

这时用 GDB 调试:

1
gdb ./loop

运行:

1
run

如果程序一直执行,可以按:

1
Ctrl + C

这会让 GDB 暂停正在运行的程序。

暂停后输入:

1
bt

查看程序当前停在哪里。

再输入:

1
list

查看附近代码。

可以看到循环部分:

1
2
3
4
while (i < 5)
{
printf("i = %d\n", i);
}

问题是变量 i 一直没有变化,所以 i < 5 永远成立。

正确写法:

1
2
3
4
5
while (i < 5)
{
printf("i = %d\n", i);
i++;
}

二十七、条件断点

如果程序中有一个循环执行很多次,我们不想每次循环都停,只想在某个条件满足时停,可以使用条件断点。

例如:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main()
{
for (int i = 0; i < 10; i++)
{
printf("i = %d\n", i);
}

return 0;
}

编译:

1
gcc -g -O0 condition.c -o condition

进入 GDB:

1
gdb ./condition

如果想让程序在 i == 5 时停下,可以设置条件断点:

1
b 7 if i == 5

这里假设第 7 行是:

1
printf("i = %d\n", i);

运行:

1
r

程序会在 i 等于 5 的时候暂停。

条件断点适合调试:

  1. 循环次数很多的程序;
  2. 某个变量达到特定值才出错的程序;
  3. 数组处理、链表遍历、数据查找等场景。

二十八、监视变量变化:watch 命令

有时候我们会遇到这种问题:

某个变量的值突然变错了,但不知道是哪一行代码改坏的。

这时可以使用 watch 命令。

例如:

1
watch sum

表示监视变量 sum

只要 sum 的值发生变化,程序就会自动暂停。

例如:

1
2
3
int sum = 0;
sum = 10;
sum = 20;

如果设置了:

1
watch sum

sum 被修改时,GDB 会停下来,并告诉你变量旧值和新值。

这对排查变量被意外修改的问题非常有用。


二十九、修改变量值

GDB 不仅能查看变量值,还能在程序运行过程中修改变量。

例如当前程序中:

1
2
int x = 10;
int y = 20;

在 GDB 中可以输入:

1
set var x = 100

然后查看:

1
p x

输出:

1
$1 = 100

这说明变量 x 已经被修改了。

这种功能适合测试不同分支,比如:

1
2
3
4
5
6
7
8
if (score >= 60)
{
printf("pass\n");
}
else
{
printf("fail\n");
}

你可以在 GDB 中直接修改:

1
set var score = 59

或者:

1
set var score = 90

这样不用反复改代码、重新编译,就可以测试不同情况。


三十、调试带命令行参数的程序

有些程序运行时需要参数,例如:

1
./main hello world

下面写一个示例程序。

创建文件:

1
vim args.c

输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
#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;
}

编译:

1
gcc -g -O0 args.c -o args

普通运行:

1
./args hello world

使用 GDB 调试时,有两种传参方式。

第一种,直接在 run 后面加参数:

1
run hello world

第二种,先设置参数:

1
2
set args hello world
run

查看当前参数:

1
show args

三十一、查看内存

GDB 还可以查看内存内容,常用命令是:

1
x

x 是 examine 的意思,表示检查内存。

常见格式:

1
x/数量格式单位 地址

例如:

1
x/4xw &x

含义如下:

部分含义
xexamine,查看内存
/4查看 4 个单位
x用十六进制显示
wword,4 字节
&x变量 x 的地址

常见显示格式:

格式含义
x十六进制
d有符号十进制
u无符号十进制
c字符
s字符串
i汇编指令

例如查看变量地址:

1
p &x

查看变量附近的内存:

1
x/4xw &x

查看字符串:

1
x/s str

查看数组中的 10 个整数:

1
x/10dw arr

这里:

1
2
3
10 表示查看 10 个
d 表示十进制
w 表示每个单位 4 字节

三十二、查看汇编代码

如果学习操作系统、嵌入式、汇编或底层调试,可以使用 GDB 查看汇编代码。

反汇编当前函数:

1
disassemble

反汇编指定函数:

1
disassemble main

显示当前指令:

1
x/i $pc

其中 $pc 表示当前程序计数器,也就是 CPU 正在执行的位置。

如果想让 GDB 显示源码窗口,可以使用:

1
layout src

显示汇编窗口:

1
layout asm

退出这个界面:

1
2
Ctrl + X
然后按 A

也就是先按住 Ctrl 再按 X,松开后再按 A


三十三、常见 GDB 报错和解决方法

1. No debugging symbols found

如果启动 GDB 时看到:

1
No debugging symbols found

说明编译时没有加 -g

错误编译方式:

1
gcc main.c -o main

正确编译方式:

1
gcc -g -O0 main.c -o main

2. No symbol table is loaded

这个问题通常也是因为没有调试信息。

解决方法:

1
2
gcc -g -O0 main.c -o main
gdb ./main

3. 程序没有停在断点

可能原因有:

  1. 断点所在代码根本没有被执行;
  2. 没有加 -g
  3. 编译时开启了优化;
  4. GDB 加载的不是刚刚编译的程序。

建议重新编译:

1
gcc -g -O0 main.c -o main

4. Cannot access memory at address 0x0

这个错误通常和空指针有关。

例如:

1
2
int *p = NULL;
*p = 10;

解决方法是:在使用指针前,确保它指向有效地址。


5. Segmentation fault

段错误常见原因包括:

  1. 空指针解引用;
  2. 数组越界;
  3. 使用已经释放的内存;
  4. 栈溢出;
  5. 字符串越界写入;
  6. 访问非法地址。

遇到段错误时,最推荐的做法是:

1
gdb ./程序名

然后:

1
2
run
bt

通常可以直接定位到出错位置。


三十四、GDB 常用命令速查表

命令简写作用
gdb ./main启动 GDB
break mainb main在 main 函数设置断点
break 10b 10在第 10 行设置断点
runr运行程序
nextn单步执行,不进入函数
steps单步执行,进入函数
continuec继续运行
print xp x查看变量 x
display x每次暂停自动显示 x
info locals查看局部变量
info args查看函数参数
backtracebt查看函数调用栈
frame 1f 1切换到第 1 层栈帧
finish执行完当前函数并返回
info breakpointsinfo b查看所有断点
delete 1d 1删除编号为 1 的断点
disable 1禁用编号为 1 的断点
enable 1启用编号为 1 的断点
watch x监视变量 x 的变化
set var x=10修改变量 x 的值
listl查看源码
quitq退出 GDB
最后再记住一句话:

GDB 调试的核心不是背命令,而是看清楚程序每一步执行到了哪里、变量发生了什么变化、错误是从哪一步开始出现的

只要掌握了这个思路,后面无论是调试 C/C++ 程序、Linux 程序、嵌入式程序,还是 ROS 工程,GDB 都会成为非常有用的工具。

  • 标题: GDB 调试工具入门教程:从零开始手把手教你调试 C/C++ 程序
  • 作者: John Doe
  • 创建于 : 2026-06-10 10:27:48
  • 更新于 : 2026-06-10 15:28:55
  • 链接: https://liu-big-github-io.vercel.app/2026/06/10/GDB-调试工具入门教程:从零开始手把手教你调试-C-C-程序/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论