1. 调试基础
调试这事儿,说白了就是程序出了问题,你得找到根源在哪。我带了这么多年团队,发现很多人上来就改代码,根本不查原因,结果越改越乱。其实,调试是有套路、有工具的。这一章,我们先聊清楚什么是调试,有哪些趁手的工具,再说透断点和单步执行这两个基本功。
1.1 调试到底是什么?
调试就是找出程序行为与预期不符的原因。嗯,听起来很简单。但你去问问那些老工程师,他们肯定跟你说:调试花的时间,往往比写代码还多。
调试的核心环节,我习惯总结成四步:
- 复现问题——让Bug稳定出现
- 定位代码——找到出错的模块或函数
- 分析原因——搞清楚变量状态、执行路径哪里不对
- 验证修复——改完后确认问题解决,且没引入新问题
举个简单的例子。我记得有一次,同事说程序跑一会就崩,但重启后又好了。这就属于典型的“幽灵Bug”。我们用了一天,才稳定复现——原来是定时器回调里有个全局变量没加锁。你看,第一步就卡了半天。
一句话总结:调试不是瞎试,而是“观察-假设-验证”的循环。
1.2 调试工具概览
工欲善其事,必先利其器。调试工具分好几类,我来罗列一下常用的。
| 工具类型 | 代表工具 | 适用场景 |
|---|---|---|
| 命令行调试器 | GDB, LLDB | C/C++ 核心转储、多线程、底层分析 |
| IDE 集成调试器 | VS Code, CLion, PyCharm | 日常开发、断点可视、变量观察 |
| 性能分析工具 | perf, Valgrind, strace | 内存泄漏、CPU热点、系统调用 |
| 日志系统 | syslog, spdlog, ELK | 分布式、线上问题、长时间跟踪 |
我个人习惯:写代码时用 IDE 的调试器,方便快捷;一旦遇到内存越界、死锁这种疑难杂症,直接上 GDB 加 Valgrind。你想想看,鼠标点点能解决的,干嘛非敲命令?但真正的大问题,IDE 往往帮不上忙。
选工具的建议:小问题用日志,中等复杂用断点,疑难杂症用追踪级调试。别一上来就上 strace,你会被刷屏刷到崩溃。
1.3 断点入门
断点,就是让程序在某一行停下来,让你有机会看看“现场”。
断点的基本类型:
- 普通断点:在某行代码上暂停
- 条件断点:符合指定条件才暂停,比如
i > 100 - 数据断点(观察点):某块内存被读写时触发
- 函数断点:进入某个函数时暂停
我曾经遇到过一个极其隐蔽的Bug:一个全局数组的值在某个地方被改脏了。设普通断点根本抓不到,因为修改的时机完全不固定。后来我用了数据断点,在那个数组上挂了观察点,谁改它,程序就停。结果发现是个不相关的模块悄悄写了越界。这个案例我记了很久。
用 GDB 设数据断点的命令是这样的:
(gdb) watch my_array[0]
Hardware watchpoint 1: my_array[0]
(gdb) continue
Continuing.
Hardware watchpoint 1: my_array[0]
Old value = 42
New value = 0
0x080485f6 in some_unrelated_func ()
看到没?位置直接给你指出来了。这种效率,远比肉眼扫代码高得多。
1.4 单步执行
断点帮你停在某个位置,那停在之后呢?你得一步步走,看程序是怎么跑的。这就是单步执行。
单步执行的三种模式:
- 单步进入(Step Into):走进函数内部,看细节
- 单步跳过(Step Over):把当前行当黑盒执行完
- 单步返回(Step Out):直接跳出当前函数,回到调用处
说白了,Step Into 是“躬身入局”,Step Over 是“隔岸观火”。怎么选?我有个很简单的判断——如果你怀疑这个函数里有问题,就进去看;如果你只是想知道这个函数返回了什么结果,就别进去。
举个 GDB 的例子:
(gdb) break main
Breakpoint 1 at 0x4004f6: file example.c, line 10.
(gdb) run
Starting program: /tmp/a.out
Breakpoint 1, main () at example.c:10
10 int result = compute(x, y);
(gdb) step # 单步进入 compute 函数
compute (a=3, b=5) at example.c:5
5 int temp = a + b;
(gdb) print temp
$1 = 8
(gdb) next # 单步跳过,执行下一行
6 return temp * 2;
(gdb) finish # 单步返回,跳出 compute 函数
Run till exit from #0 compute (a=3, b=5) at example.c:6
0x000000000040050b in main () at example.c:10
10 int result = compute(x, y);
Value returned is $2 = 16
这一连串操作,你把 compute 函数从里到外看得清清楚楚。哪个变量什么值,路径怎么走的,一清二楚。
注意:单步执行在循环里要小心。如果你不小心 Step Into 了一个万次循环,你会按到手指抽筋。这时候,条件断点或临时断点才是正解。
1.5 调试的知识体系
这一章的内容,我总结成一张图。你可以看到调试的流程和工具选择是配合着来的。
看完这张图,你应该能理解整个调试的脉络了:先搞清楚调试是什么,再选工具,最后落地到断点和单步操作上。接下来的章节,我们会深入每一种技术,把 GDB、日志、性能分析这些武器练到手里。
给新人的话:刚开始学调试,别追求花哨。固定用一套工具,把断点和单步执行练熟,你就解决了八成的问题。