2. GDB核心概念:断点、观察点、捕获点、单步执行、继续执行
调试器这东西,说白了就是让你能「看」着代码跑。但怎么「看」?你得告诉它在哪停、看什么、怎么走。GDB 的核心就围绕这几个问题展开。
我个人习惯把调试比作「侦探破案」。断点就是案发现场,观察点是盯住某个线索,单步执行是一帧一帧回放录像。今天咱们就把这几个核心概念掰开揉碎讲清楚。
2.1 断点(Breakpoint)—— 最常用的「暂停键」
断点是什么?你告诉 GDB:「程序跑到这一行,给我停下来。」就这么简单。
设置断点的方法很多,我列几个最常用的:
(gdb) break main # 在 main 函数入口停
(gdb) break 42 # 在第 42 行停
(gdb) break file.c:100 # 在 file.c 的第 100 行停
(gdb) break func # 在 func 函数入口停
嗯,这里要注意:断点可以加条件。比如你只想知道当 i == 5 时发生了什么:
(gdb) break 42 if i == 5
我在项目中遇到过一个大坑:循环里设了断点,每次都要手动 continue 几十次才到目标值。后来学会条件断点,一次搞定。你想想看,省了多少时间?
info breakpoints 查看所有断点,用 delete 1 删除编号为 1 的断点。别用 clear,那玩意儿容易搞混。
2.2 观察点(Watchpoint)—— 盯住变量的「监控摄像头」
断点是按「位置」停,观察点是按「数据变化」停。说白了就是:你告诉 GDB,「帮我盯着这个变量,它一变就停。」
(gdb) watch my_var # my_var 被写入时停
(gdb) rwatch my_var # my_var 被读取时停
(gdb) awatch my_var # 读写都停
为什么会需要观察点?我举个例子。有一次我调试一个多线程程序,某个全局变量莫名其妙被改了。设断点?根本不知道在哪改的。用 watch 一盯,程序一跑就停在了修改它的那行代码上。嗯,当场破案。
2.3 捕获点(Catchpoint)—— 捕捉「事件」的触发器
捕获点比断点更高级。它不关心代码位置,只关心「事件」。比如:
(gdb) catch throw # C++ 异常抛出时停
(gdb) catch syscall write # 系统调用 write 时停
(gdb) catch fork # fork 调用时停
我记得有一次调试一个诡异的崩溃问题,程序跑着跑着就挂了,但不知道在哪抛的异常。用 catch throw 一设,GDB 直接停在异常抛出的地方。你想想看,要是没有捕获点,你得一行一行设断点试到什么时候?
2.4 单步执行(Step / Next)—— 一行一行「走」代码
停下来了,然后呢?你得一步一步看代码怎么跑。GDB 提供了两种「走法」:
| 命令 | 作用 | 区别 |
|---|---|---|
step(或 s) |
步入 | 遇到函数调用,进入函数内部 |
next(或 n) |
步过 | 遇到函数调用,直接执行完,不进入 |
finish |
跳出 | 执行完当前函数,返回到调用处 |
until |
运行到指定行 | 跳过循环体,直接到某一行 |
我个人习惯是:不确定函数内部逻辑时用 step,确定没问题时用 next。别傻乎乎一路 step 进标准库,那里面代码量能让你怀疑人生。
(gdb) step
(gdb) next
(gdb) finish
(gdb) until 100
2.5 继续执行(Continue)—— 从暂停处「跑」起来
单步执行太慢,你只想让程序继续跑,直到下一个断点或结束。这时候用 continue:
(gdb) continue # 继续执行
(gdb) c # 简写
(gdb) continue 5 # 忽略当前断点 5 次,第 6 次再停
嗯,continue 5 这个用法很多人不知道。我在调试一个循环 100 次的 bug 时,前 5 次没问题,第 6 次才出问题。用 continue 5 直接跳过前 5 次,省了多少手动操作?你想想看。
c 而不是 n,结果程序直接跑完了。嗯,从那以后我习惯先看一眼 GDB 提示符,确认当前状态再敲命令。
2.6 知识体系总览
下面这张图把今天讲的核心概念串起来了。你一看就明白它们之间的关系:
这张图你看懂了吗?断点、观察点、捕获点,三者是「暂停」的不同方式。而单步执行和继续执行,是「暂停之后怎么走」。说白了,调试就是「暂停 — 检查 — 继续」的循环。
我个人建议:刚开始用 GDB 时,先把断点和单步执行练熟。这两个用好了,80% 的 bug 都能搞定。观察点和捕获点属于进阶技巧,遇到特定问题时再拿出来用。
嗯,今天的内容就到这。记住这些核心概念,后面咱们再聊更高级的调试技巧。