エミュレーターはファイル内のバイナリコードを解析しますか?

エミュレーターはファイル内のバイナリコードを解析しますか?

実行できると主張するエミュレータをいくつか見たことがありますが、実際に実行はできるものの、ソース コードを見ると、すべての 1 と 0 を直接解析して命令を決定しているわけではないことがわかります。

私の質問は、エミュレーターが実際の CPU とまったく同じオペコードをエミュレートする必要がある場合、CPU を正当に (またはまったく) エミュレートするには、ゲームの正しいバイナリ オペコード形式を解析する必要があるのではないでしょうか?

たとえば、ゲーム ファイルには、次のようにマークされた 1 つの命令、1 バイトを保存します。

0000 1111

私のプログラムは、この命令が実際に意味することを確認する必要があります (例:「A レジスタに 1 を加算する」)。ただし、これを保証するために、テキスト ファイル内のすべての 0 と 1 をチェックする必要はないでしょうか。

次に、エミュレーターはバイト全体を解析しますが、バイト全体は 8 ビットであり、変動するパターンによって操作出力が変わります。

たとえば、0000 1111 は A に 1 を加算することを意味しますが、0000 1110 は A に A を加算することを意味します。

答え1

説明 - 質問に直接答えようとする

エミュレータのソース コードを読み取っていて、バイナリ (実行可能) ファイルの特定のビットを読み取らずにコードを忠実に実行している場合、次の 3 つの結果が考えられます。

  1. あなたは間違っているエミュレータはファイルのすべての部分を読み取らないと考え、実際には、あなたは単に間違っています。
  2. あなたは正しいエミュレーターはすべてのビットを読み取っているわけではありません。これは、エミュレートするプログラムの動作に関する特定の事実を想定できるため、すべてのビットを読み取らなくても、必要な処理を把握できるからです (特定のゲーム エンジン、特定の種類のグラフィックス API、特定の種類のサウンド API などが実行されることを想定しているためかもしれません)。
  3. あなたは正しいエミュレーターはすべてのビットを読み取っているわけではありません。実行可能ファイルには、プログラムを正しく実行するために必要のないビットがあるためです。それらは、レガシーな「不要なもの」やメタデータ、または実際にはプログラムの機能を構成しない余分なものなどである可能性があります。
  4. あなたは正しいエミュレータは、コード内の特定の操作をより高レベルの操作に変換し、低レベルのプロセッサ/ハードウェア固有の命令を完全にバイパスするため、すべてのビットを読み取っているわけではありません。たとえば、複雑な操作を行っている人のビデオ録画で、その人が行っていることを正確に真似するように求められ、「箱の側面に穴を開けてください」と言われた場合、ビデオを見るのをやめて、ビデオの人物の文字通りの動作に従うのではなく、穴を開ける方法に関する既存の経験を活用したくなるでしょう (適切なドリルを装備していて、人生で一般的な経験があることを前提としています)。同様に、エミュレータは、プログラムが特定の座標セットで画面に 32x32 の画像を描画するように要求していると推測できる場合、それがどのような画像であるか、その画像の形式は何か、どこに描画するかがわかった時点でコードの読み取りを停止できます。エミュレートされたシステムがどのように描画しているかを確認する必要はありません。

エミュレータの仕組み

別のプラットフォームやCPUのコードを実行するエミュレータ(例えば、ワイン) はさまざまな段階で処理を実行します。一部の段階はエミュレータが動作するために絶対に必要ですが、その他の段階はオプションであり、パフォーマンスの最適化の可能性を表します。

  • 必須: 実行可能コード (マシンコード、MSIL、Java バイトコードなど) を「解析」します。解析構成:

    • 実行可能コードの各ビットを読み取ります。
    • ネイティブ コードが何を実行しているかを理解するために、ネイティブ コードの各ビット/バイト (または使用するその他の個別の情報測定単位) のレイアウト/形式 (構文) と目的 (セマンティクス) を十分に理解します。
    • 理解するプログラムが言うことをエミュレータが理解しなければならない構文バイナリ形式の、そしてセマンティクス構文は「32ビットの符号付き整数を最小符号ビット形式で表現する」などから成り、意味論は「ネイティブコードにオペコードが含まれている場合」などから成ります。52つまり、関数呼び出しを行うということです。」
    • ニモニック(なぜこれが必要なのか思い出すのに役立ちます):レシピに従うことに専念していたとしても、そのレシピを完全に無視し、読むそれをやると、私がランダムにいろいろなことを試さない限り、そのレシピに従うことは不可能です。レシピに必要なのと同じ手順を実行するように強制する。同様に、ランダムなCPU命令をランダムに実行してプログラムと同じ機能を実行するランダムモンテカルロシミュレーションがない限り、エミュレータはプログラムは言う。

  • 必須: 解析されたコード(通常は何らかの抽象データモデル、ステートマシン、抽象構文木など)を「翻訳」して、上級コマンド(例:CまたはJavaのステートメント)または低レベルコマンド (例: x86 プロセッサの CPU 命令) よりも、高レベルのコマンドの方が最適になる傾向があります。たとえば、長い CPU 命令シーケンスのコード フローを分析し、高レベルで、要求されているのはディスクから特定の MP3 ファイルを再生することであると判断すると、命令レベルのエミュレーション全体をスキップし、ネイティブ プラットフォームの MP3 デコーダ (プロセッサ用に最適化されている可能性があります) を使用して同じ MP3 ファイルを再生できます。一方、エミュレートされたプログラムの実行をできるだけ文字通りに「トレース」すると、命令をネイティブに実行することで得られる最適化の多くを放棄することになるため、速度が遅くなり、最適ではなくなります。

  • オプション: エミュレートされたプログラム コードの大部分、またはプログラム全体のコード フローを「最適化」して分析し、実行の完全なシーケンスを決定し、エミュレータがネイティブ プラットフォームの機能を使用してこの動作をエミュレートする方法の非常に詳細で洗練されたモデルを構築します。 Wine はこれをある程度実行しますが、変換するコードが x86 対 x86 であるという事実によって助けられています (つまり、どちらの場合も CPU は同じ命令セットであるため、Windows コードを外部の UNIX ベースの環境に接続して「ネイティブに」実行させるだけです)。


ケーキの例え

エミュレータのパフォーマンスを検討するときは、次のシナリオで、誰かがケーキを焼いているビデオ(音声付き)を見ているときに、自分で手順を書き留めるのに何枚の紙が必要になるかを考えてみましょう。

  • これまでの人生で一度も手を動かしたり、体の筋肉を鍛えたりしたことがない場合は、(ヒント: 手の動き、手と目の協調、角度、速度、位置、つかむ、道具を持つ、こねるなどの基本的なテクニックの詳細な手順を記録するには、何千枚もの紙が必要になります。)

  • 基本的な運動制御能力(自分で歩いたり、食事ができる)はあるが、これまで一度も食事を準備したことがない場合;(ヒント: 個々の手順を記録するには数十枚の紙が必要で、こねたり、慣れない道具の持ち方を覚えるには多くの練習が必要になるでしょうが、前のケースよりもはるかに短い時間で記録することができます)

  • これまでケーキを焼いたことがないけれど、食べ物を準備したことがある場合は、(ヒント: 紙は数枚必要ですが、10 枚以内です。材料の計量、かき混ぜ方などは、すでに慣れているはずです。)

  • これまで何度もケーキを焼いたことがあり、その工程には慣れているが、この特定の種類/味のケーキの焼き方がわからない場合(ヒント:基本的な材料とオーブンで焼く時間を書き留めるには、紙の半分くらい必要かもしれません。それだけです)

基本的に、これらの「エミュレータ能力」のレベルが上がると、エミュレータはより高レベルの処理を「ネイティブに」(既に知っているルーチンと手順を使用して) 実行できるようになり、「トレース」(エミュレートされたプログラムから文字通り従っているルーチンと手順を使用して) を実行する必要が少なくなります。

この例えをコンピューター用語で説明すると、次のようなエミュレータを想像することができます。実際のハードウェアをエミュレートするエミュレートされたプログラムが実行されるハードウェアの動作を忠実に「トレース」し、場合によってはハードウェア(回路)レベルまでトレースします。これはとてもサウンド ファイルを再生しようとしていることを理解できるほど高度なレベルでプログラムを分析して、エミュレートされたプログラムの指示をトレースすることなくそのサウンド ファイルを「ネイティブに」再生できるエミュレータと比較すると、速度は遅くなります。


「トレース」(別名、機械的な模倣)と「ネイティブ実行」について

最後にもう一つ。トレースが遅いのは、エミュレートする対象の詳細で複雑なコンポーネントを「複製」するために大量のメモリを使用する必要があるためであり、ホストCPUで命令を実行するだけでなく、命令を実行する命令(間接レベルがわかりますか?)、これは非効率につながります。プログラムだけでなくコンピュータ システムの物理ハードウェアも徹底的にエミュレートすると、CPU、マザーボード、サウンド カードなどをエミュレートすることになり、エミュレータが CPU の実行を「トレース」するのと同じように、プログラムの実行も「トレース」することになります。トレースのレベルがこれだけ高くなると、全体が非常に遅くなり、扱いにくくなります。

ここでは、エミュレータがエミュレートするために入力プログラムのすべてのビット/バイトを読み取る必要がない詳細な例を示します。

エミュレートされたソフトウェア環境用の C または C++ で書かれた API (詳細は重要ではありません) を知っていて、この API には関数があるとしますvoid playSound(string fileName)。この関数のセマンティクスは、ディスク上のファイルを開き、その内容を読み取り、ファイルのエンコード (MP3? WAV? または他の何か?) を判断し、通常の/予想されるサンプル レートとピッチでスピーカーから再生することであるとします。ネイティブ コードから、「サウンドの再生を開始するには、playSound ルーチンに入る/home/hello/foo.mp3」という一連の命令を読み取った場合、プログラム コードの読み取りをそこで停止し、自分の(最適化された!) サウンド ファイルをネイティブに開いて再生するルーチンです。エミュレートされたプロセッサを命令レベルで追う必要がありますか? いいえ、API の動作を理解していれば、その必要はありません。


野生の差が生まれる!(高地のトラブル)

もちろん、上記の例のように、一連の命令を読んで高レベルの実行計画を「推測」すると、正確に元のハードウェアで実行されている元のプログラムの動作を模倣します。たとえば、元のハードウェアには、同時に 8 つのサウンド ファイルしか再生できないというハードウェア制限があったとします。最新のコンピューターが 128 個のサウンド ファイルを同時に問題なく再生でき、ルーチンをplaySound高度なレベルでエミュレートしている場合、一度に 8 個を超えるサウンド ファイルを再生できないのはなぜでしょうか。これにより、エミュレートされたバージョンのプログラムで奇妙な動作 (良くも悪くも) が発生する可能性があります。これらのケースは、慎重にテストするか、元の実行環境を非常によく理解することで解決できます。

たとえば、DOSBox には、エミュレートされた DOS プログラムの実行速度を意図的に制限できる機能があります。これは、一部の DOS プログラムは、フルスピードで実行できるようにすると正しく動作しないためです。実際には、期待される速度で実行するために CPU のクロック レートのタイミングに依存していました。実行環境を意図的に制限するこのタイプの「機能」は、次の 2 つの間の適切なトレードオフを提供するために使用できます。忠実実行(つまり、エミュレートされたプログラムが適切に動作すること)と効率実行(つまり、最小限のトレースで効率的にエミュレートできるほど高レベルのプログラム表現を構築すること)

答え2

私の質問は、エミュレーターが実際の CPU とまったく同じオペコードをエミュレートする必要がある場合、CPU を正当に (またはまったく) エミュレートするには、ゲームの正しいバイナリ オペコード形式を解析する必要があるのではないでしょうか?

「解析」とは、「テキストを調べて、その意味を理解する」ことを意味します。テキストや言語のような構文は複雑です。個々の「単語」が意味を持つだけでなく、その位置や他の単語との距離に応じて意味が変わることがあるためです。

おそらく、CPU が命令ストリームに対して行う処理 (解析よりもはるかに単純) に関して適用するより正確な用語は「デコード」です。そして、エミュレーターも同様に命令を「デコード」する必要があります。ただし、x86 命令セットの複雑さを考えると、「解析」はそれほど悪い用語ではないと思います。

私のプログラムは、この命令が実際に意味することを確認する必要があります (例:「A レジスタに 1 を加算する」)。ただし、これを保証するために、テキスト ファイル内のすべての 0 と 1 をチェックする必要はないでしょうか。

CPU は実際には命令を検証しません。どの CPU にも、RAM 内のメモリ位置を指す内部レジスタがあります。CPU はそれを読み取り、必要に応じてさらに数バイト読み取り、実行を試みます。最新の CPU は、命令が不正な場合、「例外プロセス」を開始します。

古い 8 ビット 6502 などの古い CPU では、そのようなことすら起こりませんでした。不正な命令によって CPU がロックされ、ドキュメント化されていない奇妙な動作をする命令もありました。(検索してみると、x86 パンテオンのすべての CPU で、ドキュメント化されていない x86 命令が多数見つかるでしょう。たとえば、このCPUID命令は、Intel が公式にドキュメント化する前から、一部の 486 CPU に存在していました。)

優れた正確な CPU エミュレーターは、実際の CPU と同じように、命令ストリームを盲目的に処理します。ただし、エミュレーターをロックする代わりに、仮想 CPU が機能していないことを通知するダイアログ ボックスを表示するなど、システムのロックアップを引き起こす状況では異なる処理を実行する場合があります。

さらに、「テキスト ファイル内のすべての 0 と 1 をチェックする必要がある」とありますが、通常はエミュレータにテキスト ファイルを入力することはありません。CPU エミュレータにはエミュレートされたメモリが必要です。また、多くのプラットフォームではメモリ マップされた I/O が使用されているため、エミュレートされた I/O デバイスも必要です。ファームウェアなどをエミュレートするには、そのファームウェアのバイナリ イメージ (実際のファームウェアの合法的なコピーまたは代替のオープン ソース ファームウェア) が必要です。プラットフォーム全体 (x86 CPU がコンポーネントである「PC プラットフォーム」など) をエミュレートするには、CPU エミュレーション以上のものが必要です。

関連情報