静的再コンパイルは、バイナリを外部アーキテクチャから別のターゲット アーキテクチャに変換する有望な方法です。実行直前にコードをコンパイルする必要がなく、余分なコンパイル時間が生成コードの最適化に役立つため、ジャストインタイム (JIT) よりも高速です。
ただし、JIT コンパイルは動的プログラム分析を使用しますが、静的再コンパイルは静的プログラム分析に依存します (名前の由来)。
静的分析では、実行に関するランタイム情報はありません。
これに関する主な問題は、間接的なジャンプによって引き起こされます。switch
この用語は、特定のステートメント、関数ポインターの使用、またはランタイム ポリモーフィズム (仮想テーブルを考えてください) から生成される可能性のあるコードを対象としています。それはすべて、次の形式の命令に要約されます。
JMP reg_A
プログラムの開始アドレスを知っていて、この時点から命令の再コンパイルを開始することにしたとします。ダイレクト ジャンプに遭遇すると、そのターゲット アドレスに移動し、そこから再コンパイルを続行します。ただし、間接ジャンプに遭遇すると、行き詰まります。このアセンブリ命令では、 の内容はreg_A
静的にはわかりません。したがって、次の命令のアドレスはわかりません。動的再コンパイルでは、レジスタの仮想状態をエミュレートし、 の現在の内容がわかっているため、この問題は発生しないことに注意してくださいreg_A
。その上、静的再コンパイルでは、すべての可能な値を見つけることに興味がありますreg_A
この時点で、考えられるすべてのパスをコンパイルする必要があるためです。動的分析では、現在実行しているパスを生成するために現在の値のみが必要です。reg_A
その値を変更する必要があります。他のパスを生成することはできます。場合によっては、静的分析で候補のリストを見つけることができます (その場合switch
、可能性のあるオフセットのテーブルがどこかにあるはずです) が、一般的なケースではわかりません。
それでは、すべての命令をバイナリに再コンパイルしましょう!
ここでの問題は、ほとんどのバイナリにコードとデータの両方が含まれていることです。アーキテクチャによっては、どれがどれか分からない場合があります。
さらに悪いことに、一部のアーキテクチャでは、アライメントの制約や可変幅の命令がなく、ある時点で逆アセンブルを開始すると、オフセットを使用して再コンパイルを開始したことに気付く場合があります。
2 つの命令と 1 つのレジスタで構成される単純化された命令セットを見てみましょうA
。
41 xx (size 2): Add xx to `A`.
42 (size 1): Increment `A` by one.
次のバイナリプログラムを考えてみましょう:
41 42
開始点が最初のバイトだとしましょう41
。あなたがやる:
41 42 (size 2): Add 42 to `A`.
しかし、41 が 1 つのデータである場合はどうなるでしょうか。次に、プログラムは次のようになります。
42 (size 1): Increment `A` by one.
この問題は、多くの場合、アセンブリで直接最適化された古いゲームで拡大され、プログラマーは、コンテキストに応じて、一部のバイトがコードとデータの両方として解釈されることを意図的に期待する可能性があります!
さらに悪いことに、再コンパイルされたプログラム自体がコードを生成している可能性があります。JIT コンパイラーの再コンパイルを想像してみてください。その結果、ソース アーキテクチャのコードが出力され、そこにジャンプしようとするため、プログラムがすぐに停止してしまう可能性が高くなります。実行時にのみ利用可能なコードを静的に再コンパイルするには、無限の策略が必要です!
静的バイナリ解析は非常に活発な研究分野 (主にセキュリティの分野で、ソースが利用できないシステムの脆弱性を探すため) であり、実際に私は、プログラムを静的に再コンパイルしようとする NES エミュレーターを作成しようとする試みを知っています。記事はとても興味深いです。
JIT と静的再コンパイルの間の妥協点は、静的に変換できないビットのみを保持して、できるだけ多くのコードを静的に再コンパイルすることです。