3. GDB基本命令:run、continue、next、step、finish、until、list、print、display
调试器这东西,说白了就是给程序装个「慢放键」。GDB 是我用得最顺手的工具,没有之一。今天咱们就把最核心的几条命令捋一遍。这些命令你每天都会敲,但很多人其实没完全吃透。
核心思想:GDB 不是魔法,它只是让你能「看」到程序执行时的每一帧画面。你控制的是时间,不是代码。
3.1 启动与运行:run 和 continue
run 命令,简写 r。它的作用就是启动程序。但有个细节——如果你已经设了断点,run 会在第一个断点处停下。我刚开始用 GDB 时,总以为 run 会从头跑到尾,结果发现它「智能」地停在了断点处。嗯,这其实是好事。
(gdb) break main
(gdb) run
Starting program: /home/user/myapp
Breakpoint 1, main () at main.c:5
5 int x = 10;
continue 命令,简写 c。它让程序继续跑,直到遇到下一个断点或程序结束。我个人习惯在循环里设断点,然后用 continue 快速跳过前几次迭代,只观察最后一次。
小技巧:给 continue 加个次数参数,比如 continue 5,它会忽略前 5 次断点命中,在第 6 次停下。省得你手动敲 5 次 c。
3.2 单步执行:next 和 step
这两个命令是调试的「显微镜」。区别很简单:
next(简写n):单步执行,遇到函数调用直接跳过,不进入函数内部。step(简写s):单步执行,遇到函数调用就钻进去。
我曾经在调试一个网络协议栈时,用 next 跳过一个 recv() 调用,结果程序卡住了——因为 recv() 是阻塞的。你想想看,next 只是「跳过」函数调用,但函数本身还是会执行。如果函数里有阻塞操作,你就得等。
(gdb) n
main () at main.c:10
10 result = compute(data); // 这里用 n 会跳过 compute 函数
(gdb) s
compute (data=0x7fffffffe0) at compute.c:3
3 int sum = 0; // 用 s 会进入 compute 内部
避坑指南:我曾经在调试多线程程序时,用 step 进入了一个锁竞争严重的函数,结果 GDB 卡了十几秒。后来我学乖了——不确定函数内部逻辑时,先用 next 跳过,再单独给那个函数设断点。
3.3 跳出与快速到达:finish 和 until
finish 命令,简写 fin。它执行完当前函数,然后停在该函数的返回处。说白了就是「把函数跑完,回到调用点」。我经常用它来检查函数的返回值。
(gdb) s
compute (data=0x7fffffffe0) at compute.c:3
3 int sum = 0;
(gdb) fin
Run till exit from #0 compute (data=0x7fffffffe0) at compute.c:10
0x00005555555546a0 in main () at main.c:11
11 printf("Result: %d\n", result); // 停在这里,result 已经算好了
until 命令,简写 u。它用来快速跳出循环。比如你在一个 for 循环里单步,不想一次次敲 n,直接 until 就能跑到循环结束后的下一行。
实用场景:我在调试一个冒泡排序时,循环体有 100 次迭代。用 until 直接跳出循环,比敲 100 次 n 省了 99 条命。
3.4 查看代码与变量:list 和 print
list 命令,简写 l。它显示当前行附近的源代码。默认显示 10 行。你可以指定行号或函数名。
(gdb) list 15
10 int x = 10;
11 int y = 20;
12 int z = x + y;
13 printf("z = %d\n", z);
14 return 0;
15 }
print 命令,简写 p。它打印变量或表达式的值。这是最常用的命令,没有之一。我个人习惯在 print 后面加格式化选项,比如 p/x 打印十六进制,p/f 打印浮点数。
(gdb) p x
$1 = 10
(gdb) p/x 0xFF
$2 = 0xff
(gdb) p &x
$3 = (int *) 0x7fffffffe0
小技巧:print 可以计算表达式。比如 p x + y * 2,GDB 会直接算出结果。我在调试算法时经常用这个,省得自己心算。
3.5 自动跟踪:display
display 命令,简写 disp。它和 print 很像,但有个关键区别——display 会在每次程序暂停时自动打印变量值。你想想看,单步调试时,每次都要手动敲 p x,多累啊。用 display 一次设置,后面就不用管了。
(gdb) display x
1: x = 10
(gdb) n
2: x = 20 // 自动打印,不用再敲 p x
(gdb) n
3: x = 30
我曾经在调试一个链表插入操作时,同时 display 了三个变量:head、current、new_node。每次单步,GDB 自动打印这三个值,我一眼就能看出指针有没有飞掉。
避坑指南:我曾经在调试一个大型项目时,一口气 display 了十几个变量。结果每次暂停,屏幕刷出一大堆信息,反而看不清重点。后来我学乖了——最多同时 display 3-5 个关键变量,其他的用 print 按需查看。
3.6 知识体系总览
下面这张图把本章的命令按「控制流」和「数据流」两个维度做了分类。你调试时,左手控制程序怎么跑,右手查看数据怎么变,两手都要硬。
3.7 命令速查表
| 命令 | 简写 | 作用 | 常用场景 |
|---|---|---|---|
| run | r | 启动程序,停在第一个断点 | 开始调试 |
| continue | c | 继续执行到下一个断点 | 跳过不关心的代码段 |
| next | n | 单步执行,不进入函数 | 跳过库函数调用 |
| step | s | 单步执行,进入函数 | 调试自定义函数逻辑 |
| finish | fin | 执行完当前函数并返回 | 检查函数返回值 |
| until | u | 快速跳出循环或到达指定行 | 跳过循环体 |
| list | l | 显示源代码 | 查看上下文代码 |
| p | 打印变量或表达式值 | 查看变量内容 | |
| display | disp | 每次暂停时自动打印变量 | 持续跟踪关键变量 |
我的个人习惯:调试一个新模块时,我会先 list 看代码结构,然后在关键路径上设断点,用 run 启动。进入断点后,display 几个核心变量,再用 next 或 step 一步步走。遇到循环就用 until 跳过,遇到函数就用 finish 快速返回。这套流程我用了十年,稳得很。
嗯,这些命令看着简单,但组合起来威力巨大。你想想看,一个 display 加 next,就能让你像看电影一样观察程序的每一步变化。下次调试时,试试看能不能只用这些命令就定位到 bug——你会发现,很多时候根本不需要那些花哨的高级功能。