GDB 监视点 watch
一、什么是 watch?------ 变量的"监控摄像头"
之前学过的 break 是按位置暂停 (比如某一行代码),而 watch 是按数据变化暂停。
watch 的作用 :
告诉 GDB 持续监视某个变量(或表达式),只要它的值发生变化,就立刻中断程序执行,并显示旧值和新值。
典型场景:
- 某个变量的值莫名其妙被修改,不知道在哪改的
- 想要知道一个全局变量或关键局部变量何时被赋予新值
- 代替在大量地方手动加
printf来追踪变量变化
二、监视点的三种类型
GDB 根据监视的触发条件,将监视点分为三类:
| 类型 | 命令 | 触发条件 |
|---|---|---|
| 写监视点 (watchpoint) | watch 表达式 |
当被监视的表达式的值被写入(修改)时触发 |
| 读监视点 (read watchpoint) | rwatch 表达式 |
当被监视的表达式的值被读取时触发 |
| 读写监视点 (access watchpoint) | awatch 表达式 |
当被监视的表达式的值被读取或写入时触发 |
最常用的是 watch,也就是写监视点。今天重点掌握它。
三、如何使用 watch ------ 完整流程演示
准备工作:写一个简单的测试程序
创建文件 watch_demo.c:
c
#include <stdio.h>
int sum = 0; // 全局变量,方便全程监视
void add(int n) {
sum += n; // 这里会修改 sum
}
int main() {
int i;
for (i = 1; i <= 3; i++) {
add(i);
printf("After adding %d, sum = %d\n", i, sum);
}
return 0;
}
编译(必须加 -g,建议不要优化):
bash
gcc -g -O0 watch_demo.c -o watch_demo
进入 GDB 并设置监视点
bash
gdb ./watch_demo
1. 先给 main 函数设一个断点,确保程序跑起来后再设 watch
(因为 watch 要监视的变量必须存在于当前作用域)
gdb
(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
(gdb) watch sum
Hardware watchpoint 2: sum
"Hardware watchpoint" 表示系统使用硬件寄存器来监视,速度很快。
3. 继续运行程序
gdb
(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
(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
(gdb) break main
(gdb) run
(gdb) watch i
Hardware watchpoint 3: i
(gdb) continue
但需要注意:当 i 在循环内频繁被修改,每次都会触发,可能过于频繁。如果只想在某个特定条件下暂停,可以用条件监视点(见后文)。
五、监视表达式
watch 不仅可以监视单个变量,还可以监视任何合法的表达式。只要表达式的值发生变化,就会触发。
示例 :监视 sum + i(虽然这不太常用,但展示灵活性)
gdb
(gdb) watch sum + i
当 sum 或 i 任何一个变化导致 sum + i 的结果改变时,都会触发。
注意:复杂表达式可能无法使用硬件监视点,此时 GDB 会自动降级为软件监视点,软件监视点会让程序单步执行,非常慢,应尽量避免。
六、条件监视点
就像断点可以加条件一样,watch 也可以加条件,只关心特定情况。
格式:
gdb
watch 变量
condition 监视点编号 条件
或者一步到位:
gdb
watch 变量 if 条件
示例 :只监视 sum 被修改且新值大于 2 时才暂停。
gdb
(gdb) watch sum
(gdb) condition 2 sum > 2
或者:
gdb
(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
(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
(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,内容如下:c#include <stdio.h> void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } int main() { int x = 5, y = 10; printf("Before: x=%d, y=%d\n", x, y); swap(&x, &y); printf("After: x=%d, y=%d\n", x, y); return 0; }在 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,你将拥有非常强大的调试能力。继续加油!