Rustプログラムのデバッグ辛すぎ問題

この記事は [Rust Advent Calendar 2023 シリーズ2](https://qiita.com/advent-calendar/2023/rust) の1日目の記事である。

Rustは良くも悪くもシステムプログラミング言語なので、何も工夫しなければデバッグの体験がC言語と同じレベルになってしまう。例えば「rust lldb」でググると上位に [Debugging Rust programs with LLDB is a nightmare](https://www.reddit.com/r/rust/comments/b1krwb/debugging_rust_programs_with_lldb_is_a_nightmare/) というスレッドが出てきてしまう。

使うべきツールを知っていれば幾分かマシな体験にできる。Rustコンパイラはデバッガとして [LLDB, GDB, WinDbg/CDB をサポート](https://rustc-dev-guide.rust-lang.org/debugging-support-in-rustc.html#supported-debuggers) していて、僕は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](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) というのがある。VSCodeを使っている人はCodeLLDBを使えばその場でデバッグが開始できて便利。CodeLLDBの使い方は[こちらの記事](https://qiita.com/k0kubun/items/e5b0201869f32c911c4f)に以前書いた。

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](https://rr-project.org/) という、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](https://github.com/vadimcn/codelldb/blob/master/MANUAL.md#reverse-debugging) というところに使い方が書いてある。GUIが使いたい人はこれを試してみると良さそう。

### コンテナ型の中身の取り出し

デバッガ上だと `unwrap()` とかが使えなくて、`Some` の中身の取り出し方がわからんみたいなことがあるのだが、そういう時は `.0` を使うと動いたりする。他の場合は `value` とか `ptr` とか `pointer` なのだが、これはpretty printしていればわかる。

## 所感

コアダンプ [^1] の調査などでRustのコードが全く呼び出せない状況でデバッグをすることもあるが、直前で述べた `.0` みたいなワークアラウンドが必要なところが本当に面倒である。

なので、正直なところ僕も "Debugging Rust programs is a nightmare" だと思っている。ここに書いてないRust特有のデバッグテクニックがあればコメントなどいただけると嬉しい。

[^1]: 「RustはSEGVしないのにコアダンプ?」と思う人もいるだろう。僕はRust書いてるのに毎日SEGVしてる…というのは置いておくにしても、例えば本番環境でごくたまに`assert!`や`panic!`でクラッシュするみたいなケースでは、ログを仕込み直して再現を待つよりコアダンプを見る方が楽だと思う。