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。
立即学习“C++免费学习笔记(深入)”;
- 编译必须带
-g(生成 DWARF 调试信息),否则 rr 回放时 gdb 无法映射源码 - 推荐用
-O0或-O1;-O2及以上常导致变量生命周期被优化掉,倒退时无法读值 - 避免
-fomit-frame-pointer(现代 GCC 默认不开,但若项目显式加了,需去掉)——rr 依赖帧指针做栈回溯 - 链接时无需特殊操作;静态链接 libc 不被 rr 支持,务必用动态链接
示例编译命令:
g++ -g -O0 -pthread main.cpp -o myapp
然后用 rr record 启动:
rr record ./myapp
运行结束后会输出类似 rr recorded traceid 1234 的提示。
在 gdb 中倒退执行、设条件断点、检查竞态点
rr 自带 patched 版本的 gdb(通常叫 rr replay),它扩展了 reverse-step、reverse-continue、watch -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 -l,c 一次就停在最后一次写入它的代码行——往往就是那个漏锁的 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 解决不了外部不确定性,只解决「程序自身并发不确定性」。











