Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

LLVM のデバッグ

NOTE: コード生成に関する情報を探している場合は、代わりにこの 章を参照してください。

このセクションでは、コード生成におけるコンパイラのバグ(たとえば、 コンパイラがなぜ特定のコードを生成したのか、または LLVM でクラッシュしたのか) をデバッグする方法について説明します。 LLVM は大きなプロジェクトであり、おそらく独自のデバッグ文書が必要ですが、 以下では rustc の文脈で重要ないくつかのヒントを示します。

例を最小化する

一般的なルールとして、コンパイラはコードの解析から大量の情報を生成します。 したがって、有用な最初のステップは通常、最小限の例を見つけることです。 これを行う方法の 1 つは、次のとおりです。

  1. 問題を再現する新しいクレートを作成する(たとえば、問題の原因となっている クレートを依存関係として追加し、そこから使用する)

  2. 外部依存関係を削除してクレートを最小化する。つまり、関連するものをすべて 新しいクレートに移動する

  3. コードを短くして、さらに問題を最小化する(これを支援する creduce のような ツールがあります)

上記の手順 2 と 3 の方法論についての詳しい議論として、Rust プログラムの最小化に特化した pnkfelix による大作ブログ記事があります。

LLVM 内部チェックを有効にする

公式コンパイラ(nightly を含む)では LLVM アサーションが無効になっています。 これは、LLVM のアサーション失敗がコンパイラのクラッシュ(ICE ではなく「本物の」 クラッシュ)やその他の奇妙な挙動として現れる可能性があることを意味します。 これらに遭遇している場合は、LLVM アサーションを有効にしたコンパイラ、 つまり “alt” nightly または bootstrap.toml で llvm.assertions = true を設定して 自分でビルドしたコンパイラを使ってみて、何か分かるか確認するのがよいでしょう。

rustc のビルドプロセスでは、LLVM ツールが build/host/llvm/bin にビルドされます。 これらは直接呼び出すことができます。 これらのツールには次のものが含まれます。

  • llc。ビットコード(.bc ファイル)を実行可能コードにコンパイルします。これは LLVM バックエンドのバグを再現するために使用できます。
  • opt。LLVM 最適化パスを実行するビットコード変換ツールです。
  • bugpoint。大きなテストケースを小さく有用なものに縮小します。
  • その他多数。その一部は以下の本文で参照されています。

デフォルトでは、Rust のビルドシステムは LLVM ソースコードや そのビルド構成設定の変更をチェックしません。 そのため、rustc にリンクされる LLVM をリビルドする必要がある場合は、 まず .llvm-stamp ファイルを削除してください。このファイルは build/host/llvm/ にあるはずです。

デフォルトの rustc コンパイルパイプラインには複数のコード生成ユニットがあり、 これを手動で再現するのは困難で、LLVM が並列に複数回呼び出されることを意味します。 問題がなければ(つまり、それによってバグが消えてしまわないのであれば)、 rustc に -C codegen-units=1 を渡すとデバッグが容易になります。

生の LLVM 入力を入手する

rustc に LLVM IR を生成させるには、--emit=llvm-ir フラグを渡す必要があります。 cargo 経由でビルドしている場合は、 RUSTFLAGS 環境変数を使用します(例: RUSTFLAGS='--emit=llvm-ir')。 これにより、rustc は LLVM IR をターゲットディレクトリに出力します。

cargo llvm-ir [options] path は、path にある特定の関数の LLVM IR を出力します。 (cargo install cargo-asmcargo asmcargo llvm-ir をインストールします)。 --build-type=debug はデバッグビルド用のコードを出力します。 他にも有用なオプションがあります。 また、LLVM IR 内のデバッグ情報は出力を大きく散らかす可能性があります。 RUSTFLAGS="-C debuginfo=0" は非常に有用です。

RUSTFLAGS="-C save-temps" は、コンパイル中の さまざまな段階で LLVM ビットコードを出力します。これは場合によって有用です。 出力される LLVM ビットコードは、rustc--out-dir DIR 引数で設定される コンパイラの出力ディレクトリ内の .bc ファイルになります。

  • rustc 自体を呼び出したときに LLVM バックエンドからアサーション失敗や セグメンテーションフォルトが発生している場合は、これらの .bc ファイルをそれぞれ llc コマンドに渡してみて、同じ失敗が発生するか確認するのがよいでしょう。 (LLVM 開発者は、多くの場合、最小化された再現に Rust クレートを使うものよりも、 .bc ファイルに縮小されたバグを好みます。)

  • LLVM ビットコードの人間が読めるバージョンを取得するには、 ビットコード(.bc)ファイルを llvm-dis を使って .ll ファイルに変換するだけです。 llvm-dis は、ターゲットの rustc ローカルコンパイルに含まれているはずです。

-O が有効かどうかによって、LLVM の最適化なしでも rustc が異なる IR を出力することに 注意してください。そのため、rustc が出力する IR を試したい場合は、 次のようにする必要があります。

$ rustc +local my-file.rs --emit=llvm-ir -O -C no-prepopulate-passes \
    -C codegen-units=1
$ OPT=build/$TRIPLE/llvm/bin/opt
$ $OPT -S -O2 < my-file.ll > my

LLVM パイプライン中の LLVM IR だけを取得したい場合、たとえば、 どの IR が最適化時のアサーション失敗を引き起こすのかを確認したい場合や、 LLVM が特定の最適化をいつ実行するのかを確認したい場合は、 rustc フラグ -C llvm-args=-print-after-all を渡し、必要に応じて -C llvm-args='-filter-print-funcs=EXACT_FUNCTION_NAME を追加できます(例: -C llvm-args='-filter-print-funcs=_ZN11collections3str21_$LT$impl$u20$str$GT$\ 7replace17hbe10ea2e7c809b0bE')。

これは標準エラーに大量の出力を生成するため、それを何らかのファイルにパイプしたくなるでしょう。 また、-filter-print-funcs-C codegen-units=1 のどちらも使用していない場合、複数のコード生成ユニットが並列に実行されるため、 出力が混ざり合い、何も読めなくなります。

  • 前述の方法論に関する 1 つの注意点として、LLVM の -print 系のオプションは、 パスが実行される IR ユニット(たとえば、関数だけ)しか出力せず、 参照される宣言、グローバル、 メタデータなどは含まれません。これは、一般には -print の出力を llc に渡して特定の問題を再現することはできないことを意味します。

  • LLVM 自体の内部では、 SafeStackLegacyPass::runOnFunction の先頭で F.getParent()->dump() を呼び出すと、 モジュール全体がダンプされます。これは 再現のためのより良い基盤を提供するかもしれません。 (ただし、-C save-temps によってダンプされた .bc ファイルから 同じダンプを取得できるはずです。)

特定の関数の IR だけが必要な場合(たとえば、その関数がなぜアサーションを引き起こすのか、 または正しく最適化されないのかを確認したい場合)は、llvm-extract を使用できます。 例:

$ ./build/$TRIPLE/llvm/bin/llvm-extract \
    -func='_ZN11collections3str21_$LT$impl$u20$str$GT$7replace17hbe10ea2e7c809b0bE' \
    -S \
    < unextracted.ll \
    > extracted.ll

LLVM 最適化パスを調査する

最適化パスが原因で誤った動作が発生している場合、非常に便利な LLVM オプションとして -opt-bisect-limit があります。これは、実行する最も大きいパスのインデックス 値を表す整数を受け取ります。 実行対象となったパスのインデックス値は、実行ごとに安定しています。 これを、結果として得られるプログラムに基づいて探索空間の二分探索を自動化するソフトウェアと組み合わせることで、問題のあるパスを素早く特定できます。 -opt-bisect-limit が指定されている場合、すべての実行が 標準エラーに表示され、そのインデックスと、その パスが実行されたかスキップされたかを示す出力も併せて表示されます。制限をインデックス -1 に設定すると(例: RUSTFLAGS="-C llvm-args=-opt-bisect-limit=-1")、すべてのパスと それに対応するインデックス値が表示されます。

最適化パイプラインを試したい場合は、rustc が出力した LLVM IR とともに ./build/host/llvm/bin/ にある opt ツールを使用できます。

LLVM 自体の実装を調査する場合は、その内部デバッグインフラストラクチャを 把握しておく必要があります。 これは LLVM Debug ビルドで提供されており、rustc の LLVM ビルドでは bootstrap.toml の次の設定を変更することで有効にできます。

# LLVM assertions が有効かどうかを示します
llvm.assertions = true

# LLVM ビルドが Release ビルドか Debug ビルドかを示します
llvm.optimize = false

簡単にまとめると次のとおりです。

  • assertions=true を設定すると、粗い粒度のデバッグメッセージングが有効になります。
    • さらに、optimize=false を設定すると、細かい粒度のデバッグメッセージングが有効になります。
  • LLVM における LLVM_DEBUG(dbgs() << msg) は、rustc における debug!(msg) のようなものです。
  • -debug オプションはすべてのメッセージングを有効にします。これは rustc で 環境変数 RUSTC_LOG=debug を設定するようなものです。
  • -debug-only=<pass1>,<pass2> バリアントはより選択的です。これは rustc で 環境変数 RUSTC_LOG=path1,path2 を設定するようなものです。

ヘルプの入手と質問

質問がある場合は、rust-lang Zulip、特に #t-compiler/wg-llvm チャンネルにアクセスしてください。

知って使いこなしたいコンパイラオプション

-C help-Z help コンパイラスイッチは、役立つ可能性のあるさまざまな 興味深いオプションを一覧表示します。 LLVM 開発に関連する、最も一般的なものをいくつか示します(その一部は上記の チュートリアルで使用されています)。

  • --emit llvm-ir オプションは、テキスト形式の LLVM IR を含む <filename>.ll ファイルを出力します
    • --emit llvm-bc オプションは、バイトコード形式(<filename>.bc)で出力します
  • -C llvm-args=<foo> を渡すと、llc や opt のようなツールが受け付ける ほぼすべてのオプションを渡せます。 例: 各 LLVM パスの前に IR を出力するには -C llvm-args=-print-before-all を指定します。
  • -C no-prepopulate-passes は、LLVM パスマネージャーにパスのリストを 事前投入することを避けます。 これにより、最適化後の LLVM IR ではなく、 rustc が生成する LLVM IR を確認できます。
  • -C passes=val オプションでは、実行する追加の LLVM パスをスペース区切りのリストで指定できます
  • -C save-temps オプションは、コンパイル中のすべての一時出力ファイルを保存します
  • -Z print-llvm-passes オプションは、実行される LLVM 最適化パスを出力します
  • -Z time-llvm-passes オプションは、各 LLVM パスの時間を測定します
  • -Z verify-llvm-ir オプションは、LLVM IR の正当性を検証します
  • -Z no-parallel-backend は、別個のコンパイル単位の並列コンパイルを無効にします
  • -Z llvm-time-trace オプションは、Chrome プロファイラー互換の JSON ファイルを出力します。 これには LLVM パスの詳細とタイミングが含まれます。
  • -C llvm-args=-opt-bisect-limit=<index> オプションを使用すると、LLVM 最適化を二分探索できます。

LLVM バグレポートの提出

LLVM バグレポートを提出する場合、問題を実証する何らかの最小の 動作例が必要になるでしょう。 Godbolt compiler explorer はこれに非常に役立ちます。

  1. 問題のあるコードの LLVM IR を入手したら(上記参照)、Godbolt で 最小の動作例を作成できます。 llvm.godbolt.org にアクセスしてください。

  2. プログラミング言語として LLVM-IR を選択します。

  3. llc を使用して、IR をそのまま特定のターゲット向けにコンパイルします。

    • 便利なフラグがいくつかあります。-mattr はターゲット機能を有効にし、-march= はターゲットを選択し、-mcpu= は CPU を選択する、などです。
    • llc -march=help のようなコマンドは利用可能なすべてのアーキテクチャを出力します。これは、 Rust のアーキテクチャ名と LLVM の名前が一致しないことがあるため便利です。
    • どこかで rustc を自分でコンパイルしている場合、ターゲットディレクトリに llcopt などのバイナリがあります。
  4. LLVM-IR を最適化したい場合は、opt を使用して LLVM の 最適化がそれをどのように変換するかを確認できます。

  5. 問題を示す godbolt リンクを入手したら、LLVM のバグを登録するのは非常に簡単です。 その GitHub Issues ページにアクセスするだけです。

LLVM からのバグ修正の移植

バグが LLVM のバグであると特定できたら、それがすでに LLVM で報告され修正されているものの、私たちがまだ その修正を取得していないことがわかる場合があります(あるいは、あなたが LLVM に十分詳しく、自分で修正できる場合もあります)。

その場合、rustc がより簡単に利用できるように、そのバグの修正を 私たち自身の LLVM フォークに直接移植することを選択できる場合があります。 私たちの LLVM フォークは rust-lang/llvm-project で管理されています。 そこで修正を取り込んだら、サブモジュールのコミットを変更する PR も取り込む必要があります。 助けが必要な場合は Zulip で聞いてください。