2. GDB核心概念:程序状态、断点、单步执行、查看变量
调试,说白了就是让程序「停下来给你看」。
我刚开始用GDB时,总觉得它像个黑盒子。后来发现,只要搞懂四个核心概念——程序状态、断点、单步执行、查看变量——调试就变得很直观了。这就像学开车,先搞懂方向盘、油门、刹车和仪表盘,剩下的就是熟练度问题。
2.1 程序状态:你的程序现在「活」得怎么样?
程序在运行中,其实有几种状态。我个人习惯把GDB里的程序状态分成三类:
- 运行态(Running):程序正在执行,你没法干预它。
- 暂停态(Stopped):程序停在了某一行,等你发号施令。
- 终止态(Exited):程序跑完了,或者崩溃了。
调试的核心,就是让程序从「运行态」进入「暂停态」。为什么?因为只有在暂停时,你才能看清它的内部状态——变量值、寄存器、堆栈信息。
重要概念:GDB中,程序暂停后,你看到的每一行代码、每一个变量,都是「那一刻的快照」。时间凝固了,你可以慢慢分析。
我在项目中遇到过最头疼的事:一个bug只在特定条件下出现,跑100次才复现一次。如果没有断点,你只能干瞪眼。有了断点,就能让程序在可疑位置「停下来等你看」。
2.2 断点:让程序在指定位置「刹车」
断点就是调试的「刹车踏板」。你告诉GDB:「跑到这一行,给我停下。」
2.2.1 设置断点的基本方法
(gdb) break main # 在main函数入口设断点
(gdb) break 42 # 在当前文件的第42行设断点
(gdb) break file.c:100 # 在file.c的第100行设断点
(gdb) break func_name # 在函数func_name入口设断点
嗯,这里要注意:断点可以加条件。比如你只想知道当某个变量等于特定值时才停下:
(gdb) break 42 if count == 5 # 当count等于5时,在第42行停下
我曾经调试一个内存泄漏问题,循环跑了10万次才出问题。要是没条件断点,我得手动按「continue」按到手抽筋。条件断点救了我一命。
2.2.2 断点的管理
| 命令 | 作用 |
|---|---|
info breakpoints |
列出所有断点 |
disable 1 |
禁用编号为1的断点(不删除) |
enable 1 |
启用编号为1的断点 |
delete 1 |
删除编号为1的断点 |
clear 42 |
清除第42行的断点 |
小技巧:我习惯用disable而不是delete。因为有时候调试到一半,发现刚才禁用的断点又需要了。删了就没了,禁用还能恢复。
2.3 单步执行:一步一步看程序怎么走
断点让程序停下来,单步执行则让你「一步一步走」。你想想看,如果程序是一本书,断点就是书签,单步执行就是逐字阅读。
2.3.1 两个核心命令
step(简写s):步入。遇到函数调用,会进入函数内部。next(简写n):步过。遇到函数调用,直接执行完整个函数,不进入内部。
举个例子:
10: int result = compute(a, b); // 假设程序停在这一行
11: printf("result = %d\n", result);
如果你用 step,GDB会跳进 compute 函数内部,一行一行执行。如果你用 next,GDB会直接执行完 compute,停在下一行(第11行)。
避坑指南:我曾经在调试一个库函数时用了step,结果一头扎进了glibc的源码里,全是汇编和宏,看得我头皮发麻。如果你只想看自己的代码,用next就好。想进函数内部看细节,再用step。
2.3.2 其他单步命令
| 命令 | 作用 |
|---|---|
finish |
执行完当前函数,返回到调用处 |
until 行号 |
一直执行到指定行(跳过循环时很好用) |
continue |
恢复执行,直到下一个断点 |
我建议你多练练 until。比如你在一个循环里,想直接跳到循环结束后的那一行,用 until 行号 比一次次按 next 高效得多。
2.4 查看变量:看看程序「心里在想什么」
程序停下来了,接下来就是「问它问题」。GDB里查看变量很简单:
(gdb) print variable_name # 打印变量的值
(gdb) p variable_name # 简写
(gdb) p/x variable_name # 以十六进制显示
(gdb) p/d variable_name # 以十进制显示
(gdb) p/s variable_name # 以字符串形式显示
你还可以查看复杂的数据结构:
(gdb) p *pointer # 查看指针指向的内容
(gdb) p array[0..5] # 查看数组的前6个元素
(gdb) p struct_var.field # 查看结构体的某个字段
核心技巧:用 display 命令可以让变量「自动显示」。每次单步执行后,GDB会自动打印你指定的变量值,不用每次都手动输入 print。
(gdb) display count # 每次停下时自动显示count的值
(gdb) display/x ptr # 以十六进制自动显示ptr
(gdb) info display # 查看所有自动显示的变量
(gdb) undisplay 1 # 取消编号为1的自动显示
我记得有一次调试一个链表操作,节点指针总是不对。我设了三个 display:当前节点地址、下一个节点地址、节点里的数据值。单步走了几步,立刻发现是插入操作里忘记更新前一个节点的 next 指针了。要是没有自动显示,我可能得反复敲十几遍 print。
2.5 知识体系总览
下面这张图,是我梳理的本章核心逻辑。你可以把它当作调试时的「思维地图」:
2.6 实战小练习
光说不练假把式。我建议你打开一个简单的C程序,比如:
#include <stdio.h>
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int main() {
int result = factorial(5);
printf("5! = %d\n", result);
return 0;
}
用GDB加载它,然后:
- 在
factorial函数入口设断点 - 用
step一步步走进递归调用 - 用
print n观察每次递归时参数的变化 - 用
backtrace查看调用栈(下一章会细讲)
我的建议:别急着学高级技巧。先把这四个核心概念练熟——程序状态、断点、单步执行、查看变量。这就像学功夫先扎马步,马步稳了,后面的招式才使得出来。
调试不是玄学,是科学。你掌握了这些基础,就已经能解决80%的bug了。剩下的20%,我们后面慢慢聊。