SL9821

はじめに

ダウンロード

使い方

FAQ

実行画面

技術的なはなし

プログラム構成

CPU

テキスト

グラフィック

PEGC

サウンド

CPUエミュレーション

CPUエミュレーションを行っている機能ブロックの概略図を以下に示します。

CPUエミュレーションの処理の中核はクロック動作サイクルから呼ばれるCPU処理サイクルになります。
CPU処理サイクルでは以下の処理を行います。

1. CPUに起因する割り込みが発生しているかチェックし、発生していた場合例外発生時の処理を行い終了。
2. ハードウェアに起因する割り込みが発生しているかチェックし、発生していた場合例外発生時の処理を行い終了。
3. 処理したインストラクションの論理クロック数を積算した総クロック数が、所定のクロックに達するか、CPUまたはハードウェアに起因する割り込みが発生するまでインストラクション処理を繰り返し実行。
1.または2.の条件に当てはまる場合はそれ以降の処理を行わずに関数を抜けます。なお、例外発生時の処理というのはリアルモードの場合、現在のフラグレジスタとCS:IPをスタックに待避した後割り込みベクタを参照してCS:IPを変更、プロテクトモードの場合は変更後のセグメントディスクリプタの内容に応じ若干複雑な処理を要求されます。
ここで言うCPUに起因する割り込みとは、CPUの命令のint3/inti/intoのほか、除算エラーや一般保護例外、ページングエラーなどのような例外も含みます。

●関連ソース
フォルダi386下のファイル、メイン処理はi386e.c

インストラクション処理

CPU処理サイクルではプリフィックス以外のコードを検出するまでインストラクションフェッチの読み出しを行い、コードに応じた処理関数をジャンプテーブルを用いて呼び出します。
呼び出した先の関数では必要に応じ引き続きオペランドのフェッチを行い、処理に必要なフェッチ処理がすべて済んでからインストラクションに応じた処理を行います。
このとき、ソースオペランドがメモリの場合はメモリリード処理、ディスティネーションオペランドがメモリの場合はリードとライト処理が行われます。また、in/out命令の場合はI/Oアクセスが発生します。

インストラクション処理は実行中に割り込みが発生しない限り、インストラクションごとに設定した論理クロックの積算が指定のクロック数に達するまで繰り返して実施します。これは、単純に一命令だけ処理をして関数を抜けるとパフォーマンスが落ちるからで、もっぱらパフォーマンスの向上を目的としたものです。そのため、ループを抜けるための閾値のクロック数を大きくすることでパフォーマンスが向上する傾向にはありますが、あまり大きくするとクロック同期サイクルとの連携がうまくいかなくなり正常に動作しないことがあるためほどほどの値にする必要があります(0.2.5.0では128クロックで、これより大きくしてもそれほどパフォーマンスは向上しません)。

●関連ソース
i386e.c関数i386e_instructionProc、i386e_op_c_cpu.c

メモリアクセス

フェッチ処理を含むメモリアクセスにはCPUモード(リアルモード、プロテクトモード、仮想86モード)とページングの有無の組み合わせで6通りのメモリアクセス方法があります。またメモリに対する処理としては8ビット、16ビット、32ビットのビット幅で、リード、ライト、アップデート(メモリをリードしたあと演算を行い同じアドレスにライトする場合に使用します)の組み合わせで9通りのパターンがあります。
実装では、9つのメモリ制御用の関数にセグメントレジスタを更新するときの処理関数を加えた10の関数をテーブルにしたものをCPUの動作モードごとに用意し、メモリアクセスはこのテーブルを介して行うようにしています。どのテーブルを参照するかの決定はCPUのモードが切り替わるタイミングで行います。
なお実際には、ページングありのリアルモードは用意していないためCPUのモードとしては5種類、また、ページングなしのときのリアルモードと仮想86モードでは同じ処理関数を使用しています。
アップデート関数はインストラクション別関数においてディスティネーションがメモリオペランドの場合(例えばadd ds:[bx], axのようなケース)に使用します。使用にあたっては引数としてソースオペランドと演算用の関数を渡し、アップデート関数内部では、メモリリード、演算用関数の実行、メモリライトをまとめて行います。この関数の意図するところはCPUモードでページングモードありの場合、リニアアドレスから物理アドレスへの変換処理を挟まないといけなくなり速度低下の原因となってしまうため、それを避けるための実装になります。

リニアアドレスから物理アドレスへの変換に関して補足すると、ページングはリニアアドレスと物理アドレスの関係をリニアアドレスの最上位の10ビット毎、1024にグループ化された領域ページディレクトリ、続く10ビット毎にグループ化したものをページテーブルとして管理します。ページディレクトリはその配下の1024エントリのページテーブルの物理アドレスを指し、ページテーブルでは、残った12ビット毎(4096バイト)の物理アドレスを指します。ページングテーブルの関係を図示すると以下のような感じになります(参考:CQ出版「X86プロテクトモードプログラミング」)。

リニアアドレスから物理アドレスへの変換は図のように2回のテーブルエントリの参照を経る必要があります。ページングを有効にしているときにメモリアクセスが発生する度にこのテーブル参照を毎回しているとかなりの負担になるため、実装ではセグメント毎に最後に実施したリニアアドレスから物理アドレスへ変換した情報を保持しておき、新たにアクセスするリニアアドレスと前回アクセスしたリニアアドレスの上位20ビットが同一の場合は物理アドレスは前回の値を流用、上位10ビットのみが同一の場合はページテーブルのみ参照し直す、というようなことを行って処理を軽くしています。ここで気になるのは、ページングテーブルが知らない間に書き換えられて保持していた情報と変わってしまう場合なのですが、動いているプログラムを見る限りページングの更新後コントロールレジスタのcr3を必ず書き換えているようなので、そのタイミングで保持している物理アドレスをフラッシュすることで今のところ問題は起きていないようです(実装として正しいかは微妙なところなのですが、実行速度を考えるとこのような実装を入れないと厳しいところです)。

また、16ビット、32ビットのアクセスは、ページングの境界やエミュレータで管理しているメモリブロックの境界をまたいでアクセスするケースを考慮しなければならず、若干ごちゃごちゃした実装になっています。

●関連ソース
i386e_memaccess.c

I/Oアクセス

I/Oアクセスはin/out命令、ins/outs命令が実行されたとき行われます。
I/Oアクセスについては特に記述することはないのですが、ちょっとしたメモとして以下のことを記録しておきます。
仮想86モードを含むプロテクトモードでI/Oアクセスが発生すると、特権レベル毎に用意されたTSS(Task State Segment)で管理されるI/O保護機構により指定されたポートへのI/Oアクセスはブロックされます(そのときは一般保護例外が発生)。
IDEやSCSIの代替BIOSを作成していたとき、リアルモードでは動作するのにデバイスドライバにemm386.exeを入れ仮想86モードで動作させると動作しなくなる問題が発生しました。原因を追っていたところ、Cで実装した代替処理の中で直接I/O処理をしていたのが問題そうだと言うことが分かってきました。一部のI/Oアクセスはユーザモードでは実行させずに一般保護例外を発生させ、最高特権で改めてI/Oアクセスをするというようなことをしているようです。
現在のIDE/SCSIの代替BIOSはエミュレータ上で動くアセンブラプログラムとCで実装したコードを行ったり来たりする非常に見づらいコードになっていますが、これはメモリアクセスとI/Oアクセスに関してエミュレータ上で必要に応じ一般保護例外が発生できるように対処しているものだからです。
また、Windows95/98のように16ビットコードが動くマルチタスクOSなどは、リアルモードを想定したプログラム中でI/Oアクセスがあった場合に一般保護例外を発生させたあと内部で管理しているデータを元にI/Oアクセスしたように見せかける、I/O処理の抽象化のようなことも行っているようです。

●関連ソース
i386e_op_c_move.c、fakebiosDrive.c