原文: http://www.komkon.org/fms/EMUL8/HOWTO.html
翻訳: bero@geocities.co.jp

無許可の配布を禁止する。 コピーしないで、このドキュメントへリンクすること。

コンピュータエミュレータの書き方

Marat Fayzullin

あるコンピュータのエミュレータを書きたいが、どこから始めるべきか知らない人々から大量の電子メールを受け取った後にこのドキュメントを書いた。 次のテキストに含まれるすべての意見と助言は私の単独のものであり、絶対的な真実とうけとってはならない。 私は再コンパイル技術の経験があまりないので、ドキュメントは「コンパイル」に対立するものとして、いわゆる「インタープリタ」エミュレータを主としてカバーする。 それは、これらの技術についての情報を見つけることができる場所に対してポインタまたは2を持っている。

このドキュメントに何か見当たらない、または修正したい思う場合、自由に私に電子メールでコメントを送る。 しかし、flame、馬鹿、ROMイメージのリクエストには答えない。 このドキュメントの最後にあるいくつかの重要なftp/WWWアドレスは行方不明なので、ここのリンク切れを何か知っている場合、そのことを私に伝える。 このドキュメントの中にないすべてのFAQも同様である。


内容

さて、君はソフトウェアエミュレータを書くことを決めたかい? とてもいいね。じゃあ、このドキュメントは君の助けになるかもしれない。 これは、人々がエミュレータを書くことについて尋ねる少数の共通の技術的な質問をカバーする。 さらに、ある程度従うことができるエミュレータ内部の「青写真」を供給する。

何をエミュレートできるか。

基本的に、マイクロプロセッサを内側に持っている何でも。 もちろん、多かれ少なかれ柔軟なプログラムを実行するデバイスだけをエミュレートすることが面白い。 それらは次のものを含んでいる:

それが非常に複雑でも(例えばコモドールAmigaコンピュータのように)、どんなコンピュータシステムでもエミュレートできることに注意することが必要である。 しかし、そういったエミュレーションの性能は非常に低いことがある。


「エミュレーション」とは何か。また、「シミュレーション」とどう違うか。

エミュレーションはデバイスの内部設計を模倣する試みである。 シミュレーションはデバイスの機能を模倣する試みである。 例えば、パックマンアーケードハードウェアを模倣し、それ上で実際のパックマンROMを実行するプログラムは、エミュレータである。 コンピュータ用に書かれたパックマンゲームだが、実際のアーケードに似ているグラフィックを使用することは、シミュレータである。

所有しているハードウェアをエミュレートすることは合法か。

問題は「灰色」エリアの中であるが、それについての情報を不法な手段で得ていない限り、所有しているハードウェアをエミュレートするのは合法に見える。 さらに、著作権が取られている場合、エミュレータでシステムROM(BIOSなど)を配布することが違法であるということを知っておくべきである。

「インタープリタ・エミュレータ」とは何か。また、「再コンパイル・エミュレータ」とどう違うか。

エミュレータのために使用できる3つの基礎的な機構がある。 最良の結果のためにそれらを組み合わせることができる。


エミュレータを書きたい。どこから始める必要があるか。

エミュレータを書くためには、コンピュータプログラミングとデジタルエレクトロニクスについてのよい一般的な知識を持っていなければならない。 アセンブラプログラミングの経験もまた非常に手軽である。

  1. 使用するプログラミング言語を選択する。
  2. エミュレートされたハードウェアに関する利用可能な情報をすべて見つける。
  3. CPUエミュレーションを書く。または、CPUエミュレーションのために既存のコードを入手する。
  4. ハードウェアの残りをエミュレートするためにいくつかの草案のコードを少なくとも部分的に書く。
  5. この時点では、エミュレーションを止めてプログラムが何を行っているか調べることを可能にする、小さな内蔵のデバッガを書くことが有用である。 さらに、エミュレートされたシステムのアセンブリ言語の逆アセンブラを必要とすることがある。 どれも存在しない場合、あなたのものを書く。
  6. エミュレータ上でプログラムを実行してみる。
  7. 逆アセンブラとデバッガを使用してプログラムがどうやってハードウェアを使用するか調べ、コードを適切に修正する。

どのプログラミング言語を使用する必要があるか。

最多の明白な代案はCとアセンブラである。 ここに、それらの各々の賛成と反対がある:

エミュレータは全く複雑なプロジェクトであり、できるだけ速く動作するためにコードを最適化する必要があるので、選択した言語についてのよい知識は動作するエミュレータを書くための絶対に必要である。 コンピュータ・エミュレーションは明確にプログラミング言語を学習するプロジェクトの1つではない。


どこでエミュレートされたハードウェアについての情報を入手するか。

下記は見たいかもしれない場所のリストである。

ニュースグループ

FTP

[#] フィンランドのOuluの コンソールとゲームプログラミング サイト
[#] ftp.spies.comの アーケードビデオゲームハードウェア アーカイブ
[#] KOMKONの コンピュータ史とエミュレーション アーカイブ

WWW

[#] comp.emulators.misc FAQ
[#] 私のホームページ
[#] アーケードエミュレーションプログラミングレポジトリ
[#] エミュレーションプログラマのリソース

どうやってCPUをエミュレートするか。

最初に、標準のZ80または6502のCPUを単にエミュレートする必要があれば、 私が書いたCPUエミュレータ のうちの1つを使用することができる。 しかし、それらの使用に関してある条件がある。

自分のCPUエミュレーションコアを書きたい人や、どうやって作動するか知ることに興味のある人のために、Cの典型的なCPUエミュレータの骨格を下に提供する。 実際のエミュレータでは、いくつかの部分をスキップしたり、いくつかの他のものを独自に加えたいかもしれない。

Counter=InterruptPeriod;
PC=InitialPC;
for(;;)
{
  OpCode=Memory[PC++];
  Counter-=Cycles[OpCode];
  switch(OpCode)
  {
    case OpCode1:
    case OpCode2:
    ...
  }
  if(Counter<=0)
  {
    /* Check for interrupts and do other */
    /* cyclic tasks here                 */
    ...
    Counter+=InterruptPeriod;
    if(ExitRequired) break;
  }
}
最初に、CPUサイクルカウンタ(Counter)とプログラムカウンタ(PC)に対する初期値を割り当てる:
Counter=InterruptPeriod;
PC=InitialPC;
Counterは、次に疑われた割り込みに残された、CPUサイクルの数を含んでいる。 このカウンタが終了する時、必ずしも割り込みが生じるわけではないことに注意する:タイマの同期やスクリーン上のスキャンラインの更新のような他の多くの目的のためにそれを使用することができる。 後でもっと詳しく説明する。 PCは、エミュレートされたCPUがその次のオペコードを読むメモリアドレスを含んでいる。

初期値を割り当てた後、メインループを始める:

for(;;)
{
このループを次のようにも実装できることに注意する。
while(CPUIsRunning)
{
ここで、CPUIsRunningはブールの変数である。 CPUIsRunning=0のセットによりいつでもループを終了できるように、これはある長所を持つ。 不運にも、すべてのパスでこの変数をチェックすることは全く多くのCPU時間をとり、可能な場合、回避されるべきである。 さらに、このループを次のように実装しない。
while(1)
{
なぜなら、この場合、いくつかのコンパイラは1が真か偽かチェックするコードを生成するからである。 確かにコンパイラにループのすべてのパスのこの不必要な仕事を行ってほしくない。

さて、ループの中にいる場合、第1のものは次のオペコードを読み、プログラムカウンタを変更することである:

OpCode=Memory[PC++];
これがエミュレートされたメモリから読む最も単純で最も速い方法である一方、必ずしも実現可能だとは限らないことに注意する。 メモリにアクセスするより普遍的な方法 は、このドキュメントでその後カバーされる。

オペコードが取って来られた後、このオペコードのために必要になった多くのサイクルまでにCPUサイクルカウンタを減少させる:

Counter-=Cycles[OpCode];
Cycles[]テーブルは、各オペコードのためのCPUサイクルの数を含んでいるべきである。 あるオペコード(条件付きのジャンプやサブルーチン呼び出しのような)が、それらの引数に依存して、異なるサイクル数をとることがあることに注意する。 しかし、これはコード中でその後調節できる。

今、オペコードを解釈し、実行する時が来た:

switch(OpCode)
{
switch()の構築はif() ...else if()ステートメントの連続へコンパイルされるので非能率的である、というのは共通の誤解である。 少数のcaseでの構築ではこれが真実である一方、大きな構築(100-200以上のcase)は常ジャンプテーブル(それはそれらを全く効率的にする)へコンパイルするように見える。

オペコードを解釈する2つの代案の方法がある。 1番目は、関数のテーブルを作り、適切なものを呼ぶことである。 関数呼び出しのオーバーヘッドがあるので、この方法はswitch()ほど効率的でなく見える。 第2の方法は、ラベルのテーブルを作り、gotoステートメントを使用することである。 この方法はswitch()よりわずかに速い一方、それは「あらかじめ計算されたラベル」をサポートするコンパイラだけで動作する。 他のコンパイラは、ラベル・アドレスの配列を作ることができない。

オペコードの解釈と実行に成功した後、何か割り込みが必要かどうかチェックする時が来る。 この瞬間では、さらにシステムクロックと同時になる必要のあるすべてのタスクを実行できる:

if(Counter<=0)
{  
  /* Check for interrupts and do other hardware emulation here */
  ...
  Counter+=InterruptPeriod;
  if(ExitRequired) break;
}
これらの 周期的なタスク は、このドキュメントでその後カバーされる。

単にCounter=InterruptPeriodを割り当てないで、Counter+=InterruptPeriodを行うことに注意する:Counterの中に負のサイクル数があることがあるので、これはサイクルを数えるのをより正確にする。

さらに、

if(ExitRequired) break;
の行を見る。 ループのすべてのパスで終了をチェックするのは高価すぎるので、カウンタが終了する場合に限り、チェックを行う:ExitRequired=1をセットした時、これはなおエミュレーションを終了するだろう、しかし、それは多くのCPU時間はかからない。

どうやってエミュレートされたメモリへのアクセスを扱うか。

エミュレートされたメモリにアクセスする最も単純な方法は、バイト(ワード、等)の平易な配列としてそれを扱うことである。 その場合、メモリへのアクセスは平凡である:
  Data=Memory[Address1]; /* Read from Address1 */
  Memory[Address2]=Data; /* Write to Address2  */
しかし、そういった単純なメモリアクセスは、次の理由のため必ずしも可能だとは限らない: これらの問題に対処するために、2、3の関数を導入する:
  Data=ReadMemory(Address1);  /* Read from Address1 */
  WriteMemory(Address2,Data); /* Write to Address2  */
ページアクセス、ミラー、I/O取り扱いなどような特別の処理はすべて、これらの関数の内部で行われる。

ReadMemory()とWriteMemory()は非常に頻繁に呼ばれるので、エミュレーションで通常多くのオーバーヘッドになる。 したがって、それらはできるだけ効率的に作らなければならない。 ここに、ページがつけられたアドレス空間にアクセスするために書かれた関数の例がある:

static inline byte ReadMemory(register word Address)
{
  return(MemoryPage[Address>>13][Address&0x1FFF]);
}
static inline void WriteMemory(register word Address,register byte Value)
{
  MemoryPage[Address>>13][Address&0x1FFF]=Value;
}
inlineのキーワードに気づく。 それは、関数への呼び出しを行う代わりに、コードへ関数を埋め込むようにコンパイラに命じる。 コンパイラがinlineや_inlineをサポートしない場合、関数をstaticにしてみる:いくつかのコンパイラ(例えばWatcomC)は、それらをインライン展開することにより、短いstatic関数を最適化する。

さらに、ほとんどの場合、ReadMemory()はWriteMemory()より数倍頻繁に呼ばれることを覚えておく。 したがって、ReadMemory()をできるだけ短く単純にして、WriteMemory()中でほとんどのコードを実装することは価値がある。


周期的なタスク:それらは何か。

周期的なタスクは、次のようにエミュレートされたマシンで周期的に生じるべきものである:

そういったタスクをエミュレートするために、それらを適切なCPUサイクルの数に結び付けるべきである。 例えば、CPUが2.5MHzで動作することになっており、ディスプレイが50Hzのリフレッシュ周波数(PALビデオの標準)を使用する場合、VBlank割り込みは次のサイクル毎に生じなければならないだろう。

       2500000/50 = 50000 CPU cycles
さて、全スクリーン(VBlankを含んで)が256のスキャンラインの高さであり、そのうちの212がディスプレイで実際に表示される(つまり、他の44がVBlankに陥る)、と仮定すれば、エミュレーションは次のサイクル毎にスキャンラインを各々リフレッシュする必要があるということが得られる。
       50000/256 ~= 195 CPU cyles
その後、VBlank割り込みを生成するべきであり、その後、VBlankをやめるまで何も行ってはならない。つまり
       (256-212)*50000/256 = 44*50000/256 ~= 8594 CPU cycles
各タスクのために必要とされるCPUサイクルの数を注意深く計算する。次に、割り込み周期のために最も小さな数を使用し、それに他のすべてのタスクを結び付ける(それらは、カウンタのすべての終了上で必ずしも実行してはならない)。

どうやってCコードを最適化するか。

最初に、多くの付加的なコード性能は、コンパイラの正しい最適化オプションを選ぶことによって達成できる。 私の経験では、フラグの次の組み合わせは最良の実行速度を与えるだろう:
Watcom C++      -oneatx -zp4 -5r -fp3
GNU C++         -O3 -fomit-frame-pointer
Borland C++
これらのコンパイラ、または違うコンパイラの1つに対してオプションのよりよい設定を見つければ、私にそのことを知らせる。 Cコード自身の最適化は、コンパイラオプションの選択よりわずかに巧妙であり、一般にコードをコンパイルするCPUに依存する。 いくつかの一般的な規則は、すべてのCPUに当てはまる傾向がある。 しかし、やり方によって変わるので、それらを絶対的な真実とうけとってはならない:
(C)1997-1998 Copyright by Marat Fayzullin [fms@cs.umd.edu]