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