几年前我介绍过 rr 这个 debugger,核心技能是开倒车,比方说我们有一个妙妙程序明明在命令行里指定里 enabled=false,但是跑着跑着内存里的 enabled 变量就成 true 了,我们想知道到底是哪里在修改,可以先 record 一次
然后 replay,直接 continue 运行到程序结束
检查一下此时的内存里的变量,确定此时已经被魔法修改为 true 了。不要问为什么有 ’main.current'.v 这么奇怪的表达式,因为 go 的符号是这样的,在 gdb 里还要手动 set language c 才能 cast type。
然后打个 watchpoint 开始倒车
bt 一下看到有个隐藏的 inotify 在 watch config file 动态修改内存,QED。
上面这个例子虽然是我乱编的,但“追查一个变量的值是什么时候被修改的”在大型项目上是真实的,Cilium 那些复杂、多层级、耦合联动的 config 们让很多工程师都很阳痿(手动@),dockerd 曾经有个“动态注册 etcd endpoints”的功能也让我蛋疼好久,因为工程师可能一开始并不知道大型项目里那些动态修改的花招,也不知是 bug 还是 feature。
不过 LLM 解答万物了,也许人类文明不需要 rr 了,说起来还是有点淡淡的忧伤。
尽管如此,rr 的实现还是很神奇的,它简单来说是这样的:
rr-record: 如果了解 strace 和 ptrace 的话,那 rr-record 大体上还是很好理解的,它用 ptrace syscall 记录 syscall、signal 的调用、顺序、参数、返回值,记录到一个文件里。对于多线程的处理很细,要确定性地手动还原 record 记录下来的调度顺序;corner case 也非常多,比如 vDSO 需要修改 auxv 让 tracee 去读一个假的 rr vDSO 内存,还有 PMU counters 等等。
rr-replay: 如果了解 strace -e inject=syscall:retval=value 这种故障注入的原理的话还是很自然的,它就是把 rr-record 记录下来的 syscall 都拦截下来直接返回,达到一种确定性运行的结果。某些结构性 syscall 还是会执行的,比如 mmap/clone;signal 和调度则会按照 BR_INST_RETIRED.CONDITIONAL 这种 PMU 计数器来确定重放时间,太厉害了!
reverse-continue: 反向运行做法就更巧了,它并非真的反向执行 cpu 指令,毕竟“改革开放不会停顿,长江黄河不会倒流” by __,而是在 replay 的时候每隔一段时间达到 checkpoint ,执行 fork 把整个内存快照出来,同时保持寄存器什么的,然后之后 reverse-cont 就从最近的 checkpoint 开始 replay,实现电表倒转的时空幻境。
力量太大了,随便看一眼细节都要被吓到,这就是 mozilla 的恐怖地带😭
$ rr record ./app -enabled=false
^C然后 replay,直接 continue 运行到程序结束
$ rr replay
(rr) c
Continuing.
Thread 1 received signal SIGINT, Interrupt.检查一下此时的内存里的变量,确定此时已经被魔法修改为 true 了。不要问为什么有 ’main.current'.v 这么奇怪的表达式,因为 go 的符号是这样的,在 gdb 里还要手动 set language c 才能 cast type。
(rr) set language c
(rr) p (('main.Config'*)('main.current'.v))->Enabled
$1 = true然后打个 watchpoint 开始倒车
(rr) watch (('main.Config'*)('main.current'.v))->Enabled
Hardware watchpoint 1: (('main.Config'*)('main.current'.v))->Enabled
(rr) reverse-continue
Continuing.
Thread 1 hit Hardware watchpoint 1: (('main.Config'*)('main.current'.v))->Enabled
Old value = true
New value = falsebt 一下看到有个隐藏的 inotify 在 watch config file 动态修改内存,QED。
上面这个例子虽然是我乱编的,但“追查一个变量的值是什么时候被修改的”在大型项目上是真实的,Cilium 那些复杂、多层级、耦合联动的 config 们让很多工程师都很阳痿(手动@),dockerd 曾经有个“动态注册 etcd endpoints”的功能也让我蛋疼好久,因为工程师可能一开始并不知道大型项目里那些动态修改的花招,也不知是 bug 还是 feature。
不过 LLM 解答万物了,也许人类文明不需要 rr 了,说起来还是有点淡淡的忧伤。
尽管如此,rr 的实现还是很神奇的,它简单来说是这样的:
rr-record: 如果了解 strace 和 ptrace 的话,那 rr-record 大体上还是很好理解的,它用 ptrace syscall 记录 syscall、signal 的调用、顺序、参数、返回值,记录到一个文件里。对于多线程的处理很细,要确定性地手动还原 record 记录下来的调度顺序;corner case 也非常多,比如 vDSO 需要修改 auxv 让 tracee 去读一个假的 rr vDSO 内存,还有 PMU counters 等等。
rr-replay: 如果了解 strace -e inject=syscall:retval=value 这种故障注入的原理的话还是很自然的,它就是把 rr-record 记录下来的 syscall 都拦截下来直接返回,达到一种确定性运行的结果。某些结构性 syscall 还是会执行的,比如 mmap/clone;signal 和调度则会按照 BR_INST_RETIRED.CONDITIONAL 这种 PMU 计数器来确定重放时间,太厉害了!
reverse-continue: 反向运行做法就更巧了,它并非真的反向执行 cpu 指令,毕竟“改革开放不会停顿,长江黄河不会倒流” by __,而是在 replay 的时候每隔一段时间达到 checkpoint ,执行 fork 把整个内存快照出来,同时保持寄存器什么的,然后之后 reverse-cont 就从最近的 checkpoint 开始 replay,实现电表倒转的时空幻境。
力量太大了,随便看一眼细节都要被吓到,这就是 mozilla 的恐怖地带