2

周刊(第9期):Mozilla rr使用简介

 2 years ago
source link: https://www.codedump.info/post/20220313-weekly-9/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

引言:在之前的周刊(第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

可以看到记录执行的时候,打印出来的pid139837942626048

  • 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除了能正向执行程序,还能反向来执行程序,这点在那种看到程序的环境发生了变化,但是不知道怎么发生,想重试一下的情况下特别管用。

单向调试执行程序时,用的是stepnextcontinuefinish等命令,反向执行就在这些命令前面加上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视频教程

油管上有几个视频教程,见:

GDB相关文档

《如何在开源项目中做重构?》

《如何在开源项目中做重构?》,总结了维护一个开源项目重构的经验。

《Linux containers in 500 lines of code》

Linux containers in 500 lines of code,讲解Linux容器原理的文章。

收集整理远程工作相关的资料

greatghoul/remote-working: 收集整理远程工作相关的资料,但是貌似已经不更新了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK