この記事は Rust Advent Calendar 2023 シリーズ2 の1日目の記事である。
Rustは良くも悪くもシステムプログラミング言語なので、何も工夫しなければデバッグの体験がC言語と同じレベルになってしまう。例えば「rust lldb」でググると上位に Debugging Rust programs with LLDB is a nightmare というスレッドが出てきてしまう。
使うべきツールを知っていれば幾分かマシな体験にできる。Rustコンパイラはデバッガとして LLDB, GDB, WinDbg/CDB をサポート していて、僕はWinDbGは10年くらい触ってないので、この記事ではLLDBとGDBについて書く。
LLDB
Apple SiliconのMacだとGDBが使えないので、必然的にLLDBを使うことになる。
CodeLLDB
GDBと同じく、LLDBにも gui コマンドで起動できる TUI (Text User Interface) があるのだが、なんとなくGDBのそれより使いづらい気がするし、TUIではない普通のGUIフロントエンドの方が何かと使い方もわかりやすくUIもリッチなので、GUIフロントエンドはあった方がデバッグの体験はよくなる。
VSCode拡張に CodeLLDB というのがある。VSCodeを使っている人はCodeLLDBを使えばその場でデバッグが開始できて便利。CodeLLDBの使い方はこちらの記事に以前書いた。
CodeLLDBを使っていると、enumのVecを持つstructとかを眺めている時にそれぞれのenumがどのvariantなのか分からないという問題がある気がしていてまだ解決していないのだが、そのうち調べたら追記する。
rust-lldb
何らかの理由でCLIの lldb を使いたいこともあると思う。素の lldb コマンドを叩いてもデバッグは普通にできるが、rust-lldb というラッパーからlldbを起動しておくと、Rust用のpretty printerが使えるようになる。
後は lldb を普通に使うだけ。前述したように、gui というコマンドを叩くとTUIが起動できる。
GDB
次に述べる rr が便利すぎるというか、LLDBはリプレイやReverse Debuggingができない時点で全然話にならないため、最近はほとんどGDBしか使っていない。
rr-debugger
rr という、GDBを拡張して作られたデバッガがある。素のGDBと違って、rr を使うとプログラムの処理を記録 (rr record コマンド) しておいて、非決定的な部分も含めてrecordと全く同じ動作をするようにデバッガを起動 (rr replay) し、リバース実行 (reverse-next, reverse-continue など) も含めたデバッグができる。
Rustでシステムプログラミングをやる人は多いと思うのだが、言語処理系のようなレイヤーの低いプログラムを書いていると、エラーになった瞬間の状態を調べても何もわからないことは結構あり、しかもそのエラーもごくたまにしか発生しないみたいなことがある。そういった場合、rr replay するとそのエラーの再現率を100%にすることができ、またエラーになった地点から逆方向にステップ実行できるので、デバッグが非常に楽になる。
そういうわけで、デバッグには rr を使うのがおすすめである。リバース実行の使い方も簡単で、普段使っているGDBのコマンドに reverse- をつけるだけで大体動く。
単に rr replay すると素のGDBが起動してしまうのだが、rr replay -d rust-gdb すると次に述べる rust-gdb が使える。
rust-gdb
これも rust-lldb と同じで、Rust向けのpretty printが可能なGDBのラッパである。デフォルトだと有効になってないような気がするが、set print pretty on すれば動く。
Rustの型の名前はいちいち名前空間がついてくるので長いし、コンテナ型もいっぱい使うしで、pretty printなしで値を理解するのが不可能なことはよくある。例えば、長くもなくネストも深くもない場合でも、HashSet とかは厳しい表示だった気がする。
layout src
gdb にもTUIがあり、layout src というコマンドでも起動できるが、僕は C-x a というショートカットで起動している。僕は gdb は結構使い慣れてるので、GDBはGUIフロントエンドは無しで使っている。
CodeLLDB
そういうわけで僕はVSCode拡張とかは rr 向けには使ってないのだが、先ほどのVSCode拡張は名前がCode”LLDB”なのに何故か (中身はGDBの) rr バックエンドをサポートしているらしい。マニュアルの Reverse Debugging というところに使い方が書いてある。GUIが使いたい人はこれを試してみると良さそう。
コンテナ型の中身の取り出し
デバッガ上だと unwrap() とかが使えなくて、Some の中身の取り出し方がわからんみたいなことがあるのだが、そういう時は .0 を使うと動いたりする。他の場合は value とか ptr とか pointer なのだが、これはpretty printしていればわかる。
所感
コアダンプ 1 の調査などでRustのコードが全く呼び出せない状況でデバッグをすることもあるが、直前で述べた .0 みたいなワークアラウンドが必要なところが本当に面倒である。
なので、正直なところ僕も “Debugging Rust programs is a nightmare” だと思っている。ここに書いてないRust特有のデバッグテクニックがあればコメントなどいただけると嬉しい。