如何使用rr进行c++程序的时间旅行调试? (复现非确定性BUG)

rr 是一个 Linux x86_64 平台的确定性记录与回放调试工具,通过完整捕获系统调用、信号、线程调度和内存访问,实现 100% 可复现的时间旅行调试,专治多线程竞争、内存乱序等非确定性 BUG。

什么是 rr,它为什么能抓到非确定性 BUG

rr 不是普通调试器,而是一个「确定性记录与回放」工具。它把程序执行时的所有系统调用、信号、线程调度、内存访问(通过硬件断点/ptrace 拦截)完整记录下来,生成一个 trace 目录。之后无论重放多少次,执行路径都完全一致——这才是时间旅行调试的基础。

对 C++ 程序尤其有用:多线程竞争、内存乱序、未初始化变量、std::thread / std::async 启动时机差异导致的偶发崩溃或逻辑错误,在 rr 下变成 100% 可复现、可单步、可倒退的问题。

注意:rr 仅支持 Linux x86_64(要求 CPU 支持 Intel PT 或使用软件回溯模式),不支持 macOS / Windows;且需关闭 ASLR(echo 0 | sudo tee /proc/sys/kernel/randomize_va_space)才能保证地址空间稳定。

编译和运行 C++ 程序时必须加的参数

rr 对二进制无侵入,但调试体验严重依赖符号和优化控制。不加这些,你会遇到:断点打不到、变量显示为 、栈帧跳变、甚至 rr 自身报 unhandled ptrace event

  • 编译必须带 -g(生成 DWARF 调试信息),否则 rr 回放时 gdb 无法映射源码
  • 推荐用 -O0-O1-O2 及以上常导致变量生命周期被优化掉,倒退时无法读值
  • 避免 -fomit-frame-pointer(现代 GCC 默认不开,但若项目显式加了,需去掉)——rr 依赖帧指针做栈回溯
  • 链接时无需特殊操作;静态链接 libc 不被 rr 支持,务必用动态链接

示例编译命令:

g++ -g -O0 -pthrea

d main.cpp -o myapp

然后用 rr record 启动:

rr record ./myapp

运行结束后会输出类似 rr recorded traceid 1234 的提示。

在 gdb 中倒退执行、设条件断点、检查竞态点

rr 自带 patched 版本的 gdb(通常叫 rr replay),它扩展了 reverse-stepreverse-continuewatch -l 等指令。不要用系统自带 gdb 打开 trace。

  • 启动回放:rr replay(自动加载最新 trace)或指定 trace:rr replay 1234
  • 倒退执行:reverse-step(反向单步)、reverse-next(反向跳过函数)、reverse-continue(反向运行到上一个断点)
  • 监控某变量被谁修改:watch -l my_var-l 表示 location watchpoint,rr 会自动在写该内存的所有位置下断点)
  • 只在特定线程触发断点:thread 3; break MyClass::handle(),再配合 reverse-continue 快速定位该线程出问题前的状态
  • 查看所有线程调度事件:info threads + thread apply all bt,rr 的线程 ID 和 trace 中的 kernel TID 一一对应,不会混淆

典型竞态复现流程:先用 rr replay 运行至 crash,bt 看栈;然后 reverse-continue 回退到 segfault 前几秒;再对疑似共享变量加 watch -lc 一次就停在最后一次写入它的代码行——往往就是那个漏锁的 push_back 或未同步的 flag 修改。

常见失败原因和绕过方法

rr 不是银弹。以下情况会导致 record 失败或 replay 行为异常,不是你用错了,而是底层机制限制:

  • rr: fatal error: Unsupported system call: 431(如某些新内核的 pidfd_getfd)→ 升级 rr 到 v5.8+,或临时降级内核
  • 程序用了 mmap(MAP_SYNC)、GPU ioctl、eBPF 加载等 rr 未覆盖的系统调用 → 在 rr record 后加 --disable-syscall-logging=xxx 屏蔽(但可能丢失部分确定性)
  • 程序 fork 出子进程且子进程长期存活(如守护进程)→ rr 默认只记录主进程树,加 --preserve-files 并确保子进程不脱离 session
  • 使用了 std::random_device(从 /dev/urandom 读)→ rr 会记录其返回值,但若程序依赖真随机性做分支,回放时行为仍确定;如需模拟不同种子,改用 std::mt19937 + 固定 seed

最易被忽略的一点:rr 的「时间旅行」只作用于当前 trace 内存和寄存器状态,不包括外部文件、网络响应、共享内存段(除非由被记录进程创建并管理)。如果 BUG 依赖某个第三方服务返回的特定 JSON 字段,得先 mock 它——rr 解决不了外部不确定性,只解决「程序自身并发不确定性」。