今回開発する2試計算機は、Windowsマシン上に一つのプログラムとして仮想計算機を構築し、その上でさまざまなソフトウエアを動かす、というものです。仮想マシンのためのソフトウエアは独自のコードで記述し、仮想マシンのメモリシステムに配置して仮想マシンの仮想CPUで逐次実行するようにしましょう。ほかに、画面表示を行うビデオシステムやキーボード・マウスの入力を受け付ける仕組みも必要ですね。
ただ、今回は実用的なアプリケーションを稼動させる本格的な仮想マシンというよりは、いろいろいじって「遊ぶ」ための、また開発や実験を通してコンピュータや基本的なプログラムの概念を実践的に学ぶための教材的な仮想マシンを作ってみることにします。そのため、コードの体系は32ビットベースでハンドアセンブルが容易にできる簡易なものとするほか、他のシステムも効率や安定性・安全性よりわかりやすさ・いじりやすさを最優先で開発していくことになるでしょう。
まだ試験的な設計段階ですが、全体の構造としては、以下のような感じになると思います。
仮想マシン | |
メモリシステム | 仮想CPU |
ビデオシステム | |
入力システム |
仮想マシンは各コンポーネントをまとめて管理し、その仮想マシンにメモリシステムが仮想マシン内外のアクセス手段となるメモリ空間・実装メモリを提供します。仮想マシン本体や仮想CPUはメモリ空間を通じてビデオシステムや入出力機能を操作し、I/O機能も文字通りの「メモリマップド」I/Oで実現する予定です。たとえば、ビデオシステムなら仮想マシンが割り当てた特定アドレスのメモリにアクセスして画面モードを取得・設定し、メモリ上の特定領域がフレームバッファになります。メモリ上のバイトオーダーは、リトルエンディアンを基本にしましょう。
仮想マシンのプログラムは、仮想マシンクラスを中心とするクラスの集合として作成します。構造としては、仮想マシンクラスの内部にクラス化した他のコンポーネントを含むような形で、この仮想マシンを外部のメインプログラムで駆動・描画するようにする予定です。開発環境はWin32 APIベースのC++としますが、仮想マシン本体はなるべくAPIを使用しないで書くつもりなので、うまく行けばLinuxなどにもほぼそのまま移植できるかもしれません(メインプログラムは個別に開発する必要がありますが)。
今回はまず、コードを配置するメモリシステムとコードを実行する仮想CPUの基本的な部分を実装し、それらを格納するための仮想マシンを作ってみましょう。
メモリシステムは、内部にメモリ用のバッファを持ちそのバッファにアクセスするインターフェースを公開します。今回は、読み書きそれぞれ8/16/32ビット単位でアクセスできるようにしましょう。
class CMemory { public: CMemory(UI32); UI8 read8(UI32) const; UI16 read16(UI32) const; UI32 read32(UI32) const; bool write8(UI32,UI8); bool write16(UI32,UI16); bool write32(UI32,UI32); ~CMemory(); private: I32 i32MemSize; UI32 u32LastAddress; BUF bfMemory; bool clear(); };
このメモリシステムは、仮想マシンの起動時に仮想マシンがバッファのサイズを指定して生成し、以降は仮想マシンが管理します。メモリを読み書きする仮想CPUも、仮想マシンによって生成される時に仮想マシンからメモリシステムのポインタを取得するようにしましょう。なお、宣言中でUI/I32などとあるのは整数の型です。UIが符号なし、Iが符号付で後ろの数字がビット数になっています。
メモリシステムのコンストラクタでは、仮想マシンから渡されたサイズでバッファbfMemoryを確保し、その最後のアドレスをu32LastAddressに保存しておきます。これでbfMemory[0]からbfMemory[u32LastAddress]までのu32Argバイトのバッファを仮想マシンのメモリとして利用できるようになります。
CMemory::CMemory(UI32 u32Arg) { bfMemory=(BUF)new UI8[u32Arg]; for (int i=0;i<u32Arg;i++) bfMemory[i]=0; u32LastAddress=u32Arg-1; }
バッファが用意できれば、読み書きはポインタbfMemoryに指定された値を足してその領域を読み書きするだけですね。ただ、領域外のアドレスの場合は例外out_of_rangeを投げるようにしましょう。これで、たとえば8ビット読み込みを行うread8()は以下のようになります。
UI8 CMemory::read8(UI32 u32Arg) const { if (u32Arg>u32LastAddress) throw out_of_range("無効なメモリアドレス指定"); return ((PUI8)bfMemory)[u32Arg]; }
仮想CPUが実行するコードは、とりあえず32ビットのうち最上位8ビットをコード体系、次の8ビットを実際のコード、残り16ビットをパラメータとします。16ビットコードでも良い気もしますが、今後拡張してパラメータの配置を変えるかもしれないので、まあ32ビットで良いでしょう。仮想CPU内部には16本のレジスタがあり、このうち8本を汎用レジスタ、3本をシステム用レジスタ(プログラムカウンタPC、システムスタックポインタレジスタSP、ステータスレジスタSR)として使用します。
仮想マシンと仮想
仮想CPUクラスのインターフェースは、とりあえず以下のようにしてみました。
class CVCPU { // 仮想CPUインターフェースクラス public: virtual bool exec()=0; virtual UI32 getRegister(I32) const=0; virtual UI32 getPC() const=0; virtual UI32 getSP() const=0; virtual bool getSRZero() const=0; virtual bool getSRSig() const=0; virtual bool getSROF() const=0; virtual bool setPC()=0; virtual ~CVCPU() {} };
exec()は、コードを一つ取り込んで実行する関数、残りはレジスタの値や状態を取得する関数です。実際の仮想CPUクラスはこのインターフェースクラスを継承し、exec()に実際のコード実行処理を実装していくことになります。メモリや仮想マシンとの接続は、コンストラクタにポインタを渡して行うようにしましょう。
今回は、動作テストのためexec()に以下のような代入系のコードを実装してみました。
コード値 | ニーモック | マクロ値 | コードサイズ |
00 | ld32 r1,r2 | LDRR32 | 4 (0000r1r2) |
---|---|---|---|
01 | ld8 r1,Imm8 | LDRI8 | 4 (0001r1Imm8) |
02 | ld16 r1,Imm16 | LDRI16 | 6 (0002r100,Imm16) |
03 | ld32 r1,Imm32 | LDRI32 | 8 (0003r100,Imm32) |
04 | ld8 r1,(r2) | LDRM8 | 4 (0004r1r2) |
05 | ld16 r1,(r2) | LDRM16 | 4 (0005r1r2) |
06 | ld32 r1,(r2) | LDRM32 | 4 (0006r1r2) |
07 | ld8 (r1),r2 | LDMR8 | 4 (0007r1r2) |
08 | ld16 (r1),r2 | LDMR16 | 4 (0008r1r2) |
09 | ld32 (r1),r2 | LDMR32 | 4 (0009r1r2) |
0a | ld8 (r1),Imm8 | LDMI8 | 4 (000ar1Imm8) |
0b | ld16 (r1),Imm16 | LDMI16 | 6 (000br100,Imm16) |
0c | ld16 (r1),Imm32 | LDMI32 | 8 (000cr100,Imm32) |
0f | exc r1,r2 | EXC | 4 (000fr1r2) |
上の表でr1/r2とあるのはレジスタ、()はカッコ内のレジスタがポイントするアドレス(たとえば、(r1)はr1レジスタの内容が指し示すメモリアドレス)、Immは即値で後ろの数字がビット数です。たとえば、Imm8は「8ビットの数値」を意味します。代入コードldは8/16/32ビット単位で操作を行い、操作対象のビット数は後ろの数字で指定するようにしました。ただし、レジスタに対する代入では8ビットの代入でも32ビットにゼロ拡張され、レジスタの全ビットに対する代入になります。
代入は、オペラントの右から左に行われ、たとえば
ld32 r1,r2
では、r2レジスタの内容がr1レジスタに代入されます。
実際のコードのバイナリ列は、コード32ビットのうち最上位8ビットが0、次の8ビットがコードの値、そして下位16ビットが対象レジスタや代入する即値などのパラメータです(パラメータがなければ0)。また、この他16・32ビットの代入では、コードの後に即値やアドレスをとる場合もあります。レジスタの指定は8ビットで行い、コードの中に収まらない即値はすべてコードの後に配置するようにしました。
今回定義する仮想CPUクラス・CCpuは、以下のようになっています。
class CCpu:public CVCPU { public: CCpu(CMemory *,UI32); bool reset(); virtual bool exec() throw(logic_error); virtual UI32 getRegister(I32) const; virtual UI32 getPC() const; virtual UI32 getSP() const; virtual UI32 getSR() const; virtual bool getSRZero() const; virtual bool getSRSig() const; virtual bool getSROF() const; virtual bool setRegister(I32,UI32); virtual bool setPC(UI32); virtual bool setSP(UI32); virtual bool setSR(UI32); virtual ~CCpu(); private: UI32 u32Reg[16]; UI32 GR_LAST_INDEX,REG_SP,REG_SR,REG_PC,SR_ZERO,SR_SIG,SR_OF; CMemory *pmMemory; bool exec(UI32); };
レジスタは要素数16のUI32型配列u32Reg[]で確保し、汎用レジスタの最終インデックス(今回は7)をGR_LASR_INDEXに定義しておきましょう。また、仮想マシンから渡されたメモリシステムのポインタはpmMemoryに格納しておきます。関数の方は、exec()とレジスタの設定・取得関数くらいですね。
まず、コンストラクタでは仮想マシンから渡されたメモリシステムのポインタを保存し、各種の初期化を行ってからプログラムカウンタを仮想マシンから渡されたu32Argに設定します。初期化関数reset()は、すべて(汎用+システムレジスタ)レジスタを0クリアするものです。また、REG_SPはスタックポインタ、REG_SRはステータス、REG_PCはプログラムカウンタの各システムレジスタのu32Reg[]内のインデックス定義で、SR_*の定数はステータスレジスタの各ビットの意味の定義ですが、今回は使いません。
CCpu::CCpu(CMemory *pmArg,UI32 u32Arg) { pmMemory=pmArg; GR_LAST_INDEX=7; REG_SP=0x0d; REG_SR=0x0e; REG_PC=0x0f; SR_ZERO=0x00000001; SR_SIG= 0x00000002; SR_OF= 0x00000004; reset(); setPC(u32Arg); }
汎用レジスタやシステムレジスタには、以下のような値の設定・取得用アクセス関数を用意しました。汎用レジスタ用のアクセス関数では、範囲外のレジスタにアクセスしようとすると例外(out_of_range)が発生します。
UI32 CCpu::getRegister(I32 i32Arg) const { if (i32Arg<0 || i32Arg>GR_LAST_INDEX) throw out_of_range("無効なレジスタ指定"); return u32Reg[i32Arg]; } bool CCpu::setRegister(I32 i32Arg,UI32 u32Arg) { if (i32Arg<0 || i32Arg>GR_LAST_INDEX) throw out_of_range("無効なレジスタ指定"); u32Reg[i32Arg]=u32Arg; return true; } UI32 CCpu::getPC() const { return u32Reg[REG_PC]; } UI32 CCpu::getSP() const { return u32Reg[REG_SP]; } UI32 CCpu::getSR() const { return u32Reg[REG_SR]; } bool CCpu::setPC(UI32 u32Arg) { u32Reg[REG_PC]=u32Arg; return true; } bool CCpu::setSP(UI32 u32Arg) { u32Reg[REG_SP]=u32Arg; return true; } bool CCpu::setSR(UI32 u32Arg) { u32Reg[REG_SR]=u32Arg; return true; }
仮想マシンからコード実行のメッセージを受け取る公開関数exec()は、以下のように次に実行すべきコードのアドレスが格納されたPCの値を引数に、オーバーロードされたprivateなコード実行関数exec()を呼び出します。
bool CCpu::exec() throw(logic_error) { exec(getPC()); return true; }
コードを実行するexec()では、最初に引数で指定されたアドレスから4バイトのコードを取得します。
UI32 u32Code=pmMemory->read32(u32Arg);
続いて、32ビットコードu32Codeのコード部分(上位16ビット)で各コードを判別するswitch文で、各コードの処理を行います。
switch ((u32Code & 0xffff0000) >> 16) { case LDRR: u32Wrk1=(u32Code & 0xff00) >> 8; u32Wrk2=u32Code & 0x00ff; setRegister(u32Wrk1,getRegister(u32Wrk2)); u32Addr=u32Arg+4; break; ・・・・・ case LDMI32: u32Wrk1=(u32Code & 0xff00) >> 8; u32Wrk2=pmMemory->read32(u32Arg+4); pmMemory->write32(getRegister(u32Wrk1),u32Wrk2); u32Addr=u32Arg+8; break; case EXC: u32Wrk1=getRegister((u32Code & 0xff00) >> 8); setRegister((u32Code & 0xff00) >> 8,getRegister(u32Code & 0xff)); setRegister(u32Code & 0xff,u32Wrk1); u32Addr=u32Arg+4; break; default: throw invalid_argument("無効なコード"); }
今回は、代入系とレジスタの値交換(EXEC)だけなので、実装も単にメモリシステムpmMemoryやレジスタとの間で値を読み書きする程度ですね。ただ、16ビットや32ビットの即値を取る時にはコードサイズが変わってきますので、コードごとに「コード終了後に設定すべきPC」の値をu32Addrに設定しておきましょう。このu32Addrの値は、たとえば8バイトのサイズを持つコードLDRI32(レジスタに32ビットの即値を代入)では、以下のようにコードの先頭アドレス+8に設定されます。
case LDRI32: u32Wrk1=(u32Code & 0xff00) >> 8; u32Wrk2=pmMemory->read32(u32Arg+4); setRegister(u32Wrk1,u32Wrk2); u32Addr=u32Arg+8; break;
コードの実行が終わったら、PCにu32Addrを設定して関数を終了します。
setPC(u32Addr); return true;
以上でメモリシステムと仮想CPUができたので、次にこれらをまとめる仮想マシンクラスCVmを定義しておきましょう。今回の仮想マシンは、メモリシステムと仮想CPUを生成・保持し、それらのポインタを外部(仮想マシンを操作するメインプログラム)に公開するごく単純なものにします。仮想マシンを動かす時には、step()を呼び出すと仮想CPUに対してコード実行メッセージ(CCpu::exec()呼び出し)を送るようにしました。この時に実行する実験用のコードは、コンストラクタでメモリ上に書き込むことにします。そのコードは、
ld8 r0,30 ld32 r1,12345678 ld32 (r0),r1 ld8 r1,(r0) ld16 r1,(r0) ld32 r1,(r0)
というものにしましょう。このコードはr0レジスタに00000030(10進数で48)、r1レジスタに12345678を代入し、r0レジスタの値のアドレス(から連続4バイト)にr1レジスタの値を代入します。続いて、r0レジスタの値のアドレスから8/16/32ビットでr1レジスタに値を読み込む、というものです。このコードを実行してメモリやレジスタの値を見てみれば、代入がうまく動くか、確認できるでしょう。このコードををハンドアセンブルすると、以下のようになります。
0000 00010030 ld8 r0,30 0004 00030100 12345678 ld32 r1,12345678 000c 00090001 ld32 (r0),r1 0010 00040100 ld8 r1,(r0) 0014 00050100 ld16 r1,(r0) 0018 00060100 ld32 r1,(r0)
メモリのサイズは256バイトとし、コンストラクタで以上の24バイトのコードを00000000-0000001bに書き込みます。
以上のことから、今回の仮想マシンCVmクラスは以下のように定義しました。
CVm::CVm() { pmMemory=new CMemory(256); pcpCpu=new CCpu(pmMemory,0); pmMemory->write32(0x00,0x00010030); pmMemory->write32(0x04,0x00030100); pmMemory->write32(0x08,0x12345678); pmMemory->write32(0x0c,0x00090001); pmMemory->write32(0x10,0x00040100); pmMemory->write32(0x14,0x00050100); pmMemory->write32(0x18,0x00060100); } CVCPU * CVm::getCpu() { return pcpCpu; } CMemory * CVm::getMemory() { return pmMemory; } bool CVm::step() { try { pcpCpu->exec(); } catch (logic_error e) { MessageBox(NULL,e.what(),"エラー",MB_OK); return false; } return true; } CVm::~CVm() { delete pcpCpu; delete pmMemory; }
今回は、仮想マシンのインスタンスを生成し、コード実行とメモリ・レジスタの表示機能を持つメインプログラム(main.cpp)を作ってみました。実行すると、レジスタとメモリの状態が表示されますので、進行ボタンでコードを一つずつ実行してみましょう。実行していくとアドレス00000030からの4バイトに数値が代入され、それがレジスタr1に8/16/32ビットで代入されていきます。