C/C++向け rr – 記録・再生デバッガ
(rr-project.org)- rr は Linux 上で失敗した実行を一度記録し、その同じ実行を gdb で繰り返し再生できるようにすることで、再現が難しい C/C++ バグの追跡を容易にする
- 再生中はアドレス空間、レジスタ、システムコールのデータ、メモリ配置が毎回同じになり、オブジェクトのアドレスやイベント順序といった デバッグの手掛かり が失われない
- 一般的な gdb コマンド、スクリプティング、IDE 統合をサポートし、ハードウェアのデータ watchpoint と 逆方向実行 を組み合わせて、値が変わった地点まで巻き戻せる
- Firefox、Chrome、QEMU、LibreOffice、Go プログラム、マルチプロセスのワークロードやコンテナまで扱える一方、単一コアのエミュレーション、共有メモリ、CPU・システムコールのサポート範囲には制約がある
- 断続的な失敗や、ファザー・ランダム障害注入によって生じた失敗を保存して繰り返し分析できるため、バグ修正コストを下げ、ソフトウェア品質を高めるのに有用である
rr のデバッグモデル
- rr は Linux 向けの C/C++ デバッグツールであり、gdb を置き換えるのではなく、記録・再生機能によって gdb ワークフローを強化する
- 失敗が発生した実行を一度保存すれば、その後は同じ実行を好きなだけ繰り返してデバッグできる
- 再生はライブ実行のように非決定的に変化せず、毎回同一の状態で進行する
- gdb の下で効率的な 逆方向実行 を提供する
- breakpoint と data watchpoint を設定できる
- 注目地点まで素早く reverse-execute できる
- 実際のアプリケーションで使われており、複数の開発者が実際のバグ修正に活用している
サポート機能と適用範囲
- rr の機能は実運用アプリケーションと gdb ベースの作業フローに合わせて設計されている
- 類似ツールより低いオーバーヘッド。特に大半がシングルスレッドのワークロードで低オーバーヘッド
- Firefox、Chrome、QEMU、LibreOffice、Go プログラムなど多様なアプリケーションの記録・再生をサポート
- コンテナ全体を含む マルチプロセスワークロード の記録・再生・デバッグをサポート
- gdb スクリプティングと IDE 統合 をサポート
- マシン間で移動可能な、永続性があり圧縮された trace をサポート
- 断続的バグをより表面化しやすくする Chaos mode を提供
記録と再生の流れ
- アプリケーションは
rr record /your/application --argsで記録する - 失敗を含む実行全体がディスクに保存され、その後
rr replayでデバッグできる - 再生中はライブ実行ではなく、記録された trace をデバッグする
- アドレス空間、レジスタ内容、システムコールのデータは再生のたびに同一に保たれる
- 一般的な gdb コマンドの大半をそのまま使える
- デバッグセッションを再開し直す必要があるときは、gdb の
runコマンドで記録を最初から再生する- 再起動後も同じ実行が再び再生される
- デバッグ状態は再起動の間も保持される
- 動的に割り当てられたオブジェクトの
thisポインタも再生セッションごとに同じである- メモリ割り当てが毎回同じなので、監視するアドレスをハードコードできる
逆方向実行と watchpoint
- rr は、値が誤って設定された原因を探す際に、結果から原因へさかのぼる 形のデバッグを可能にする
- Firefox の layout デバッグ例では、
mRect.widthの値が誤っていることを確認した後、watch -l mRect.widthとreverse-contを使う - ハードウェア watchpoint と reverse execution を組み合わせ、値が
11220から12000に変わった地点を突き止める - この方法により、問題が表面化した時点から実際の変更地点まで戻る作業を減らせる
インストールと開始
- ソースからビルドするには Building And Installing の手順に従えばよい
- パッケージが合わない場合はソースビルドが推奨される
- カーネル変更や OS アップデートによって rr 側の変更が必要になることがある
- Fedora のインストール例では
rr-5.9.0-Linux-$(uname -m).rpmをダウンロードし、sudo dnf installで導入する - Ubuntu のインストール例では
rr-5.9.0-Linux-$(uname -m).debをダウンロードし、sudo dpkg -iで導入する
断続的な失敗を扱う方法
- rr の当初の動機は、断続的な失敗 のデバッグを容易にすることだった
- 断続的な失敗は、ある実行では失敗が現れないためデバッグが難しい
- テスト実行を低オーバーヘッドで記録し、失敗が起きたらその実行をデバッガで繰り返し再生できる
- 決定論的再生により、一般的なバグデバッグでも手掛かりを蓄積し続けられる
- 一般的なデバッガでは再実行時に注目オブジェクトのアドレスや重要なイベント順序が変わり、以前の情報が役に立たなくなることがある
- rr では失敗した実行について得た知識が、再生を繰り返しても保持される
- デバッグは通常、結果から原因へさかのぼる過程なので、時間を逆向きに実行できれば原因追跡が容易になる
- rr は低オーバーヘッドの記録・再生システムと、gdb の reverse execution コマンド向けの実用的なバックエンドを提供する
- 大小さまざまな多くのプロジェクトで、開発者が定期的に利用している実用ツールである
決定論的再生が保証する状態
- rr は Linux ユーザー空間のプロセスグループを記録する
- 記録対象プロセスがカーネルから受け取るすべての入力と、非決定的な CPU 効果をキャプチャする
- 再生では 命令レベルの制御フロー、メモリ、レジスタ内容の保持が保証される
- メモリ配置は常に同じで、オブジェクトのアドレスは変わらず、レジスタ値は同一で、システムコールは同じデータを返す
- ファザーやランダム障害注入ツールは rr と組み合わせるとさらに強力になる
- これらのツールは断続的な失敗を引き起こすのは得意だが、同じ失敗を再現するのは難しいことがある
- rr はランダム実行を記録し、失敗した場合は保存された記録によって問題を決定論的にデバッグできるようにする
既存の記録・再生ツールとの違い
- rr は、record-and-replay debugging という古くからあるアイデアを、特定の設計目標に沿って実装したものだ
- 当初の焦点は Firefox だった
- 多くの記録・再生手法は特定言語を必要としたり、スケールが合わなかったりして Firefox を扱いにくい
- Firefox は複雑なアプリケーションなので、Firefox のデバッグに有用なら一般にも有用である可能性が高い
- 配備しやすさを重視している
- rr は一般的な Linux カーネルと汎用ハードウェアで動作する
- システム設定の変更を要求しない
- 他の多くの手法は、カーネル変更や仮想マシン内での OS 実行を必要とする
- 実行時間オーバーヘッドの低さを目標としている
- gdb ワークフローを置き換えるには、gdb を使う場合と近い速度で結果を得られなければならない
- 低オーバーヘッドはテストへの攪乱も減らす
- 設計の単純さを重視している
- 動的バイナリ計測のような複雑な手法に依存するアプローチを避けている
- 単純さは rr の堅牢性と低オーバーヘッドにも寄与している
性能と制約
- rr のオーバーヘッドはアプリケーションのワークロードによって異なる
- Firefox のテストスイートでは、記録性能は実用的な水準にある
- 場合によっては slowdown が ≤ 1.2x まで下がる
- 10 分かかるテストスイートは、rr での記録時には約 12 分で済む程度である
- オーバーヘッドはワークロードによって大きく変動しうる
- 大半がシングルスレッドのプログラムでは、既知の競合する記録・再生システムよりはるかに低いオーバーヘッドを持つ
- 主な制約は次のとおり
- 単一コアマシンをエミュレートするため、並列プログラムは単一コア実行による slowdown を受ける
- 記録ツリー外のプロセスとメモリを共有するプロセスは記録できない
- 記録プロセスでは X shared memory のような機能を自動的に無効化する
- 比較的新しい x86 CPU、または Apple M1+ のような特定の ARM CPU が必要である
- 記録対象プロセスが実行するすべてのシステムコールを把握している必要がある
- Firefox や複数のアプリケーションに必要な広範なシステムコールをサポートするが、完全ではない
- サポートされていないシステムコールは GitHub issue で報告できる
- カーネル変更、システムライブラリアップデート、新しい CPU ファミリへの対応のため更新が必要になることがある
参考資料とコミュニティ
- Extended Technical Report は rr の動作方式と性能を扱う概要資料である
- rr wiki は rr に関する技術トピックを扱う
- 質問は mailing list または #rr on chat.mozilla.org で行える
- デモ動画も提供されている
1件のコメント
Hacker News のコメント
rr で変数変更ブレークポイントと reverse-continue を併用し、大規模なコードベースをリバースエンジニアリングするのに非常にうまく活用したことがある
深い場所にあるコアロジックを抽出するのに少し時間はかかったが、有用だった
誰かが rr を Rust にポートしようと試み、約 6 万行まで書いた後にプロジェクトをアーカイブした点も言及に値する
C++ から Rust に書き直す際の影響、利点、限界、難しさを比較する興味深いケーススタディになりそうだ
https://github.com/sidkshatriya/rd/
ただし、それを公式版へ移すのが難しかった大きな理由は、1) rr には 10 年間蓄積された、奇妙なカーネル/プロセス挙動に対応するための複雑な修正が多く、ポート中に失われることを懸念したこと、2) rr の上に作られた非公開ソースのプロジェクト remix[0] も一緒にポートする必要があったことだった
[0] https://robert.ocallahan.org/2020/12/rr-remix-efficient-repl...
Rust はよく知らないが、C との相互運用性は良さそうだ。すでにうまく動いているソフトウェアなら、Rust での書き直しの利点が何なのか気になる
ざっと確認すると rr 1.0 には 3 年かかり、3〜4 人程度の大きな貢献があり、少なくとも 5 人が貢献したように見える。現在の rr はそこにさらに 10 年分の作業が加わった結果だ
本当に C/C++ 専用なのか気になる
限定的な理解では、デバッガにはシンボル一覧、Windows の .pdb ファイルのようなものと Linux の対応物、システムコールの理解、その他同様のものが必要だ。デバッグ対象のバイナリを何が生成したかは、あまり気にしないと思っていた。もちろんネイティブコードであることが前提だ
rr が Rust、Zig、Odin、Nim のような言語でも動作しないのか気になる。Python、JS、C# のようにマネージドメモリを使う言語は当然期待していない
Zig のアロケータと組み合わせるとかなり便利だ。解放時に 0xaa バイトを書き込み、アドレスを再利用しないのでクラッシュする可能性が高く、その後そのメモリにウォッチポイントを設定して、解放された時点まで巻き戻れる
https://github.com/python/devguide/issues/1283
https://morepypy.blogspot.com/2016/07/reverse-debugging-for-...
https://github.com/mesalock-linux/mesapy/blob/mesapy2.7/READ...
システムを GDB の視点でしか見せないが、インタープリタ言語でもコンパイル言語でも動作し得る
動作しないのは、マップされたアドレスをドライバが直接更新する場合だ。CUDA がその例で、再生するにはドライバとの相互作用をモデル化する必要があり、UVM に至る前からすでにそうだ
もう一つ良い点は、RR がプロセスツリーを記録するため、実行ファイルが起動した他のプロセスを簡単に調べられることだ
ただし rr はシステムコールをすべてラップする必要があり、Linux に非常に特化しているため Windows では動かない。Linux のシンボル情報形式は DWARF だ
rr は本当に素晴らしいが、自分が「大物兵器」として持ち出す決心をするたび、ほとんどの場合は並行性バグで、そのため rr では再現できないことが多かった
それでも、いくつかの言語が rr をツールチェーンに直接組み込んでくれたら本当に良いと思う。もちろん、いつでも rr/gdb を「ただ」使うことはできるが、rr の呼び出し設定と使用が Python の pdb と同じくらい簡単になるところを想像してほしい
基本的には実行中のスレッドを頻繁に切り替えて、複数コアが同時に実行されている状況を模倣する。実際に自分の場合はいくつかの競合状態を見つけてくれたが、もちろん限界はある
MPI ジョブで rank 0 にだけ rr を付け、異なる send/recv の順序が問題を引き起こす箇所を見つけた。しかも、多くのネイティブコード生成と結びついた Python モデルだったので、かなり複雑な問題だった
使いたいが、自分の作業の大半は何らかの形で GPU が絡んでいる
本当に必要だったときは Mac だったが、残念ながら rr は Linux でしか動かない
Mac で動作し、おそらくマルチスレッドにも対応できる Undodb があるが、残念ながら約5 万ドルかかる
rr ベースだが、プログラム実行全体をクエリ可能なデータベースとして追加してくれる https://pernos.co/ も見る価値がある
こういうことができる
GDB に組み込まれている リバースデバッグ もある: https://www.sourceware.org/gdb/wiki/ProcessRecord/Tutorial
rr のほうが多くの機能と柔軟性を提供していると思うが、いずれにせよ GDB 自体もすでにしばらく前からリバースデバッグができていた
GDB の実装は必要なときに記録を開始/停止できるので rr より便利だが、効率は数桁低い。ごく小さなコード片にしか使いものにならず、それ以外では実質的に永遠に終わらないか、リソースが尽きる
動作し、バグの追跡にも役立ったが、苦痛なほど遅かった。そもそも使えるようにするために入力サイズを小さくする必要があり、幸いその後も問題を再現できた
目的のプログラム状態まで 10000 回未満の再起動で二分探索できるなら、リバース実行を使うよりマシン時間は少なくて済む。実際、低速化があまりに大きいので、デバッガを対話的にほぼ正しい状態まで何度か移動させてからプログラムを再起動するだけで十分な場合も多い
gdb のリプレイ機能を有用に使えたのは、起動後 1 秒もたたずにプログラムをクラッシュさせる入力ファイルがあったからだ。そのため「この変数が間違っている」から「この変数はどうやってその誤った値に設定されたのか?」まで、数分待つだけでさかのぼることができた
Windows では同じ用途に WinDbg を使える
マルチスレッド問題のデバッグ支援がより優れている
https://www.forrestthewoods.com/blog/windbg-time-travelling-...
rr は記録・再生方式のタイムトラベルデバッグ実装を使っており、適切に実装されていればはるかに少ないオーバーヘッドで済む。最後に見たとき、rr のオーバーヘッドは約 2 倍の低速化程度で、記憶が正しければ他の記録・再生型タイムトラベルデバッガでは 10% 程度のものも見たことがある
10% は WinDbg より 100 倍安く、プロダクションで常時オンにしておくのに十分低い。これはゲームチェンジャーとなる差だ
昔の VMWare Workstation もこうした機能をサポートしていた
ユーザー空間のプログラムだけでなく、VM 内の カーネルやドライバ まで可能だった。この機能はリリースされ、数バージョンにわたって存在したあと、社内政治のため削除された
プラグインで拡張可能で、プラグインがエミュレーション中の CPU を完全に制御できた。似たものがまだどこかにあるのか気になる
rr の Ryzen CPU 問題 は、もう確実に過去のものになったのか気になる
かなり新しい Threadripper Pro 7950 で Firefox の実行を記録するために rr を毎日終日使っており、Pernosco とも併用している。GitHub の rr wiki に動作させる方法が説明されている。小さな回避策を適用するだけで、非常に安定して動作する
過去の議論もある
https://news.ycombinator.com/item?id=31617600 (2022年6月)
https://news.ycombinator.com/item?id=18388879 (2018年11月)