第4章:观察点入门

调试嵌入式程序时,我们经常遇到这样的场景:一个变量莫名其妙地被改了,但不知道是谁干的。断点?打断点太多次了,根本找不到元凶。这时候,观察点就派上用场了。

我个人习惯把观察点叫做「数据断点」。它不关心代码执行到哪一行,只关心你指定的数据有没有变化。说白了,就是给变量装了个监控摄像头。

什么是观察点

观察点(Watchpoint)是GDB提供的一种特殊断点。它监视某个内存地址或变量,一旦值发生变化,程序就会停下来。

举个例子。我在项目中遇到过这样一个bug:一个全局结构体的某个字段,总是在某个函数返回后变成0。打断点?函数里几百行代码,根本不知道哪一行改的。用观察点,直接监视那个字段,程序一改它就会停。嗯,问题瞬间定位。

观察点的核心原理:

  • 硬件观察点:利用CPU的调试寄存器实现,效率高,数量有限(通常4个)
  • 软件观察点:通过GDB单步执行并检查值变化,效率低,但数量不限

重要概念:观察点监视的是内存地址,不是变量名。变量可能被优化掉,但内存地址是实实在在的。

观察点与断点的区别

很多初学者搞不清这两个东西。我画个图帮你理解:

断点 观察点 监视代码位置 停在指定行号 关心「执行到哪里」 适合跟踪代码流程 数量无限制 监视数据变化 停在变量被改时 关心「数据怎么变的」 适合定位数据异常 硬件观察点有限 断点问「到哪了?」 观察点问「谁动了我的数据?」

你想想看,断点就像在路口设卡检查,只查经过这个路口的人。观察点呢?就像给某个物品装了定位器,不管它被带到哪里,都能找到。

具体区别我用个表格总结:

对比项 断点 观察点
监视对象 代码行/函数 内存地址/变量
触发条件 执行到指定位置 数据值发生变化
典型场景 跟踪执行流程 定位数据被篡改
性能影响 几乎无影响 软件观察点较慢
数量限制 无限制 硬件观察点有限

设置第一个观察点

好,理论说完了,咱们动手试试。我会用一个简单的例子带你走一遍。

先写个测试程序:

// test_watch.c
#include <stdio.h>
#include <unistd.h>

int counter = 0;

void modify_counter() {
    counter = 42;  // 谁调用了这个函数?
}

int main() {
    for (int i = 0; i < 5; i++) {
        counter = i * 10;
        printf("counter = %d\n", counter);
        sleep(1);
    }
    
    modify_counter();
    printf("final counter = %d\n", counter);
    return 0;
}

编译时记得加 -g 选项:

gcc -g -o test_watch test_watch.c

启动GDB并设置观察点:

gdb ./test_watch
(gdb) start
(gdb) watch counter
Hardware watchpoint 1: counter
(gdb) continue

程序会在 counter 每次变化时停下来。你会看到类似这样的输出:

Continuing.
Hardware watchpoint 1: counter

Old value = 0
New value = 0
main () at test_watch.c:12
12              counter = i * 10;

嗯,这里要注意。第一次触发时,Old value 和 New value 都是0。为什么?因为 counter 被初始化为0,循环第一次赋值时,值从0变成0,GDB认为这也是变化。我个人习惯在设置观察点后,先 continue 一次跳过这个无意义的触发。

小技巧:如果你只想监视变量被写入特定值,可以用条件观察点。比如 watch counter if counter > 100,只有超过100时才停下。

观察点的三种变体

GDB提供了三种观察点命令,各有各的用处:

  • watch:监视写入操作。变量被写入时触发。
  • rwatch:监视读取操作。变量被读取时触发。
  • awatch:监视读写操作。变量被读取或写入都触发。

我在项目中遇到过一个问题:某个全局变量总是被读成0,但明明之前赋过值。用 rwatch 监视读取操作,发现是另一个模块在初始化之前就读取了它。这种问题用普通断点很难查,但观察点一下就抓住了。

注意事项:

  • 硬件观察点数量有限,ARM Cortex-M系列通常只有2-4个
  • 观察点不能监视局部变量(函数返回后地址失效)
  • 优化编译(-O2)可能导致观察点失效,建议调试时用 -O0

实战:用观察点定位野指针

讲个真实案例。有一次我调试一个网络协议栈,发现某个缓冲区的内容总在 recv 之后被破坏。代码几千行,打断点根本找不到谁干的。

我的做法:

  1. 找到缓冲区地址(假设是 0x1000)
  2. 设置观察点:watch *(char*)0x1000
  3. 运行程序,等待触发

结果发现是一个DMA传输越界了,写到了缓冲区后面的地址。观察点直接告诉我哪个地址被写了,省了我至少半天时间。

核心要点:观察点是你调试数据相关问题的利器。遇到「变量值莫名其妙变了」、「内存被踩」、「数据被篡改」这类问题,第一个想到的就应该是观察点。

好了,观察点的基本用法就这些。记住一句话:断点管流程,观察点管数据。下次遇到数据异常,试试观察点,你会爱上它的。

可可精品资料库