周刊(第9期):Mozilla rr使用简介
引言:在之前的周刊(第7期):一个C系程序员的Rust初体验中,简单提到过Mozilla rr这款调试工具,由于这个工具并不是太为人所知,所以本文对该工具做一个简介。
Mozilla rr使用简介
rr是由Mozilla出品的一款调试工具,用官网的话来说:
rr aspires to be your primary C/C++ debugging tool for Linux, replacing — well, enhancing — gdb. You record a failure once, then debug the recording, deterministically, as many times as you want. The same execution is replayed every time.
即它的特点是:可以记录下来程序运行时的上下文环境,包括线程、堆栈、寄存器等等,这样的好处有两个:
- “deterministically”:很多问题问题的产生,都与特定的环境相关,如:
- 线程调度执行的顺序,先执行A线程再B线程,以及反之,可能得到的是不同的结果。
- 环境参数,如输入不同的参数,尤其一些边界条件的触发就跟输入不同的参数有关。
- replay:记录下来程序执行的环境之后,rr除了支持gdb方式的调试之后,还能利用环境来不停的重放程序,甚至反向来执行程序。
以下对rr
的使用做一些简单的介绍。
deterministically
以下面一个最简单的多线程程序来解释何为deterministically
:
#include <pthread.h>
#include <stdio.h>
void * doPrint(void *arg)
{
return NULL;
}
int main() {
pthread_t pid;
pthread_create(&pid, NULL, doPrint, NULL);
printf("pid = %lu\n", pid);
return 0;
}
这个程序很简单:创建一个线程之后,打印线程的pid。
如果多次执行,会发现每次打印出来的pid并不一样:
$ ./a.out
pid = 140301410010880
$ ./a.out
pid = 139804250023680
这个原因自不必多说:每次程序执行的时候,执行环境是有变化的。
这个简单的结论,对应到bug出现的场景上,有的代码可能正常的情况下没有异常,但是会出现在特定的场景下:特定的输入参数、特定的线程执行顺序,等等。换言之,问题并不是必现的,即un-deterministically
。
rr
的一大功能,就是要解决这个deterministically
问题,即在问题发现的时候,能有一个确定的环境,可以反复重现问题。
record and replay
rr
这个名字里的两个r,意指record and replay
,即“记录及回放”,它的使用也很简单,就是这两步:
- record:
rr record /your/application --args
记录下来程序的执行环境。 - replay:
rr replay
,默认将使用最近保存的记录文件进行回放,回放时可以进入类似gdb那样的调试环境。
比如前面那个多线程程序,使用rr
来记录及回放就是:
- record:
$ rr record ./a.out
Freezing performance counters on SMIs should be turned on for maximum rr
reliability on Comet Lake and later CPUs. Consider putting
'w /sys/devices/cpu/freeze_on_smi - - - - 1' in /etc/tmpfiles.d/10-rr.conf
See 'man 5 sysfs', 'man 5 tmpfiles.d' (systemd systems)
rr: Saving execution to trace directory `/home/codedump/.local/share/rr/a.out-0'.
pid = 139837942626048
可以看到记录执行的时候,打印出来的pid
是139837942626048
。
- replay:
$ rr replay
...省略不重要信息...
(进入replay之后第一次执行,是按`c`)
(rr) c
Continuing.
pid = 139837942626048
(前面已经执行完毕,想要第二次执行,按`r`(run))
(rr) r
[Inferior 1 (process 36022) exited normally]
Starting program: /home/codedump/.local/share/rr/a.out-0/mmap_hardlink_4_a.out
Program stopped.
0x00007f2e8f19c100 in ?? () from /lib64/ld-linux-x86-64.so.2
(rr) c
Continuing.
pid = 139837942626048
可以看到,前后两次重放执行,打印的pid
都是之前记录的值139837942626048
。
这就意味着:record下来的程序执行环境,后面可以不停的回放。因为重现问题的场景很重要,有时候还不好复现,那么类似rr
这样能记录下来执行环境并且重放的能力,对于查找问题就特别重要了。
高级用法
前面提到过,使用rr replay
来重放记录时,实际会进入类似gdb那样的一个调试环境,在这个环境里,常用的gdb命令都可以使用,所以这些不会展开讨论,只说一下rr
在这里具备的一些其他更高级的调试能力。
事件
为了能够更加精准的跟进某个问题,rr
提供了事件(event)的概念,每个事件有与之相关的两个值:
- 进程pid。
- 事件编号。
rr
在replay的时候,可以带上-M
参数打印出来事件编号,比如前面的实例程序改成这样:
#include <pthread.h>
#include <stdio.h>
void * doPrint(void *arg) {
int i = *((int*)arg);
printf("in thread %d\n", i);
return NULL;
}
int main() {
int i = 0;
for (i = 0; i < 10; ++i) {
pthread_t pid;
pthread_create(&pid, NULL, doPrint, &i);
printf("pid = %lu\n", pid);
}
return 0;
}
这里创建了10个线程,线程中分别打印循环变量,如果使用-M
在输出时就能看到如下的信息:
(rr) c
Continuing.
[rr 4450 288]pid = 140096584951552
[rr 4450 305]in thread 1
[rr 4450 313]pid = 140096574461696
[rr 4450 330]in thread 2
[rr 4450 337]pid = 140096563971840
[rr 4450 354]in thread 3
[rr 4450 361]pid = 140096555579136
[rr 4450 378]in thread 4
[rr 4450 385]pid = 140096547186432
[rr 4450 402]in thread 5
[rr 4450 409]pid = 140096538793728
[rr 4450 426]in thread 6
[rr 4450 433]pid = 140096530401024
[rr 4450 450]in thread 7
[rr 4450 457]pid = 140096522008320
[rr 4450 474]in thread 8
[rr 4450 481]pid = 140096513615616
[rr 4450 498]in thread 9
[rr 4450 505]pid = 140096505222912
最左边显示事件的格式为[rr pid 事件id]
。
知道了事件对应的(pid,事件id)
二元组之后,在replay的时候,可以指定这两个值,比如:
rr -M replay -g 事件id 或者 rr -M replay -p pid
让程序replay的时候迅速到达指定事件发生的场景下。比如上面的例子中,如果使用rr -M replay -g 354
就能马上重放到[rr 4450 354]in thread 3
这一处。
这种基于事件的调试方式,调试那种代码相同,但是由于输入参数不同导致的问题时,特别管用,因为可以直达问题发生的环境。
反向执行(Reverse execution)
有了记录的能力之后,rr
除了能正向执行程序,还能反向来执行程序,这点在那种看到程序的环境发生了变化,但是不知道怎么发生,想重试一下的情况下特别管用。
单向调试执行程序时,用的是step
、next
、continue
、finish
等命令,反向执行就在这些命令前面加上reverse-
前缀,如reverse-cont
(后面的可以简写)。
缺点
前面介绍了rr
要解决的问题,最后聊一下它的缺陷。
rr
最开始是由Mozilla开发的工具,看来Mozilla对这类运行时问题也是深恶痛绝,发明了很多工具试图提高效率,Rust是另外一个重要的工具,可以参见之前周刊(第7期):一个C系程序员的Rust初体验中我对Rust的使用体验。- 很可惜,这个工具貌似只能在Linux上面运行,并没有Win\Mac版本,而且貌似也不怎么更新了。
- 由于
rr
在记录时,需要记录大量的数据来保存程序运行时的场景,这样一来会给程序带来卡顿,二来会有大量的记录数据,所以并不适合直接在生产环境上使用这个工具。更适合的场景是:已经找到了重现问题的办法,此时可以搭建一个环境开启rr
记录下来重现时的环境,即并不适合漫无目的的就打开这个工具使用。 - 以上
rr
的缺陷,归根到底还是之前提到的:查找运行时的问题太难了,尽量在编译时屏蔽可能出现的问题,真等问题到了运行时再解决的时候,时间、精力、场景复现等等都是不可控的。
番外篇
rr使用的wiki
rr
的github上,有一篇更为详细一些的介绍使用的Usage · rr-debugger/rr Wiki。
rr视频教程
油管上有几个视频教程,见:
- Quick rr Demo - YouTube
- rr 1.2 basic demo - YouTube
- Robert O’Callahan - Corporation Taming Nondeterminism - YouTube
其它文档
GDB相关文档
其他推荐
《如何在开源项目中做重构?》
《如何在开源项目中做重构?》,总结了维护一个开源项目重构的经验。
《Linux containers in 500 lines of code》
Linux containers in 500 lines of code,讲解Linux容器原理的文章。
收集整理远程工作相关的资料
greatghoul/remote-working: 收集整理远程工作相关的资料,但是貌似已经不更新了。