TL; DR

我对超棒的 rr debugger 进行了修改,使其无需访问 CPU 硬件性能计数器即可运行。

这使得 rr 可以在更多环境中使用,例如云 VM 和容器,在这些环境中,通常会禁用对 CPU HW 性能计数器的访问。 上游 rr 需要访问 CPU HW 性能计数器才能运行,所以这是一个新功能,并为使用 rr 调试软件开辟了许多额外的可能性。

我将这个变体称为 Software Counters mode rr。 像 rr 这样的 Record and Replay 系统应该能够在任何地方运行,这是我为此所做的尝试!

在没有访问 CPU HW 性能计数器的情况下运行 rr record/replay 是通过轻量级动态(和静态)instrumentation实现的Software Counters mode rr 的 wiki 包含更多详细信息,如果您对更多内部原理感兴趣的话。

要构建、安装和运行 Software Counters mode rr,请访问 https://github.com/sidkshatriya/rr.soft

继续阅读以了解 Record and Replay 背后的基本概念,以及为什么它是一种强大的程序调试技术。

什么是程序 Record and Replay?为什么它有用?

当您观看 YouTube 视频时,存在一种不言而喻的期望,即当您倒退或前进任意秒数时,视频与录制时完全相同!

您不会在视频中看到以前从未出现过的新内容! 视频和音频完全相同。 事实上,如果您重播视频时视频略有不同,那将会非常奇怪。 如果每次重播视频都略有不同,您可能会认为自己疯了:-)!

允许您在观看视频时前后移动时间至关重要。 一旦视频上传到 YouTube 网站,它基本上就被“冻结”了。 这使得任何人都可以多次观看同一视频,并专注于视频中最有趣的部分或从中学习。

从重放视频到程序

让我们将重点从 YouTube 视频转移到计算机程序。

假设您正在尝试一遍又一遍地运行程序,因为您遇到了一些错误。 当您尝试调试错误时,一个好的策略是尝试为程序提供相同的输入,因为您想了解为什么它对您的特定输入失败。

但遗憾的是,每次运行程序时,情况都略有不同:

我希望您能明白:即使您尝试为足够复杂的程序提供相同的输入,每次运行都是一个特殊的“雪花”。

因此,当程序出错时,您不仅面临一个问题,而是同时面临两个问题:

  1. 在代码中找到程序错误的位置
  2. 尝试设置系统的条件并复制用户输入(以及通常微妙的事情,如线程交错),以便程序再次运行时显示相同的错误。

但正如上面讨论的,每次运行都会发生很多变化! 事实上,这是工程师们古老的问题“但这对我来说是有效的!”或者“出现了一个可怕的错误,很可能在生产中出现……但我不知道如何再次出现这个错误!”

Record and Replay 来救援

如果在运行程序时,您同时使用 record/replay 功能对其进行录制会怎样? 将其视为某种摄像机,但适用于程序。 这样,当您重放它时,它的运行方式完全相同:执行的 CPU 指令完全相同,当“读取”或“写入”文件或进行网络调用时,结果完全相同,依此类推。

作为另一个例子,假设您使用此 record/replay 功能录制了一个 vim/emacs 会话。 当重放录音时,程序代码会认为您在完全相同的时间序列中按下了完全相同的键。 vim/emacs 的内部状态将完全相同! 现在,如果在录制阶段发生 vim/emacs 崩溃,您将能够再次查看它(通过再次重放),并深入研究正在执行的函数并检查数据结构中的程序状态。

这一切是如何运作的? 在录制阶段,程序执行的所有非确定性方面,如随机数、用户输入、用于时钟时间的系统调用和其他系统调用结果都会保存到日志中。 然后,在重放程序时,这些程序执行的“非确定性”方面会根据需要在重放的程序中插入。 更准确地说,当程序被重放时,许多事情实际上并没有再次发生; 例如,实际的网络调用不会再次发出。 重放器只是在日志中查找存储的网络调用结果并插入这些网络调用结果。 重放的程序认为它发出了一个网络调用,但实际上它并没有。 您可以说所有非确定性来源都会在重放期间自动插入,而程序并没有意识到。

我们能得到什么?

现在,如果我们有能力录制然后忠实地重放一个程序“树”(一个程序可能会启动另一个程序,该程序可能会启动其他程序,依此类推。 这是一个执行“树”),那么它将成为一个极其强大的功能。 如果您在录制期间捕获到一个错误,则该错误将在每次将来重放该录制时出现

即使您不想修复程序中的任何错误,Record and Replay 也是有用的。 可能只是您想了解程序如何针对特定输入工作。 现在程序行为对于特定录制不会改变,您可以多次查看该程序的运行并放大该程序重放:查看每个函数如何调用另一个函数等等。 将其视为多次查看视频讲座的棘手部分,以真正理解讲座的内容

此外,如果您有 record/replay 功能,您可以做很多令人惊奇的事情,比如在重放时释放 gbd/lldb 调试器。 您可以前进/后退到执行中,检查回溯、变量,天空才是极限。 最好的部分是,一旦您重放,您将重放程序的“冻结”运行。 每次您重放/调试它时,它的行为都相同,就像我们的 YouTube 视频一样!

rr,一个开源的 Record and Replay 调试器

许多系统可以执行 record/replay。 许多系统要么是专有的,要么是被废弃的,要么具有各种限制,使其在实际中无法使用。 在撰写本文时,我只知道 1 个适用于 Linux 的开源 record/replay 系统具有以下所有功能:

  1. 广泛的可用性(录制/重放以任何语言编写的程序)
  2. 合理良好的性能
  3. 健壮(例如,很好地处理信号,这对于许多 record/replay 系统来说是一大难题)
  4. 以足够非侵入的方式工作(不需要自定义内核模块、自定义 libc、root 权限等)
  5. 简约(例如,仅录制感兴趣的程序“树”,而不是整个虚拟机)
  6. 与现成的调试器(如 gdb 和 lldb)一起工作
  7. 不断改进和开发
  8. 在工业界广泛使用

正如您可能猜到的那样,该系统就是 rr

顺便说一句,要理解一般的 record and replay 系统以及 rr 的工作原理,最好的资源是阅读 arXiv 论文 Engineering Record And Replay For Deployability: Extended Technical Report。 请注意,该报告写于 2017 年,即使代码发生了很多变化(更健壮、大量的错误修复、更多功能、aarch64 支持等),rr 背后的核心概念仍然相同。 在我看来,它仍然是以稍微更深入的层次学习 rr 的最佳资源。 在深入研究本文之前,您应该复习一下 Linux 系统调用、信号、ptrace 等主题(有很多关于 Linux/Unix 系统编程的优秀书籍——选择任何一本)。

rr 的一个限制

现在,每个程序都会进行大量的系统调用,这些系统调用与用户输入、网络调用、内存布局、线程、进程等有关,这些都是非确定性的(即每次运行可能会略有变化)。 当重放程序时,record/replay 系统需要查阅该日志(上面讨论过)。 但是我们如何知道要查阅日志的哪个条目? 我们如何知道程序进行了多远? 我们可以简单地计算迄今为止进行的系统调用次数作为进度的衡量标准,但这由于多种原因而不起作用。 这是一个原因:在录制多线程程序时,线程通常在 CPU 上进行调度和取消调度(抢占)。 抢占点是任意的,即通常甚至可以在程序 调用 syscall 时发生。 在重放期间,我们如何 精确地 知道何时切换线程,以便我们的程序在录制期间以完全相同的线程交错方式运行? 这就是 HW 性能计数器可以派上用场的地方。 HW 性能计数器的一个例子可以是自进程启动以来执行的汇编分支的数量。 这些计数器(通过 Linux perf API 访问)可以帮助衡量程序“进度”。 它们允许您 精确地 查看程序已经进行了多远,以便您知道要在日志中查阅哪个记录,以便可以在重放期间查阅它并执行切换线程等操作。 这是一个高度简化的解释。

虽然 rr 是一个出色的系统,但它有一个很大的缺陷:它假设程序可以访问上面讨论的“CPU 硬件性能计数器”。 正如讨论的那样,这些性能计数器很少在云 VM 和容器中启用。

插曲:我对 Record/Replay 和 rr 的热爱

多年来,我一直对执行 record and replay 的系统,特别是 rr 着迷。 我为 rr 做出了一些代码贡献。

不幸的是,rr(主要)是用 C++ 构建的,在我看来,这是一种庞大、臃肿且复杂的语言。 尽管我尊重 C/C++ 语言的可扩展性和裸机特性,但我认为存在更好的替代方案,例如 Rust。

我想特别提到我的一个旧项目 rd,它是 rr 到 Rust 的一个移植。

我写了 4 篇关于 rd 的博客文章。 在这里阅读关于 rd 的最后一篇博客文章。 如果您有兴趣阅读所有这些文章,它包含指向以前关于 rd 的博客的链接。

一个出色的团队继续致力于 rr,进行错误修复并添加新功能。 使用 rr 才有意义。 对我来说,不断将东西移植回 rd 并不有趣。 由于这个原因和各种其他原因,我决定退休(存档)rd 项目,这很遗憾。

尽管如此,将大型代码库从 C/C++ 移植到 Rust 的实验是一次绝佳的学习体验,并且让我能够理解 rr 调试器的高度复杂的代码库。

Software Counters mode rr 的需求

多年来,我意识到人们越来越多地在受限或受限的环境中工作,例如通常禁用 CPU HW 性能计数器访问的云 VM 和容器。 我想将 rr 的乐趣和强大功能带到这些环境中。

结果就是 Software Counter mode rr

在没有访问 CPU HW 性能计数器的情况下运行 rr record/replay 是通过轻量级动态(和静态)instrumentation实现的Software Counters mode rr 的 wiki 包含更多详细信息,如果您对更多内部原理感兴趣的话。

构建 Software Counters mode rr 确实对我来说是一种爱的劳动。 当我更深入地研究与动态和静态instrumentation、Linux 系统内部原理、信号、底层汇编、DWARF/ELF 等相关的许多领域时,这对我来说是一次非常有意义的旅程。

请访问 https://github.com/sidkshatriya/rr.soft 了解如何在您自己的机器上构建和运行 Software Counters mode rr

由于这是一个个人博客,目前我不接受对其的任何 Pull Requests。 如果您希望就此或任何其他博客文章提请我注意一些重要事项,您可以提交一个 issue。 感谢您的阅读!