2. GDB 高级断点技巧:硬件断点与软件断点区别、条件断点、数据观察点(Watchpoint)、断点命令脚本(Commands)、断点范围与线程断点
断点,说白了就是调试器里的“暂停键”。但很多人用了好几年 GDB,还只会最基础的 b main。嗯,这其实挺可惜的。GDB 的断点系统远比你想的丰富,用好它,调试效率能翻好几倍。
我个人习惯把断点分成两类:软件断点和硬件断点。它们的底层机制完全不同,适用场景也天差地别。
2.1 软件断点 vs 硬件断点
软件断点的原理很简单:GDB 会把目标地址的指令替换成一条特殊的“陷阱指令”(比如 ARM 上的 BKPT,x86 上的 INT3)。CPU 执行到这里就会触发异常,调试器接管控制。等调试器恢复执行时,再把原来的指令写回去。
好处是数量不限,随便设。坏处呢?代码会被修改。这在调试 ROM、Flash 或校验严格的固件时,会直接翻车。
硬件断点则依赖 CPU 内部的调试寄存器(比如 ARM 的 FPB、x86 的 DR0-DR3)。CPU 在取指时会硬件比较地址,匹配成功就触发调试异常。代码本身纹丝不动。
我在项目中遇到过一件事:调试一个 Bootloader,代码跑在 Flash 里,Flash 有 CRC 校验。我设了个软件断点,结果 CRC 校验失败,系统直接死机。折腾了半天才反应过来——断点改了 Flash 内容!换成硬件断点,问题立刻解决。
| 特性 | 软件断点 | 硬件断点 |
|---|---|---|
| 原理 | 替换指令为陷阱指令 | 使用 CPU 调试寄存器 |
| 数量限制 | 无限制 | 通常 2-6 个(视芯片而定) |
| 适用内存 | RAM(可写区域) | RAM、Flash、ROM 均可 |
| 代码修改 | 会修改目标代码 | 不修改代码 |
| GDB 命令 | break / b |
hbreak |
2.2 条件断点
条件断点,就是“满足条件才停”。你想想看,一个循环跑 10000 次,你只关心第 5000 次的情况。如果每次都手动 continue,手都按酸了。
GDB 的条件断点语法很直接:
(gdb) break function_name if variable == 0x55
(gdb) break 42 if count > 100 && flag == 1
(gdb) break *0x08001234 if *(int*)0x20000100 == 0
这里有个坑,我曾经踩过:条件表达式中的变量必须是当前作用域可见的。如果你在函数入口设条件断点,但条件用到了函数内部的局部变量,而该变量还没被初始化——嗯,GDB 会报错,断点直接失效。
2.3 数据观察点(Watchpoint)
Watchpoint 是调试界的“监控摄像头”。它不关心程序执行到哪里,只关心某块内存什么时候被读写。
三种常用模式:
- 普通观察点:监控变量被写入(
watch variable) - 读观察点:监控变量被读取(
rwatch variable) - 读写观察点:读写都监控(
awatch variable)
实际用法:
(gdb) watch global_flag
(gdb) rwatch buffer[128]
(gdb) awatch *(int*)0x40002000
我记得有一次排查一个“全局变量莫名其妙被改”的 bug。代码里搜遍了所有赋值的地方,都没发现问题。最后用 watch 监控那个变量,一跑就停住了——原来是一个 DMA 回调函数通过指针写错了偏移量。没有 Watchpoint,这种问题查起来简直大海捞针。
2.4 断点命令脚本(Commands)
这是 GDB 里最被低估的功能之一。你可以给一个断点绑定一组命令,断点命中后自动执行。
语法很简单:
(gdb) break some_function
(gdb) commands
> print "Hit some_function"
> print arg1
> continue
> end
上面这段脚本的意思是:每次命中 some_function,就打印一条消息和参数值,然后自动继续执行。整个过程不需要你手动敲任何命令。
更高级的用法:
(gdb) break malloc
(gdb) commands
> print "malloc called, size = %d", size
> bt 3
> continue
> end
这个脚本可以追踪每次 malloc 调用时的调用栈。我在分析内存泄漏时经常这么干——配合 continue,可以无感收集几百次调用信息。
- 自动打印变量值并继续执行
- 记录调用栈(bt)
- 条件满足时启用/禁用其他断点
- 自动收集日志到文件
- 模拟“条件断点”但避免性能开销(手动判断条件,不满足就 continue)
2.5 断点范围与线程断点
在多线程程序里,断点默认是“全局”的——任何一个线程命中都会停。但很多时候,你只关心某个特定线程的行为。
线程特定断点:
(gdb) break function_name thread 3
(gdb) break 88 thread 2 if data == 0
这样只有线程 3 命中 function_name 时才会暂停,其他线程不受影响。
断点范围(Breakpoint Ranges):
GDB 支持给断点设置“启用计数”和“忽略计数”:
(gdb) break func
(gdb) ignore 1 100 # 忽略前 100 次命中
(gdb) break func if count > 100 # 等价写法
还有 enable count 和 enable delete:
(gdb) break func
(gdb) enable count 1 5 # 只启用 5 次,之后自动禁用
(gdb) enable delete 1 3 # 只启用 3 次,之后自动删除
这些功能在调试“偶发 bug”时特别有用。比如一个 bug 每 100 次才出现一次,你可以设一个断点,忽略 99 次,第 100 次再停。
thread apply 配合条件判断来定位特定线程,而不是依赖固定的线程号。
2.6 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以把它当作调试时的“断点决策树”。
这张图把断点分成了四大类,再加上底部的 Commands 和线程控制,基本覆盖了日常调试会用到的所有断点技术。你调试时遇到问题,可以顺着这个结构想想:该用软件还是硬件?要不要加条件?是不是该用 Watchpoint 监控内存?能不能用 Commands 自动化?
嗯,把这些技巧练熟了,调试就不再是“设个断点、跑一下、看看值”的原始操作了。你会开始享受那种“断点一停,问题就暴露”的快感。