断点设置:让程序在你想停的地方停下来
调试C/C++程序,说白了就是回答三个问题:程序现在在哪?它怎么到这的?它接下来要去哪?而断点,就是我们回答这些问题的起点。
我个人习惯把断点比作「路障」。你在代码的某个位置放一个路障,程序跑到那就得停下来,把现场交给你检查。今天我们就来聊聊GDB里各种断点的玩法。
1. break命令:最基础的断点
break(简写b)是最常用的命令。用法很简单:
(gdb) break main # 在main函数入口停
(gdb) break 42 # 在第42行停
(gdb) break file.c:100 # 在file.c的第100行停
(gdb) break func_name # 在某个函数入口停
我在项目中遇到过一种情况:一个函数被调用了上千次,我只想在某次特定调用时停下来。这时候就需要条件断点了。
2. 条件断点:精准打击
条件断点就是带条件的break。格式是:
(gdb) break 42 if i == 5
(gdb) break func if ptr != NULL
(gdb) break file.c:100 if count > 100 && flag == 1
嗯,这里要注意:条件表达式里不能有宏定义,因为GDB不认识宏。我曾经在这个坑里摔过一次——调试一个内核模块,条件里用了#define MAX_SIZE 1024,结果GDB直接报错。后来我改成直接写数字才搞定。
3. 临时断点:用完即走
有时候你只想停一次,比如确认某个路径是否被执行到。这时候用tbreak(temporary break):
(gdb) tbreak 42
(gdb) tbreak func
(gdb) tbreak file.c:100 if x == 0
临时断点命中一次后自动删除。我个人习惯在「我就看一眼这里会不会走到」的场景下用,省得后面还要手动清理。
4. 硬件断点:调试只读内存
硬件断点(也叫数据断点、观察点)用来监控内存变化。用watch命令:
(gdb) watch var # 变量被写入时停
(gdb) watch *(int*)0x7fff1234 # 监控某个地址
(gdb) rwatch var # 变量被读取时停
(gdb) awatch var # 变量被读写时都停
硬件断点依赖CPU的调试寄存器,数量有限(通常是4个)。我记得有一次调试一个内存越界问题,变量被莫名其妙改写了。用watch监控那个变量,程序一跑就停——原来是另一个线程通过野指针写到了这里。
5. 断点管理:info、disable、enable、delete
断点多了以后,管理就变得重要。GDB给每个断点分配一个编号,从1开始递增。
| 命令 | 作用 | 示例 |
|---|---|---|
info breakpoints |
列出所有断点 | info b |
disable N |
禁用某个断点 | disable 2 |
enable N |
启用某个断点 | enable 2 |
delete N |
删除某个断点 | delete 2 |
clear |
删除当前行的断点 | clear 42 |
info breakpoints的输出里,每个断点会显示:编号、类型(break/watch/catch)、是否启用(y/n)、位置、条件(如果有)。
我常用的一个技巧:调试循环时,先disable掉循环体内的断点,等程序跑到循环外再enable回来。这样不用反复设置删除。
知识体系总览
下面这张图帮你理清断点的分类和关系:
核心要点:
- 普通断点按位置停,条件断点按条件停,临时断点停一次就消失
- 硬件断点监控内存,适合追踪变量被谁改了
- 断点管理命令要熟记:
info查看、disable/enable开关、delete删除 - 条件断点表达式里不要用宏,GDB不认识
说实话,断点这东西看着简单,但用好了能省大量时间。我见过不少同事调试时满屏幕设断点,跑起来到处停,反而把自己搞晕了。我的建议是:少而精。先想清楚你要查什么问题,再决定在哪个位置设断点。条件断点能帮你过滤掉99%的不相关命中,值得花时间写好条件。
好了,断点就聊到这。下一节我们聊聊怎么在断点停住后查看程序状态——那才是真正开始「调试」的时候。