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调试的核心流程画了张图,方便你记忆:

GDB调试核心流程 1. 编译加 -g 2. 启动 GDB 3. 设置断点 4. run 启动程序 单步执行 step / next 查看变量 print / display 循环调试 发现bug? 修代码,重新来

记住这个循环:设断点 → 运行 → 单步执行 → 查看变量 → 发现问题 → 修代码 → 重新编译调试。直到程序行为符合预期为止。

10. 避坑指南

最后分享几个我踩过的坑:

  • 忘记加-g:我曾经在客户现场调试,编译时忘了加-g,结果GDB报错说找不到符号表。当时那个尴尬啊……所以我现在每次编译都条件反射地加上-g
  • 优化级别太高:用-O2-O3编译,GDB单步执行时可能会跳来跳去,变量也可能被优化掉。调试阶段建议用-O0
  • 断点设在了空行或注释行:GDB不会停在这些地方,因为根本没有对应的机器指令。设断点前最好确认那一行有实际代码。
  • 多线程调试:GDB默认在所有线程里都生效断点。如果只想让某个线程停,需要用thread apply 线程号 break。这个稍微复杂点,以后有机会再细讲。

好了,GDB最核心的三个操作就这些。你找个简单的程序练练手,很快就能上手。记住,调试不是浪费时间,而是帮你理解程序到底在干什么。用熟了之后,你会发现自己写bug的概率都降低了——因为你开始「看见」代码的执行过程了。

一句话总结:break设停点,run跑起来,step/n单步走,print看变量。三板斧用好,大部分bug无处遁形。

公众号:蓝海资料掘金营,微信deep3321