GDB 监视点watch
一、什么是 watch?—— 变量的“监控摄像头”
之前学过的break是按位置暂停(比如某一行代码),而watch是按数据变化暂停。
watch的作用:
告诉 GDB 持续监视某个变量(或表达式),只要它的值发生变化,就立刻中断程序执行,并显示旧值和新值。
典型场景:
- 某个变量的值莫名其妙被修改,不知道在哪改的
- 想要知道一个全局变量或关键局部变量何时被赋予新值
- 代替在大量地方手动加
printf来追踪变量变化
二、监视点的三种类型
GDB 根据监视的触发条件,将监视点分为三类:
| 类型 | 命令 | 触发条件 |
|---|---|---|
| 写监视点(watchpoint) | watch 表达式 | 当被监视的表达式的值被写入(修改)时触发 |
| 读监视点(read watchpoint) | rwatch 表达式 | 当被监视的表达式的值被读取时触发 |
| 读写监视点(access watchpoint) | awatch 表达式 | 当被监视的表达式的值被读取或写入时触发 |
最常用的是watch,也就是写监视点。今天重点掌握它。
三、如何使用 watch —— 完整流程演示
准备工作:写一个简单的测试程序
创建文件watch_demo.c:
#include<stdio.h>intsum=0;// 全局变量,方便全程监视voidadd(intn){sum+=n;// 这里会修改 sum}intmain(){inti;for(i=1;i<=3;i++){add(i);printf("After adding %d, sum = %d\n",i,sum);}return0;}编译(必须加-g,建议不要优化):
gcc-g-O0watch_demo.c-owatch_demo进入 GDB 并设置监视点
gdb ./watch_demo1. 先给 main 函数设一个断点,确保程序跑起来后再设 watch
(因为 watch 要监视的变量必须存在于当前作用域)
(gdb) break main Breakpoint 1 at 0x...: file watch_demo.c, line ... (gdb) run Starting program: ./watch_demo Breakpoint 1, main () at watch_demo.c:12 12 for (i = 1; i <= 3; i++) {2. 设置监视点:监视全局变量sum
(gdb) watch sum Hardware watchpoint 2: sum“Hardware watchpoint” 表示系统使用硬件寄存器来监视,速度很快。
3. 继续运行程序
(gdb) continue Continuing.程序会一直运行,直到sum的值发生变化。
4. 触发监视点
当第一次执行sum += n后,GDB 会突然暂停:
Hardware watchpoint 2: sum Old value = 0 New value = 1 add (n=1) at watch_demo.c:7 7 }它告诉我们:
- 监视点编号 2
sum的旧值是 0,新值变成了 1- 程序停在修改
sum的那一行(实际上是add函数的第 7 行,即修改sum语句的结束)
此时你可以用print n查看参数,用backtrace查看调用栈。
5. 继续运行,观察每次变化
(gdb) continue Continuing. After adding 1, sum = 1 Hardware watchpoint 2: sum Old value = 1 New value = 3 add (n=2) at watch_demo.c:7 7 }重复操作,每次sum被修改都会暂停,直到程序结束。
四、监视局部变量
如果监视的是局部变量,必须在它的作用域内设置 watch,并且一旦离开作用域,监视点会自动删除。
例子:监视main中的循环变量i。
(gdb) break main (gdb) run (gdb) watch i Hardware watchpoint 3: i (gdb) continue但需要注意:当i在循环内频繁被修改,每次都会触发,可能过于频繁。如果只想在某个特定条件下暂停,可以用条件监视点(见后文)。
五、监视表达式
watch不仅可以监视单个变量,还可以监视任何合法的表达式。只要表达式的值发生变化,就会触发。
示例:监视sum + i(虽然这不太常用,但展示灵活性)
(gdb) watch sum + i当sum或i任何一个变化导致sum + i的结果改变时,都会触发。
注意:复杂表达式可能无法使用硬件监视点,此时 GDB 会自动降级为软件监视点,软件监视点会让程序单步执行,非常慢,应尽量避免。
六、条件监视点
就像断点可以加条件一样,watch也可以加条件,只关心特定情况。
格式:
watch 变量 condition 监视点编号 条件或者一步到位:
watch 变量 if 条件示例:只监视sum被修改且新值大于 2 时才暂停。
(gdb) watch sum (gdb) condition 2 sum > 2或者:
(gdb) watch sum if sum > 2这样,当sum从 1 变到 3 时会触发(新值 3 > 2),但之前sum从 0 变到 1 时不会触发。
七、管理监视点
监视点和断点共用相同的编号体系,所以可以用管理断点的命令来管理监视点。
| 命令 | 作用 |
|---|---|
info watchpoints或info breakpoints | 查看所有监视点和断点 |
delete 编号 | 删除指定监视点/断点 |
disable 编号 | 暂时禁用监视点 |
enable 编号 | 启用监视点 |
ignore 编号 count | 忽略监视点触发 count 次(针对软件监视点) |
示例:
(gdb) info breakpoints Num Type Disp Enb Address What 1 breakpoint keep y 0x... in main at watch_demo.c:12 2 hw watchpoint keep y sum (gdb) delete 2就会删除对sum的监视。
八、硬件监视点 vs 软件监视点
- 硬件监视点 (hw watchpoint)
依赖处理器提供的调试寄存器,数量有限(一般 x86 有 4 个),但对程序运行速度几乎无影响。 - 软件监视点 (sw watchpoint)
当硬件监视点不够用或表达式复杂无法用硬件实现时,GDB 会使用软件监视点。其原理是每执行一条指令后都检查变量是否变化,导致程序运行极慢。如果看到Software watchpoint提示,要尽量简化监视表达式。
查看监视点类型:info breakpoints中 Type 列会显示hw watchpoint或watchpoint(软件)。
九、实战综合示例:追踪一个神秘错误
假设有一个程序,某个变量counter莫名被清零,我们可以这样调试:
(gdb) watch counter Hardware watchpoint 1: counter (gdb) run ... Hardware watchpoint 1: counter Old value = 100 New value = 0 0x... in some_function () at buggy.c:42立刻就找到了清零的代码位置。
十、✅ watch 命令速查卡
| 命令 | 简写 | 作用 | 示例 |
|---|---|---|---|
watch 变量 | wa 变量 | 当变量被写入时暂停 | watch sum |
rwatch 变量 | rw 变量 | 当变量被读取时暂停 | rwatch flag |
awatch 变量 | aw 变量 | 当变量被读或写时暂停 | awatch status |
watch 变量 if 条件 | - | 条件监视 | watch x if x > 10 |
info breakpoints | i b | 列出所有监视点和断点 | i b |
delete 编号 | d 编号 | 删除监视点/断点 | d 2 |
disable 编号 | - | 禁用监视点 | disable 3 |
enable 编号 | - | 启用监视点 | enable 3 |
十一、练习(由易到难)
基础篇
- 使用上面的
watch_demo.c程序,在 GDB 中对sum设置写监视点,运行并记录每次触发时的新值。 - 在同一个程序中,尝试对
main中的局部变量i设置watch i,观察触发频率。然后删除该监视点。 - 使用
awatch sum,观察程序是否在读取sum时也会暂停(如在printf中)。
进阶篇
编写一个小程序
swap_test.c,内容如下:#include<stdio.h>voidswap(int*a,int*b){inttemp=*a;*a=*b;*b=temp;}intmain(){intx=5,y=10;printf("Before: x=%d, y=%d\n",x,y);swap(&x,&y);printf("After: x=%d, y=%d\n",x,y);return0;}在 GDB 中:
- 对
x设置写监视点(注意:x是main中的局部变量,需在进入main后设置)。 - 运行程序,观察
x是否在swap中被修改,GDB 会停在哪里? - 用
backtrace查看调用栈。
- 对
对指针变量设置监视点:在
swap函数内,对*a设置watch *a,观察它何时变化。
提高篇
- 编写一个程序,包含一个全局数组
int data[10]。在循环中修改data[3]的值。在 GDB 中对data[3]设置写监视点(直接watch data[3])。注意观察,硬件监视点可能支持,但若提示软件监视点,解释原因并记录运行速度。 - 尝试条件监视:监视
sum只在变为偶数时暂停(watch sum if sum % 2 == 0)。用之前的watch_demo.c验证。
十二、答案与关键点解析
基础篇
- 每次暂停输出新旧值,依次为 0→1, 1→3, 3→6。
i每次循环变化都会触发,非常频繁。awatch sum会在printf读取sum时也触发(如果监视成功),通常硬件支持读写监视。
进阶篇
- 编译加
-g -O0。watch x必须在main断点后设置。当执行swap(&x,&y)时,*a的修改会改变x,GDB 会停在*a = *b;或下一行。backtrace显示swap和main。 - 在
swap函数内watch *a,会在*a = *b时触发,旧值 5,新值 10。
提高篇
- 对数组元素
data[3]设置监视点通常可以用硬件监视点(如果支持)。若是软件监视点,运行会明显变慢。 - 条件监视:
watch sum if sum % 2 == 0。当sum从 0 变 1(不触发),1 变 3(不触发),3 变 6(触发,因为新值 6 是偶数)。
十三、学习检查清单
watch和break的根本区别是什么?- 设置
watch x后,什么时候程序会暂停? - 如果监视的局部变量离开了作用域,会发生什么?
- 如何查看当前设置的所有监视点?如何删除一个监视点?
- 硬件监视点和软件监视点有什么区别?哪种更快?
- 怎样设置“只在新值为负数时才暂停”的监视点?
如果这些问题都能流畅回答,说明你已经把watch命令的核心用法完全吸收。现在,你可以用这把“变量监控刀”去精准地剖析任何程序中的数据变化。实际编程中,结合break、print和watch,你将拥有非常强大的调试能力。继续加油!