1. 调试基础

调试这事儿,说白了就是程序出了问题,你得找到根源在哪。我带了这么多年团队,发现很多人上来就改代码,根本不查原因,结果越改越乱。其实,调试是有套路、有工具的。这一章,我们先聊清楚什么是调试,有哪些趁手的工具,再说透断点和单步执行这两个基本功。

1.1 调试到底是什么?

调试就是找出程序行为与预期不符的原因。嗯,听起来很简单。但你去问问那些老工程师,他们肯定跟你说:调试花的时间,往往比写代码还多。

调试的核心环节,我习惯总结成四步:

  1. 复现问题——让Bug稳定出现
  2. 定位代码——找到出错的模块或函数
  3. 分析原因——搞清楚变量状态、执行路径哪里不对
  4. 验证修复——改完后确认问题解决,且没引入新问题

举个简单的例子。我记得有一次,同事说程序跑一会就崩,但重启后又好了。这就属于典型的“幽灵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 调试的知识体系

这一章的内容,我总结成一张图。你可以看到调试的流程和工具选择是配合着来的。

调试基础知识体系 调试定义(四步循环) 复现问题 定位代码 分析原因 验证修复 调试工具概览(四类) 命令行调试 IDE调试器 性能分析 日志系统 断点与单步执行 普通断点 条件断点 数据断点 函数断点 Step Into Step Over Step Out 核心原则 调试不是瞎试,而是“观察 - 假设 - 验证”的循环

看完这张图,你应该能理解整个调试的脉络了:先搞清楚调试是什么,再选工具,最后落地到断点和单步操作上。接下来的章节,我们会深入每一种技术,把 GDB、日志、性能分析这些武器练到手里。

给新人的话:刚开始学调试,别追求花哨。固定用一套工具,把断点和单步执行练熟,你就解决了八成的问题。