断点根据代码位置暂停程序,观察点(watchpoint)则根据数据变化暂停程序。当变量的值在复杂代码路径中被意外修改时,观察点通常比逐行跟踪更有效。

一、准备测试程序

下面的程序使用两个线程修改同一个原子变量。使用原子变量是为了避免示例本身包含数据竞争;如果使用普通 int 在两个线程中同时读写,程序会产生未定义行为,不能作为正常的调试示例:

#include <atomic>
#include <thread>

std::atomic<int> g_value{0};

void ThreadFunc1() {
  for (int i = 1; i < 100; ++i) {
    if (i % 4 == 0) {
      g_value.fetch_add(1, std::memory_order_relaxed);
    }
  }
}

void ThreadFunc2() {
  for (int i = 1; i < 100; ++i) {
    if (i % 2 == 0) {
      g_value.fetch_add(1, std::memory_order_relaxed);
    }
  }
}

int main() {
  std::thread first(ThreadFunc1);
  std::thread second(ThreadFunc2);
  first.join();
  second.join();
}

编译时加入调试信息和线程库:

g++ -std=c++17 -g3 -O0 -pthread watchpoint.cpp -o watchpoint

二、写观察点 watch

启动程序并设置观察点:

(gdb) file ./watchpoint
(gdb) start
(gdb) watch g_value
(gdb) continue

当 g_value 的值发生变化时,GDB 会暂停程序并输出旧值和新值。watch 也可以缩写为 wa。

观察点通常依赖处理器硬件调试寄存器。硬件观察点的数量和可观察的数据大小有限,超过限制时 GDB 可能退化为软件观察点,或者报告无法设置观察点。

三、读观察点和读写观察点

3.1 读观察点

rwatch 可以在表达式被读取时暂停程序:

(gdb) rwatch g_value

3.2 读写观察点

awatch 会在表达式被读取或写入时暂停程序:

(gdb) awatch g_value

这两个命令也支持使用 rw 和 aw 作为缩写。读观察点的触发次数可能很多,适合在已经缩小范围后使用。

四、管理观察点

观察点和断点共用编号空间,可以使用下面的命令查看和管理:

(gdb) info breakpoints
(gdb) info watchpoints
(gdb) disable 2
(gdb) enable 2
(gdb) delete 2

可以给观察点设置条件。例如只在值达到某个范围时暂停:

(gdb) condition 2 g_value >= 10

条件表达式的编号需要替换成观察点实际编号。

五、只观察特定线程

GDB 会为每个线程分配一个调试器线程编号。先查看线程信息:

(gdb) info threads

输出中的编号不一定等于操作系统线程 ID。可以使用 thread 切换当前线程:

(gdb) thread 2

设置只在线程 2 中生效的观察点:

(gdb) watch g_value thread 2

多线程程序的执行顺序通常是不确定的,即使使用原子变量,观察点触发的线程和顺序也可能每次不同。调试时应关注状态变化是否合法,不要依赖某一次运行的具体顺序。

六、观察点不生效时的检查项

如果 GDB 无法观察某个变量,可以依次检查:

  1. 是否使用 -g 生成了调试信息。
  2. 是否关闭或降低了优化级别,建议先使用 -O0。
  3. 变量是否已经离开作用域,或只存在于寄存器中。
  4. 硬件观察点数量是否已经耗尽。
  5. 观察的表达式是否包含复杂的临时对象或重载运算。
  6. 多线程程序是否把观察点限定到了错误的 GDB 线程编号。

如果调试目标是定位数据竞争,观察点只能帮助观察某次读写,不能替代线程分析工具。还应结合 ThreadSanitizer 等工具检查同步问题:

g++ -std=c++17 -g -O1 -fsanitize=thread -pthread watchpoint.cpp -o watchpoint-tsan

七、调试截图

下面是使用 watch、删除观察点以及限定线程观察的实验结果:

设置写观察点

管理观察点

限定线程观察

八、总结

  • watch 追踪写入变化,rwatch 追踪读取,awatch 同时追踪读取和写入。
  • 观察点与断点共用编号,可以使用 info watchpoints、disable 和 delete 管理。
  • 使用 watch expr thread id 可以将观察点限定到指定 GDB 线程。
  • 调试多线程程序时应避免在示例中引入未定义的数据竞争。
  • 观察点受调试信息、优化级别和硬件观察点数量限制。