この記事は 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特有のデバッグテクニックがあればコメントなどいただけると嬉しい。
-
「RustはSEGVしないのにコアダンプ?」と思う人もいるだろう。僕はRust書いてるのに毎日SEGVしてる…というのは置いておくにしても、例えば本番環境でごくたまに
assert!
やpanic!
でクラッシュするみたいなケースでは、ログを仕込み直して再現を待つよりコアダンプを見る方が楽だと思う。 ↩