GDB 调试指南(第 2 章):断点与观察点
← 上一章:GDB 快速上手 | 目录 | 下一章:运行与参数传递 →
第 2 章 断点与观察点:精确控制程序在哪里停下
上一章已经用过最简单的 b main。断点的本质很简单:在程序执行流上做一个标记,CPU 跑到这个标记就自动暂停,把控制权交还给你。这一章把断点的各种玩法、以及它的“进阶版”——观察点(watchpoint)——讲完整。
2.1 break 的六种定位方式
break(缩写 b)支持好几种方式告诉 GDB “我要在哪里停下”:
(gdb) b 49 # 1. 行号(在当前默认文件里)
Breakpoint 1 at 0x15b2: file main.cpp, line 49.
(gdb) b main.cpp:5 # 2. 文件名:行号(跨文件,明确指定在哪个文件)
Breakpoint 2 at 0x17ac: file main.cpp, line 12.
(gdb) b testFun # 3. 函数名(若有重载,会在多个位置下断点)
Breakpoint 3 at 0x1379: testFun. (2 locations)
(gdb) b FUNC::testFun1() # 4. 类名::成员函数名
Breakpoint 8 at 0x17ac: file main.cpp, line 12.
(gdb) b main.cpp:testFun(std::string) # 5. 文件名:函数名(跨文件 + 重载消歧)
Breakpoint 6 at 0x555555555379: file main.cpp, line 17.
(gdb) b 61 if sum > 500 # 6. 条件断点:位置 + if 条件
Breakpoint 7 at 0x555555555662: file main.cpp, line 61.
条件断点命中后,info breakpoints(缩写 i b)会显示 stop only if sum > 500,方便确认条件生效:
(gdb) i b
Num Type Disp Enb Address What
7 breakpoint keep y 0x0000555555555662 in main(int, char**) at main.cpp:61
stop only if sum > 500
【新手提示】条件断点非常适合“循环跑一万次,但我只关心第 999 次那一轮”这种场景。不用在代码里写
if (i == 999) { /* 在这里手动打断点 */ },直接b 61 if i == 999就行,省得改代码。
【常见误区】为什么我的断点“下在了别的地方”或者“根本不触发”
新手最容易遇到的几种情况:
- 行号选到了空行或声明行:GDB 只能在“会生成机器指令”的行上停下。如果你
b的那一行是空行、注释,或者只是一个变量声明但没有赋值,GDB 会自动把断点挤到下一条真正有代码的行,这是正常行为,不是 bug。 - 跨文件没写文件名,断在了同名函数的另一个文件里:如果项目里多个文件都有叫
testFun的函数,只写b testFun可能会在好几个位置同时下断点(见上面示例 3,提示2 locations),不是你以为的那一个。想精确指定,用“文件名:函数名”的写法(示例 5)。 - 开了编译优化:
-O2/-O3优化会把代码顺序打乱、把小函数直接内联展开、把变量直接放进寄存器不再有固定的内存地址。这会导致断点行号和你看到的源码对不上、step单步时跳着走、甚至某些变量用print查不到(提示<optimized out>)。调试阶段建议用-O0编译,调试体验会正常很多。 - 二进制和源码对不上:参考第 1 章提到的“改了代码忘记重新编译”。
2.2 break *地址:直接按内存地址下断点
有时候你是在看反汇编、而不是源码(比如没有调试信息的第三方库),这时候可以直接按指令地址下断点:
(gdb) disassemble
Dump of assembler code for function main:
0x000055555555543b <+0>: endbr64
0x000055555555543f <+4>: push %rbp
0x0000555555555440 <+5>: mov %rsp,%rbp
=> 0x0000555555555443 <+8>: push %rbx
0x0000555555555444 <+9>: sub $0x118,%rsp
0x000055555555544b <+16>: mov %edi,-0x114(%rbp)
...
(gdb) b *0x000055555555544b
Breakpoint 2 at 0x55555555544b
【常见误区】现代 Linux 默认开启 PIE(位置无关可执行文件)和 ASLR(地址空间随机化),同一个程序每次运行,实际加载的内存地址都可能不一样。如果你把某次运行看到的绝对地址记下来,下次启动再用
b *0x地址,大概率是错的、或者压根落不到你想要的指令上。按地址下断点只适合“在本次 GDB 会话里、刚用disassemble看到地址马上就用”这种场景;只要能用函数名/行号,就优先用符号化的方式,不用关心地址到底是多少。
2.3 tbreak:一次性断点
语法与 break 完全一致,唯一区别是命中一次后自动删除,适合“只想在某处停一次、看完就不想再被反复打断”的场景(比如循环体内某一行,正常断点会每轮循环都停一次,很烦):
(gdb) tbreak 51
Temporary breakpoint 8 at ...: file main.cpp, line 51.
(gdb) i b
8 breakpoint del y 0x... in main(...) at main.cpp:51
i b 的 Disp 列显示 del(命中后删除),区别于普通断点的 keep(一直保留)。
2.4 rbreak:按正则表达式批量下断点
对函数名做正则匹配,一次性给所有符合条件的重载/同名函数下断点,省得一个个手动找:
(gdb) rbreak testFun
Breakpoint 1 at 0x1aa8: file FUN2.cpp, line 8.
int FUN2::testFun2();
Breakpoint 2 at 0x17ac: file main.cpp, line 12.
void FUNC::testFun1();
Breakpoint 3 at 0x13f1: file main.cpp, line 26.
int testFun(int);
Breakpoint 4 at 0x1379: file main.cpp, line 17.
void testFun(std::string);
因为本身就是正则匹配,rbreak testFun* 和 rbreak testFun 效果相同——testFun 这个正则已经能匹配所有以它开头的符号了,末尾加不加 * 不影响结果。
【新手提示】
rbreak适合“我想看这个类/这组同名重载函数每次被调用时都发生了什么”这种排查场景,比如怀疑某个函数被意外调用、或者想确认重载决议选对了版本。
2.5 info breakpoints:查看所有断点
(gdb) i b
Num Type Disp Enb Address What
4 breakpoint keep y 0x0000555555555458 in main(int, char**) at main.cpp:36
breakpoint already hit 1 time
命中过的断点会额外显示 breakpoint already hit N time,方便确认执行路径是否符合预期——比如你以为一个函数只会被调用一次,结果这里显示命中了 5 次,那说明理解有偏差,值得深挖。
2.6 delete / clear:删除断点
(gdb) d 2 # 删除 2 号断点(d 是 delete 的缩写)
(gdb) d # 不带编号:删除全部断点(会二次确认)
Delete all breakpoints, watchpoints, tracepoints, and catchpoints? (y or n) y
另外还有 clear [位置],按源码位置而不是编号删除断点,例如 clear main.cpp:49 会删除该行上的断点;不带参数的 clear 需要当前有“默认源文件”上下文,否则会报 No source file specified。日常更推荐用 delete <编号>,定位更精确、不容易误删。
2.7 disable / enable / enable once
断点太多、暂时不想被某几个打断,又不想直接删掉(删了还得重新敲一遍),可以用禁用/启用:
(gdb) disable 6 # 禁用 6 号断点:保留但不再触发
(gdb) enable 6 # 重新启用
(gdb) enable once 7 # 只启用一次,命中后自动变回禁用(Disp 列显示 dis)
enable once 很适合“这个断点只在第一次经过时有意义,之后不想再被打断”的场景,比手动 disable/enable 省事。
2.8 观察点(watchpoint):当你不知道“哪一行”改了变量时
断点需要你先知道“在哪一行”停下,但很多排查场景里,bug 的真实情况是:你知道某个变量的值被改错了,但不知道是哪段代码改的。这正是观察点要解决的问题——它监视的是“值”而不是“位置”,值一旦变化就自动暂停,不管改动发生在源码的哪里。
2.8.1 硬件观察点 vs 软件观察点
| 类型 | 硬件观察点 | 软件观察点 |
|---|---|---|
| 触发方式 | CPU 调试寄存器直接比较 | GDB 单步 + 软件判断 |
| 数量限制 | 受硬件调试寄存器数量限制,x86 上通常为 4 个 | 理论上无限制 |
| 性能 | 高(硬件级,几乎不拖慢程序) | 低(相当于每条指令都要停下来检查,非常慢) |
GDB 会优先尝试用硬件观察点;只有当硬件寄存器用完、或者监视的表达式太复杂无法映射到硬件比较条件时,才会退化成软件观察点。退化之后调试会明显变卡,这是正常现象,不是 GDB 出问题了。
2.8.2 基本用法
(gdb) watch g_num
(gdb) i watchpoints
Num Type Disp Enb Address What
1 hw watchpoint keep y g_num
(gdb) r
...
Hardware watchpoint 1: g_num
Old value = 0
New value = 111
testFun (num=9988) at main.cpp:32
32 printf("cnt: %d\n", cnt);
触发时 GDB 会直接告诉你旧值、新值,以及此刻停在了哪个函数的哪一行——这正是“值被改的那一刻”,不需要你猜。
2.8.3 不止能监视简单变量
watch 后面跟的其实是一个表达式,不局限于单个变量名,常见的还有:
(gdb) watch arr[3] # 监视数组某个元素
(gdb) watch *ptr # 监视某个指针指向的内容
(gdb) watch obj.field # 监视某个成员变量
【新手提示】监视的表达式越复杂(比如涉及指针解引用、数组下标),越容易因为 GDB 无法把它转换成一个固定内存地址上的硬件比较条件,而退化为软件观察点,导致明显变慢。如果只是想抓“某个指针指向的值什么时候变”,可以先用
2.8.4 用完记得清理
观察点和断点一样会一直占用硬件寄存器资源,调试完某个变量后,记得 delete <编号> 或者直接 d 清一下,不然硬件观察点数量有限(x86 上通常只有 4 个),很容易“没额度用了”。
2.9 小结
- 断点解决的是“在哪一行停下”,观察点解决的是“在什么值变化时停下”,两者互补。
- 条件断点(
b 位置 if 条件)能省掉大量手动改代码加判断的麻烦。 - 调试阶段尽量用
-O0编译,断点和单步的行为会符合直觉得多。 - 硬件地址下断点只适合临时、当次会话使用,不要当成固定方式依赖。

浙公网安备 33010602011771号