非确定性BUG是C++开发中非常棘手的问题,这类BUG的出现往往依赖特定的时序、内存状态或者外部输入,传统断点调试很难稳定复现问题。rr是一款运行在Linux系统上的时间旅行调试工具,它可以完整记录程序的一次执行过程,之后支持反向执行、反向断点等操作,让开发者可以像倒放视频一样回溯程序运行步骤,非常适合复现和定位非确定性BUG。

rr的核心工作原理
rr的工作分为两个阶段,第一个是记录阶段,它会利用Linux的ptrace和系统调用拦截机制,完整记录程序执行过程中的所有状态变化,包括寄存器值、内存修改、系统调用结果等,将这些信息保存到本地的记录文件中。第二个是重放阶段,rr会严格按照记录的执行顺序重放程序,保证每次重放的行为和记录时完全一致,同时支持反向调试操作,比如反向单步、反向运行到指定断点等。
rr的安装与基础配置
rr目前仅支持Linux系统,且需要内核版本在3.11以上,同时需要CPU支持硬件性能监控功能。以Ubuntu系统为例,安装rr的步骤如下:
# 安装依赖 sudo apt-get install cmake g++ gdb python3 libcap-dev # 克隆rr源码 git clone https://github.com/rr-debugger/rr.git cd rr # 编译安装 mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release .. make -j$(nproc) sudo make install
安装完成后可以通过rr --version命令验证是否安装成功,如果正常输出版本号则说明安装完成。
使用rr记录C++程序执行
首先我们编写一个存在非确定性BUG的C++示例程序,这个程序会创建两个线程对同一个全局变量进行修改,由于线程调度的不确定性,最终变量的值可能不符合预期:
#include <iostream>
#include <thread>
#include <vector>
#include <unistd.h>
int global_counter = 0;
void increment_counter() {
// 模拟随机的线程调度延迟
usleep(rand() % 100);
for (int i = 0; i < 1000; i++) {
global_counter++;
}
}
int main() {
std::vector<std::thread> threads;
// 创建两个线程执行计数操作
for (int i = 0; i < 2; i++) {
threads.emplace_back(increment_counter);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final global_counter value: " << global_counter << std::endl;
return 0;
}
编译这个程序时需要开启调试符号,方便后续调试时查看源码和变量:
g++ -g -o nondeterministic_demo nondeterministic_demo.cpp -lpthread
接下来使用rr记录程序的执行过程,执行以下命令:
rr record ./nondeterministic_demo
执行完成后,rr会在当前目录生成记录文件,同时会输出本次记录的进程ID,后续重放和调试都需要基于这个记录。
使用rr重放并复现非确定性BUG
非确定性BUG的难点在于无法稳定复现,而rr的重放机制可以保证每次重放的行为和记录时完全一致。执行以下命令重放刚才记录的程序:
rr replay
执行后会自动进入GDB调试界面,此时程序的状态和记录时的初始状态完全一致。我们可以先运行程序查看结果:
# 在GDB中输入以下命令运行程序 run
此时输出的global_counter值会和之前记录时的值完全一致,不管你运行多少次replay,只要使用同一个记录文件,结果都不会变化,这就解决了非确定性BUG无法复现的问题。
配合GDB进行时间旅行调试
rr重放模式下支持GDB的大部分反向调试命令,我们可以利用这些命令回溯程序执行过程定位问题:
设置反向断点
假设我们发现最终global_counter的值小于预期的2000,想要知道是在哪个位置出现了计数丢失,可以先在main函数结束的位置设置断点,然后反向运行到修改global_counter的位置:
# 在main函数返回前设置断点 break main # 运行到断点位置 continue # 反向单步执行,回溯程序运行步骤 reverse-step # 反向运行到global_counter++的位置 reverse-break increment_counter
查看历史变量状态
在回溯到global_counter++的位置后,可以查看当前线程的global_counter值,以及相邻几次执行时的值变化,判断是否存在计数覆盖的问题:
# 查看global_counter当前值 print global_counter # 查看寄存器状态 info registers # 查看当前线程信息 info threads
反向继续执行
如果需要回到更早的执行位置,可以使用reverse-continue命令,让程序反向运行到上一个断点位置:
reverse-continue
rr使用注意事项
- rr仅支持Linux系统,且需要CPU支持硬件性能监控特性,部分虚拟机环境可能不支持该特性,建议在物理机或者支持嵌套虚拟化的环境中使用。
- 记录阶段会带来一定的性能开销,通常程序运行速度会比正常执行慢2-5倍,属于可接受范围。
- 记录文件会占用一定的磁盘空间,复杂程序的记录文件可能达到几百MB,需要预留足够的磁盘空间。
- rr目前对C++的支持非常完善,大部分C++标准库的功能都可以正常记录和重放,少部分依赖特殊系统调用的场景可能存在兼容性问题。
常见问题排查
如果执行rr record时提示不支持硬件性能监控,可以检查CPU是否支持,执行以下命令查看:
cat /proc/cpuinfo | grep -E 'perf|pmu'
如果没有相关输出,说明CPU不支持该特性,需要更换运行环境。如果重放时程序行为和记录时不一致,通常是记录文件损坏或者rr版本不匹配导致,可以重新记录后尝试。