85、GDB 调试:如何设置断点、查看变量、单步执行?
说实话,很多C语言初学者写代码,遇到bug就靠printf大法。我以前也这么干,直到有一次在嵌入式项目里,printf把实时性给打乱了,bug反而藏得更深了。从那以后,我老老实实学起了GDB。
GDB是什么?说白了就是一个让你能「钻进程序内部」的工具。你可以让程序在任意一行停下来,看看变量到底变成了什么鬼样子。今天我就带你过一遍最核心的三个操作:设置断点、查看变量、单步执行。
核心要点:GDB调试三板斧——断点让你停在感兴趣的位置,单步让你看清每一步的变化,变量查看让你抓住问题的根源。
1. 准备工作:编译时加-g选项
要用GDB调试,编译的时候必须带上-g选项。这个选项会把源代码的行号、变量名等信息塞进可执行文件里。没有它,GDB就像瞎子摸象。
// demo.c
#include <stdio.h>
int add(int a, int b) {
int sum = a + b;
return sum;
}
int main() {
int x = 10;
int y = 20;
int result = add(x, y);
printf("result = %d\n", result);
return 0;
}
// 编译命令
gcc -g -o demo demo.c
小提示:我习惯在Makefile里把-g和-O0一起用。-O0是关闭优化,否则编译器可能会把变量优化掉,导致你查看变量时看到「optimized out」——嗯,那感觉就像你明明放了东西在桌上,却被人偷偷收走了。
2. 启动GDB
启动方式很简单:
gdb ./demo
进去之后你会看到(gdb)提示符。这时候程序还没跑,你可以先做点准备工作。
3. 设置断点(break)
断点就是让程序停下来的地方。我常用的方式有几种:
| 命令 | 含义 | 示例 |
|---|---|---|
break 行号 |
在指定行设置断点 | break 10 |
break 函数名 |
在函数入口设置断点 | break add |
break 文件名:行号 |
在多文件项目中指定文件 | break demo.c:12 |
break 行号 if 条件 |
条件断点,满足条件才停 | break 15 if x == 10 |
举个例子,我想在add函数入口停一下:
(gdb) break add
Breakpoint 1 at 0x4004f6: file demo.c, line 4.
或者我想在main函数里第12行(调用add的那一行)停一下:
(gdb) break demo.c:12
Breakpoint 2 at 0x40052a: file demo.c, line 12.
注意:我曾经在调试一个大型项目时,不小心在循环里设了断点,结果每次循环都停一次,按c按到手抽筋。后来我学会了用条件断点——break 循环体行号 if i == 100,只在第100次迭代时停下来。省心多了。
4. 查看断点列表(info breakpoints)
设了一堆断点后,有时候会忘记设在哪了。用这个命令查看:
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x4004f6 in add at demo.c:4
2 breakpoint keep y 0x40052a in main at demo.c:12
想删除某个断点,用delete 编号。比如delete 2就把第二个断点删掉了。
5. 运行程序(run)
设好断点后,输入run(或者简写r)启动程序。程序会一直跑,直到遇到第一个断点停下来。
(gdb) run
Starting program: /home/user/demo
Breakpoint 1, add (a=10, b=20) at demo.c:4
4 int sum = a + b;
看到没?程序停在了add函数的第4行,而且GDB还贴心地告诉你参数a=10, b=20。
6. 查看变量(print)
程序停下来了,这时候最想干的事就是看看变量值对不对。用print命令(简写p):
(gdb) print a
$1 = 10
(gdb) print b
$2 = 20
(gdb) print &a
$3 = (int *) 0x7fffffffe4a0
你也可以一次看多个变量:
(gdb) print a + b
$4 = 30
想看局部变量列表?用info locals:
(gdb) info locals
sum = 0 // 还没执行到赋值语句,所以是未初始化的值
小技巧:我经常用display命令让某个变量每次停顿时自动显示。比如display sum,之后每次单步执行,GDB都会自动打印sum的值。省得我一遍遍敲print。
7. 单步执行(step 和 next)
这是调试的核心操作。两个命令的区别一定要搞清楚:
| 命令 | 简写 | 含义 |
|---|---|---|
step |
s |
单步进入,遇到函数会跟进去 |
next |
n |
单步跳过,遇到函数直接执行完,不进去 |
finish |
fin |
一直执行到当前函数返回 |
continue |
c |
继续执行,直到下一个断点 |
举个例子。假设我们停在main函数的第12行(调用add的那一行):
(gdb) break demo.c:12
(gdb) run
Breakpoint 2, main () at demo.c:12
12 int result = add(x, y);
这时候:
- 如果输入
step,会进入add函数内部,停在int sum = a + b;这一行。 - 如果输入
next,会直接执行完add函数,停在下一行printf。
我个人习惯是:想确认函数内部逻辑时用step,想快速跳过时用next。调试自己写的代码,我一般用step;调试库函数,我肯定用next——谁想钻进printf的实现里啊?
8. 实战演练:完整调试流程
咱们把上面的demo完整走一遍:
$ gdb ./demo
(gdb) break main
(gdb) run
Breakpoint 1, main () at demo.c:8
8 int x = 10;
(gdb) next
9 int y = 20;
(gdb) print x
$1 = 10
(gdb) next
10 int result = add(x, y);
(gdb) print y
$2 = 20
(gdb) step
add (a=10, b=20) at demo.c:4
4 int sum = a + b;
(gdb) next
5 return sum;
(gdb) print sum
$3 = 30
(gdb) next
6 }
(gdb) finish
main () at demo.c:11
11 printf("result = %d\n", result);
(gdb) print result
$4 = 30
(gdb) continue
Continuing.
result = 30
[Inferior 1 (process 12345) exited normally]
(gdb) quit
你看,每一步都清清楚楚。变量从创建到赋值到返回,全程都在你的掌控之中。
9. 核心流程总结
我把GDB调试的核心流程画了张图,方便你记忆:
记住这个循环:设断点 → 运行 → 单步执行 → 查看变量 → 发现问题 → 修代码 → 重新编译调试。直到程序行为符合预期为止。
10. 避坑指南
最后分享几个我踩过的坑:
- 忘记加-g:我曾经在客户现场调试,编译时忘了加
-g,结果GDB报错说找不到符号表。当时那个尴尬啊……所以我现在每次编译都条件反射地加上-g。 - 优化级别太高:用
-O2或-O3编译,GDB单步执行时可能会跳来跳去,变量也可能被优化掉。调试阶段建议用-O0。 - 断点设在了空行或注释行:GDB不会停在这些地方,因为根本没有对应的机器指令。设断点前最好确认那一行有实际代码。
- 多线程调试:GDB默认在所有线程里都生效断点。如果只想让某个线程停,需要用
thread apply 线程号 break。这个稍微复杂点,以后有机会再细讲。
好了,GDB最核心的三个操作就这些。你找个简单的程序练练手,很快就能上手。记住,调试不是浪费时间,而是帮你理解程序到底在干什么。用熟了之后,你会发现自己写bug的概率都降低了——因为你开始「看见」代码的执行过程了。
一句话总结:break设停点,run跑起来,step/n单步走,print看变量。三板斧用好,大部分bug无处遁形。