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

2022-03-13
6分钟阅读时长

引言:在之前的周刊(第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: 收集整理远程工作相关的资料,但是貌似已经不更新了。