LLVM bugpointツール:設計と使用方法

説明

bugpointは、LLVMツールとパスにおける問題の原因を絞り込むためのツールです。最適化のクラッシュ、最適化による誤コンパイル、またはネイティブコード生成の不良(静的コンパイラとJITコンパイラの問題を含む)の3種類のエラーのデバッグに使用できます。大きなテストケースを小さく、有用なものに削減することを目的としています。たとえば、optがファイルの最適化中にクラッシュした場合、クラッシュを引き起こす最適化(または最適化の組み合わせ)を特定し、クラッシュを引き起こす小さな例にファイルを縮小します。

optやLLVMコードジェネレータのデバッグなど、詳細なケースシナリオについては、LLVMバグレポートの提出方法を参照してください。

設計思想

bugpointは、LLVMインフラストラクチャへのフックを一切必要とせずに、有用なツールとなるように設計されています。すべてのLLVMパスとコードジェネレータで動作し、「動作方法」を知る必要はありません。このため、愚かな動作に見える場合や、明らかな簡素化を見逃す場合があります。bugpointは、コンパイラのデバッグプロセスにおいてプログラマの時間をコンピュータの時間とトレードオフするように設計されています。そのため、テストケースの縮小には長い時間(無人)がかかる場合がありますが、それでも価値があると私たちは考えています。bugpointは、プログラムの実行(時間がかかる)が必要な誤コンパイルのデバッグでない限り、一般的に非常に高速です。

デバッガの自動選択

bugpointは、コマンドラインで指定された各.bcまたは.llファイルを読み取り、それらを単一のモジュール(テストプログラム)にリンクします。コマンドラインでLLVMパスが指定されている場合、テストプログラムでこれらのパスを実行します。パスがクラッシュした場合、または不正な出力を生成した場合(検証プログラムが中止される)、bugpointクラッシュデバッガを起動します。

それ以外の場合、-outputオプションが指定されていない場合、bugpointは「安全な」バックエンド(良好なコードを生成すると仮定される)を使用してテストプログラムを実行し、参照出力を生成します。bugpointがテストプログラムの参照出力を持つと、選択したコードジェネレータで実行しようとします。選択したコードジェネレータがクラッシュした場合、bugpointはコードジェネレータでクラッシュデバッガを起動します。それ以外の場合、結果の出力が出力と異なる場合、その違いはコードジェネレータのエラーによって発生したと仮定し、コードジェネレータデバッガを起動します。

最後に、選択したコードジェネレータの出力が参照出力と一致する場合、bugpointは、すべてのLLVMパスが適用された後にテストプログラムを実行します。その出力が参照出力と異なる場合、その違いはLLVMパスのいずれかのエラーによって発生したと仮定し、誤コンパイルデバッガに入ります。それ以外の場合、bugpointがデバッグできる問題はありません。

クラッシュデバッガ

最適化またはコードジェネレータがクラッシュした場合、bugpointは、パスリスト(最適化クラッシュの場合)とテストプログラムのサイズを可能な限り削減しようとします。まず、bugpointは、どの最適化パスの組み合わせがバグを引き起こすかを特定します。これは、たとえばoptによって公開される問題をデバッグする場合に役立ちます(38個以上のパスを実行するため)。

次に、bugpointは、テストプログラムから関数を削除してサイズを縮小しようとします。通常、手続き内最適化をデバッグする場合、単一の関数にテストプログラムを縮小できます。関数の数が減ると、制御フローグラフのさまざまなエッジを削除して、関数のサイズを可能な限り小さくしようとします。最後に、bugpointは、存在しないことで障害が解消されない個々のLLVM命令を削除します。最後に、bugpointは、クラッシュするパスを伝え、ビットコードファイルを提供し、optまたはllcで障害を再現する方法に関する指示を提供する必要があります。

コードジェネレータデバッガ

コードジェネレータデバッガは、選択したコードジェネレータによって誤コンパイルされているコードの量を絞り込もうとします。これを行うために、テストプログラムを取り、それを2つの部分に分割します。1つは「安全な」バックエンドでコンパイルする部分(共有オブジェクトに)、もう1つはJITまたは静的LLCコンパイラで実行する部分です。問題の可能性のある範囲を縮小するために、LLVMコードジェネレータにプッシュされるコードの量を減らすためのいくつかの手法を使用します。終了後、2つのビットコードファイル(それぞれ「test」[コードジェネレータでコンパイルされる]と「safe」[「安全な」バックエンドでコンパイルされる]と呼ばれる)と、問題を再現するための手順を出力します。コードジェネレータデバッガは、「安全な」バックエンドが良好なコードを生成すると仮定します。

誤コンパイルデバッガ

誤コンパイルデバッガは、コードジェネレータデバッガと同様に機能します。テストプログラムを2つの部分に分割し、一方の部分で指定された最適化を実行し、2つの部分を再びリンクしてから、結果を実行することによって機能します。誤コンパイルを引き起こしているパス(1つまたは複数)にリストを絞り込み、誤コンパイルされているテストプログラムの部分を削減しようとします。誤コンパイルデバッガは、選択したコードジェネレータが正しく動作すると仮定します。

bugpointの使用に関するアドバイス

bugpointは非常に便利なツールですが、場合によっては分かりにくい方法で動作します。いくつかのヒントとコツを示します。

  • コードジェネレータと誤コンパイルデバッガでは、bugpointは、決定的な出力を生成するプログラムでのみ機能します。したがって、プログラムがargv[0]、日付、時刻、またはその他の「ランダム」なデータを生成する場合、bugpointは、これらのデータの違いを出力時に誤コンパイルの結果として誤解する可能性があります。実行ごとに変化する可能性のある出力を無効にするために、プログラムを一時的に修正する必要があります。

  • クラッシュデバッガでは、bugpointは、削減中の異なるクラッシュを区別しません。したがって、新しいクラッシュや誤コンパイルが発生した場合、bugpointは新しいクラッシュで続行します。特定のクラッシュに固執する場合は、エラーメッセージを検証するためのチェックスクリプトを作成する必要があります。bugpoint - 自動テストケース削減ツール-compile-commandを参照してください。

  • コードジェネレータと誤コンパイルデバッガでは、プログラムまたはその入力を手動で修正して実行時間を短縮しても、問題が発生する場合はデバッグが高速になります。

  • bugpointは、新しい最適化に取り組む際に非常に役立ちます。回帰を迅速に追跡するのに役立ちます。ただし、最適化を変更するたびにbugpointを再リンクする必要を回避するために、-loadオプションを使用してbugpointが最適化を動的にロードするようにします。

  • bugpointは多くの出力を生成し、長時間実行される可能性があります。プログラムの出力をファイルにキャプチャすることが役立つことがよくあります。たとえば、Cシェルでは、次のように実行できます。

    $ bugpoint  ... |& tee bugpoint.log
    

    これにより、端末だけでなく、ファイルbugpoint.logにもbugpointの出力がコピーされます。

  • bugpointは、LLVMリンカーの問題をデバッグできません。「すべての入力がOK」メッセージが表示される前にbugpointがクラッシュした場合、同じ入力ファイルセットでllvm-link -vを試してください。それでもクラッシュする場合は、リンカーのバグが発生している可能性があります。

  • bugpointは、LLVMのバグを事前に見つけるのに役立ちます。-find-bugsオプションを指定してbugpointを呼び出すと、指定された最適化のリストがランダム化され、プログラムに適用されます。バグが見つかるか、ユーザーがbugpointを強制終了するまで、このプロセスが繰り返されます。

  • bugpoint は、長い名前を含むIRを生成することがあります。opt -passes=metarenamer をIRに実行して、読みやすいメタ構文的な名前を使用してすべてを名前変更します。または、opt -passes=strip,instnamer を実行して、非常に短い(多くの場合、純粋に数値的な)名前ですべてを名前変更します。

bugpointが不十分な場合の対処法

場合によっては、bugpoint では不十分です。特に、InstCombineとTargetLoweringはどちらも、多くの潜在的な変換を持つビジター構造のコードを持っています。bugpointの使用プロセスで、まだ解明するには多すぎるコードが残っており、問題がinstcombineにあると思われる場合は、次の手順が役立つ場合があります。これらの同じテクニックは、TargetLoweringでも役立ちます。

-debug-only=instcombine を有効にして、「IC」を含む行を選択することで、instcombine内でどの変換が実行されているかを確認します。

この時点で、決定を下す必要があります。変換の数がデバッガーを使用してステップ実行できるほど少ないかどうかです。もしそうであれば、それを試してみてください。

変換が多すぎる場合は、ソースコードの変更アプローチが役立つ場合があります。このアプローチでは、テスト入力に対して実行されている変換のみを無効にするためにinstcombineのソースコードを変更し、変換のセットに対してバイナリサーチを実行できます。「visit*」メソッド(例:visitICmpInst)のInstCombinerをメソッドの最初の行に「return false」を追加することで変更できます。

それでも十分に削除できない場合は、InstCombiner::DoOneIterationの呼び出し元であるInstCombiner::runOnFunctionを変更して、反復回数を制限します。

ここで「-stats」を使用すると、instcombineのどの部分が実行されているかを確認できます。これは、追加のレポートコードを配置する場所の指針となります。

この時点で、変換の数がまだ多すぎる場合は、visit関数のコード本体を実行するかどうかを制限するコードを挿入すると役立つ場合があります。関数の呼び出しごとにインクリメントされる静的カウンターを追加します。次に、必要な範囲でfalseを返すコードを追加します。例:

static int calledCount = 0;
calledCount++;
LLVM_DEBUG(if (calledCount < 212) return false);
LLVM_DEBUG(if (calledCount > 217) return false);
LLVM_DEBUG(if (calledCount == 213) return false);
LLVM_DEBUG(if (calledCount == 214) return false);
LLVM_DEBUG(if (calledCount == 215) return false);
LLVM_DEBUG(if (calledCount == 216) return false);
LLVM_DEBUG(dbgs() << "visitXOR calledCount: " << calledCount << "\n");
LLVM_DEBUG(dbgs() << "I: "; I->dump());

visitXORvisitXorを呼び出し212と217のみに適用するように制限するために追加できます。これは実際のテストケースからのもので、重要な点を提起しています。相互作用する変換では、複数の呼び出しを分離する必要があるため、単純なバイナリサーチでは不十分な場合があります。TargetLoweringでは、return false;の代わりにreturn SDNode();を使用します。

これで変換の数が管理可能な数に減ったため、出力を見て、どの変換が行われているかを調べることができます。それが判明したら、通常のデバッグを行います。どのコードが実行されている変換に対応するかが明らかでない場合は、呼び出し回数に基づいた無効化後にブレークポイントを設定し、コードをステップ実行します。あるいは、「printf」スタイルのデバッグを使用して、ウェイポイントを報告することもできます。