エミュレータの作り方

  • (by K, 2020.06.27)

(0)

  • 仮に、ES-VMのバイトコードから任意のCPU/OSに向けて、自由にコンパイルができるようになったとします。
  • これができるようになったら、あとはx86のコードをES-VMのバイトコードに変換するルーチンを書けば、x86のコードはすべての環境で動くという世界が完成します。
  • ・・・ここではx86を例に出しましたが、ARMやPowerPCとかでも同様です。
  • そこで、ここでは、x86のコードをES-VMに変換する際のアイデアをいくつか書いておきます。

(1) エンディアンの問題

  • x86は全面的にリトルエンディアンを前提とした仕様になっています。そしてx86向けに書かれたプログラムの中には、このリトルエンディアンを前提に書かれているものも少なくありません。
  • 一方で、ES-VMはエンディアンに依存したプログラムを許容しません。そもそもそういう仕様のプログラムを作れなくしています。ES-VMでは、内部表現に依存したプログラムを作ることができないように、慎重に設計されているためです。
  • ということで、x86のプログラムをどうにかしてエンディアンに依存しない形式に変換しなければいけないわけです。

  • 私の考えているアルゴリズムはこうです。
  • まずメモリ全体を32bit-intの配列として表現します。
  • そしてプログラムがメモリアクセスをする際には、メモリアドレスが4の倍数になっているかを逐一確認して、もしその通りであればそのまま対応する配列の要素にアクセスします。もし二つの配列をまたがるアクセスであれば、双方にアクセスしてシフトしたりANDしたりORしたりして、結果を再現します。また16bitや8bitのアクセスの場合も、32bitアクセスの上で、ビットシフトしたりAND/ORして結果を再現します。
    • これはかなり遅くなるやり方ですが、結局、このやり方が一番無難だと思いました。仕方ないです。
    • 何らかの方法で、「この個所に限ってはこのチェックを省略できる」みたいなことが判定できるようになれば、ある程度の高速化が可能になると思われます。

  • それにしても、どうしてこんなひどい変換をしなければいけないのかと思うかもしれません。・・・でも、冷静になって考えればこの仕様は当然の結果というか、妥当なものなのです。
  • そもそもCPUは、メモリに対しては基本的にいつも32bitでアクセスをしています(これはi386とかの場合であって、最近のCPUはまたいろいろ事情があると思われます)。バイトアクセスをしても32bitのアクセスが発行されます。そしてその上で、まさに上記で書いているような処理をしているのです。だからこの変換はx86というCPUがハードウェアで暗黙のうちにやっていたことを(ほかのどのCPUでも再現できるように)ソフトウェアで明示したにすぎません。
  • そして多くのプログラムはその機能をあてにして作られてしまったのです。
  • この点をもっとはっきりさせるために、次のような思考実験をします。
  • もしインテルが、「メモリアクセスは32bitアラインされていなければいけないし、さらに32bit以外のメモリアクセスは許さない」という仕様のx86を作っていたとしましょう。そしてこれを守らなければ例外でプログラムが停止するような仕様だったとしましょう。こんな仕様ならCPUはかなりシンプルになるでしょう。なぜなら上記の変換をハードウェアでやらなくてよくなるからです。そしてプログラムのほうも停止されたらたまらないので、このルールを徹底して守るようになります。
  • もしそうなっていたら、ES-VMが変換する場合も、アドレスが4の倍数になっているかどうかとか、32bit以外のアクセスが来たらどうしようとか、そんな本質じゃないところで悩む必要はなかったですし、速度が落ちる心配もしなくて済んだはずなのです。
  • ということで、x86のハードウェアが相応に過剰だったから、技術負債付きのソフトウェア資産がどんどんたまって、それをES-VMが救済することになったので、こんな変換になったわけです。まあx86だって8086との互換性とかを気にして、苦労してこの仕様にしたと思うので、結局はまたしても「互換性」が元凶なのでしょう。

(2) 自己書き換えプログラムの問題

  • x86のプログラムの一部は、プログラム内で動的に生成したバイト列を命令列として解釈させるような挙動を含んでいます。私の作ったES-BASICだって、JITコンパイル部ではまさにそういうことをしています。
  • これは高性能なプログラムを作るには必須の手法だと思うので、これ自体を批判する気はないのですが、今はいつでも自由にこれができてしまうので、ES-VMへの変換ということを考えると頭痛の種です。・・・たとえば、メモリ上に置いたバイト列を命令列として固定するシステムコールや、逆に命令列をバイト列に戻して自由に読み書きできるように戻すシステムコールとかがもしあれば、そしてこれらの手続きを経ずにバイト列に分岐したりしたら例外で強制終了する、みたいな仕様だったら、話はずっと簡単だったでしょう。なぜならそのタイミングで、x86の機械語列をES-VMのバイトコードに変換すれば済むからです。

  • ということで、ES-VMに変換済みのコードが書き換えられたかどうかを検知する仕組みが必要になります。検知したら、そこに分岐したタイミングで、書き換えられた新しいx86機械語列に対してES-VMのバイトコードを生成する必要があります。
  • これは、例えばこうします。メモリを4KBとか、32バイトとか、まあ単位は適当に決めなきゃいけないと思うのですが、メモリをそれくらいの粒度で切り分けて「その中にコンパイル済みの機械語列を含んでいるかどうか」を1ビットの配列で管理します。これでメモリのライトアクセスのたびに、このビット配列にアクセスして、ヒットすれば自己書き換えしうるメモリライト命令なので、専用ルーチンに入って時間をかけてチェックさせます。ヒットしなければ何もせずに続行します。
  • 一方、たとえば関数を呼び出す際には、呼び出し先のコンパイル結果がまだ有効かどうかをチェックするための命令を入れておきます。これは自己書き換えさえなければ数命令でチェックが完了します。だからオーバヘッドは小さいです。

こめんと欄


コメントお名前NameLink

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2020-06-27 (土) 15:41:47 (5d)