rr 是一个 Linux x86_64 平台的确定性记录与回放调试工具,通过完整捕获系统调用、信号、线程调度和内存访问,实现 100% 可复现的时间旅行调试,专治多线程竞争、内存乱序等非确定性 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)才能保证地址空间稳定。
rr 对二进制无侵入,但调试体验严重依赖符号和优化控制。不加这些,你会遇到:断点打不到、变量显示为 、栈帧跳变、甚至 rr 自身报 unhandled ptrace event。
-g(生成 DWARF 调试信息),否则 rr 回放时 gdb 无法映射源码-O0 或 -O1;-O2 及以上常导致变量生命周期被优化掉,倒退时无法读值-fomit-frame-pointer(现代 GCC 默认不开,但若项目显式加了,需去掉)——rr 依赖帧指针做栈回溯示例编译命令:
g++ -g -O0 -pthread main.cpp -o myapp
然后用 rr re
cord 启动:
rr record ./myapp
运行结束后会输出类似 rr recorded traceid 1234 的提示。
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 屏蔽(但可能丢失部分确定性)--preserve-files 并确保子进程不脱离 session
std::random_device(从 /dev/urandom 读)→ rr 会记录其返回值,但若程序依赖真随机性做分支,回放时行为仍确定;如需模拟不同种子,改用 std::mt19937 + 固定 seed最易被忽略的一点:rr 的「时间旅行」只作用于当前 trace 内存和寄存器状态,不包括外部文件、网络响应、共享内存段(除非由被记录进程创建并管理)。如果 BUG 依赖某个第三方服务返回的特定 JSON 字段,得先 mock 它——rr 解决不了外部不确定性,只解决「程序自身并发不确定性」。