■補講 JavaScriptでCPUをエミュレート/シミュレート
HTML5時代ではJavaScriptの実行速度は飛躍的に向上しました。速度が向上した事で可能になったものとしてCPUのエミュレート(またはシミュレート)があります。JavaScriptでCPUをエミュレートするというのは、すでに実例がありますので特に珍しいというわけではありません。
また、File APIによりバイナリファイルが扱えるようになったというのもあげられます。過去の資産を活かすこともできるわけです。特にコンピューター業界は進歩が早く、数年前に使っていたコンピューターが、すぐに使い物にならなくなってしまうこともあります。これにともなって、そのマシン上で動作していたソフトウェアも消え去ることになります。
それでは実際どのようにしてCPUをエミュレートすればよいのでしょうか。ここでは、25年前に多く使われていた8bit CPUのZ80の一部をエミュレートしてみます。
------------------------------------------------------------------------------------------------
■CPUは、どのようにしてプログラムを実行するのか
まず、コンピューター内部のCPUは、どのようにして動いているのでしょうか。最近の複雑なCPUはさておき、昔のCPUは非常にシンプルです。
実際のZ80のマシン語コードで見てみましょう。サンプルのHTMLにも書かれているのが以下のコードです。数字ですが10進数ではなく16進数となっています。
21 D0 00 36 01 76
数字が2桁ずつ6つ並んでいます。これは一体何のプログラムなのでしょうか?これは画面にAの文字を表示するだけのプログラムです。どうして、これで画面にAの文字が表示できるのか説明しましょう。
CPUはメモリ(RAM)に入っている数字を1つずつ解釈していきます。最初から見ていきましょう。最初にあるのは21です。この21が何なのか人間が見ても分かりませんが、CPUは21は命令コードだと考えて処理を行います。
では21は何の命令コードなのでしょう?これはHLレジスタに以後に続く2バイトの数値を入れるという命令です。つまり、
21 D0 00
で1セットなのです。ここでHLレジスタという名前がでてきました。レジスタとは何でしょう?これはCPU内で使える一時的にデータを入れておくものです。JavaScriptでたとえるなら変数が最も近いでしょう。
Z80 CPUではHLやBC、DEなどのレジスタが用意されています。JavaScriptなどの高級言語とは異なり、数も少なく入れることができるのは0〜255や0〜65535までの数値だけです。CPUの世界では文字列やオブジェクトなどといったものは存在しません。全て数値で処理されています。
21 D0 00はHLレジスタに値を入れますが、D0 00の値ではなく順番が入れ替わった00 D0の値が入ります。これはZ80 CPUが、このようになっているためで、他のCPUではそのままD0 00となるものもあります(*1)。
*1:リトルエンディアン、ビッグエンディアンのキーワードで検索してみてください
------------------------------------------------------------------------------------------------
■命令を解釈し続ける
CPUはひたすらメモリに入っている命令を解釈し続けます。それでは21 D0 00の後のコードを見てみましょう。
36 01 76
36とは何なのでしょうか。これはHLレジスタが示す番地(場所)に値を入れる命令です。値は次に続く1バイトが書き込まれます。上記の場合は01が書き込まれることになります。先ほどHLレジスタには00D0を入れましたから、00D0番地に01が書き込まれることになります。
それでは、次の76は何でしょう? これはCPUを停止させる命令です。実際には完全にCPUが停止するわけでなく、ずっと同じ76という命令を解釈することになります。
これで画面にAの文字が表示されます。わずか6つの数値で画面にAの文字が表示できるのです。
今回のサンプルでエミュレートする命令は以下の5つだけです。これだけあれば画面にHELLOといったメッセージを表示することができます。実際のZ80 CPUと全く同じ命令セットですので、昔Z80を使った事があればすぐに分かるでしょう(本書を読んでいる読者層には少ないかもしれませんが)。
00 : NOP(何もせずPC[プログラムカウンタを1つ進めるだけ])
21 : LD HL, nnmm(HLレジスタに次に続く2バイトを入れる)
23 : INC HL(HLレジスタの値を1つ増やす)
36 : LD (HL), n(HTMLレジスタが示す番地にnの値を書き込む)
76 : HALT(CPUの動作を止める。割り込み以外では復帰しない)
------------------------------------------------------------------------------------------------
■画面の表示
「21 D0 00 36 01 76」で画面にAの文字が表示できると書きましたが、どうしてAの文字が表示できるのでしょう? 実はCPUは上記のコードではAが表示されることは分かりません。画面に文字や画像を表示するのは別の回路が必要だからです。
今回のサンプルでは00D0から16バイト分を画面表示に使える領域としました。つまり画面には16文字しか表示することができません。
画面に文字を表示するには定期的に00D0から16バイトのメモリを読み込んで「数値に応じた文字」を描画します。今回のサンプルでは01がA、02がB、03がCというようにA〜Zまでの英文字が01〜1A(10進数なら26)に対応するようにしています。
画面は定期的に書き換えていますが、あまり頻繁に書き換えると速度が遅くなってしまいます。本サンプルでは1秒間に1回だけ書き換えることにしました。もちろん、もっと速くしてもよいかもしれません。この書き換え速度がリフレッシュレートです。テレビ画面なら1/60秒で書き換えが行われます。以下のサンプルも1/60秒で書き換えるようにしてみるとよいでしょう。
------------------------------------------------------------------------------------------------
■実際のエミュレートプログラム
以下が実際のZ80のエミュレートプログラムです。最低限の機能しかない上にZ80コードは数値でなく文字列で処理しています。
テキストエリアに入力するZ80マシン語コードは1バイトずつ半角空白で区切って入力してください。改行などは使えません。また、RAMは256バイトしかありません。8bit CPUでは最大64KB (65536バイト)まで扱えますので、改良してみるとよいでしょう。
全てのZ80コードを実行できるようにしてみると、非常によい勉強になるはずです。Java言語で作成したプログラムも仮想CPUのバイトコードです。つまり、JavaScriptでJavaをエミュレートすることも可能なはずです(意味がないかもしれませんが)。
昔の8bitマシンはヤフーオークションなどに出品されていることもあります。昔の8bitコンピューターを入手して楽しむというのもよいかもしれません。
------------------------------------------------------------------------------------------------
■本サンプルでのZ80 CPUのサンプルコードをいくつか
以下に本サンプルで使えるZ80プログラムを示します。テキストエリアに入れて動かしてみて下さい。
●ABと表示
21 D0 00 36 01 21 D1 00 36 02 76
●ABと表示(その2)
21 D0 00 36 01 23 36 02 76
●1文字おきにA B Cと表示
21 D0 00 36 01 23 23 36 02 23 23 36 03 76
●HELLOと表示
21 D0 00 36 08 23 36 05 23 36 0C 23 36 0C 23 36 0F 76
----------------------------------------------------------------------------------------
■サンプルプログラム
----------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------
// 8bit CPUをエミュレート
// ---------------------------------------------------------------------------------------------------
// ---------------------------------------------------------------------------------------------------
// ページが読み込まれたらボタンにイベントを割り当て(ここはJavaScript)
// ---------------------------------------------------------------------------------------------------
window.addEventListener("load", function(){
// 「以下のマシン語を実行する」ボタンがクリックされた時の処理
document.getElementById("execButton").addEventListener("click", function(){
var text = document.getElementById("byteCode").value;
RAM = text.split(" "); // 半角空白区切りなので分割してRAMに入れる
execCPU(); // CPUを0番地から実行
}, true);
}, true);
// ---------------------------------------------------------------------------------------------------
// 8bit CPU 命令一覧
// ---------------------------------------------------------------------------------------------------
// 00 : NOP(何もせずPC[プログラムカウンタを1つ進めるだけ])
// 21 : LD HL, nnmm(HLレジスタに次に続く2バイトを入れる)
// 23 : INC HL(HLレジスタの値を1つ増やす)
// 36 : LD (HL), n(HTMLレジスタが示す番地にnの値を書き込む)
// 76 : HALT(CPUの動作を止める。割り込み以外では復帰しない)
// ---------------------------------------------------------------------------------------------------
// 電源ボタンが押された時の処理。RAMの内容を消去
// ---------------------------------------------------------------------------------------------------
var RAM = [];
for(var i=0; i<256; i++){ // 256バイトのRAMをクリア
RAM[i] = 0;
}
// ---------------------------------------------------------------------------------------------------
// CPUをエミュレート。常に0番地から実行。
// ---------------------------------------------------------------------------------------------------
function execCPU(){
var PC = 0; // プログラムカウンタ。最初は0番地から実行
var HL = 0; // HLレジスタの内容をクリア
while(true){
var code = RAM[PC]; // PCが示す番地から1バイト読み出す。都合により数値でなく文字列になっている(手抜き)
PC = PC + 1; // プログラムカウンタを1進める
if (PC > 256){
alert("メモリエラー。256バイトまでしか利用できません");
return;
}
//console.log(code+", PC="+PC);
switch(code){
case "00": // NOPの場合(なにもしない)
break;
case "21":
var low = parseInt("0x" + RAM[PC]);
PC = PC + 1; // プログラムカウンタを1進める
var high = parseInt("0x" + RAM[PC]);
PC = PC + 1; // プログラムカウンタを1進める
HL = high * 256 + low; // 新しい番地(アドレス)を設定する
break;
case "23":
HL = HL + 1; // HLレジスタの内容を1つ増やす
break;
case "36":
RAM[HL] = parseInt("0x" + RAM[PC]);
PC = PC + 1; // プログラムカウンタを1進める
break;
case "76":
return; // CPUの処理を停止。ここでは呼び出し元に戻す
}
}
}
// ---------------------------------------------------------------------------------------------------
// VRAMを画面に表示。0xD0から16文字(バイト)分をVRAMとする
// ---------------------------------------------------------------------------------------------------
// 01がA、02がBとなっている。英文字のみ対応
setInterval(function(){
var c = "";
var ele = document.getElementById("VRAM");
ele.innerHTML = ""; // 一旦画面を全て消去する
for(var i=0; i<16; i++){
var d = RAM[0xD0+i]; // 1バイト読み出す
if ((d >0) && (d<=26)){
c = String.fromCharCode(0x40+d); // 画面に表示できるようにコードを変換
}else{
c = "□"; // 英文字以外は空白(ここでは分かりやすいように□で表示)
}
ele.innerHTML = ele.innerHTML + c;
}
}, 1000); // 画面の書き換え速度(リフレッシュ)。ここでは1秒ごとに書き換える