ブログの名前なんて適当で良いのでは

説明を求めるな、記事を読め

新年なのでRustでComet IIエミュレータを書いた

あけましておめでとうございます.今年もよろしくお願いします.

さて,昨日は大晦日だったので海老天蕎麦を食ったり,大掃除したりといろいろしていたんですが,最終的には夜からガキ使を見ながらだらだらとRustでComet IIのエミュレータを書いていました.去年の冬コミケ(C93)ではRustでWebアプリケーションフレームワークIronを使った同人誌を売ったりしていたのもあり,自分の中でRust熱が強いのでこれを書いてみました.

git.alicemacs.com

一応仕様に記載されている命令(SVC以外)は実装しました.ただシフト系の命令はゴリ押し実装しているのでミスってる感じあると思うのでぼちぼち修正していきます.上記のexampleにもありますが,現状は機械語を実行するだけなので,ハンドアセンブルした機械語を格納する必要があります.まだリファクタリングをしていないので変数名やモジュール分けが綺麗ではないですが,ざっくりとexampleのコードと共に実装の裏側を紹介して行きたいと思います.上記に掲載されているexampleコードを張っておきます.

extern crate rust_casl2;
use rust_casl2::emu::Emu;

fn main() {
    let mut emu = Emu::new();
    emu.gr[1] = 0x0;
    emu.gr[2] = 0xdead;
    emu.memory[0] = 0x1412; // LD GR1, GR2
    let code = emu.fetch();
    emu.execute(code);
    println!("{:x}", gr[1]); //=> 0xdead
    println!("{:x}", gr[2]); //=> 0xdead
}

冒頭では,初めにミュータブルなEmu構造体のインスタンを生成しています.現状memoryフィールドに機械語を突っ込んだりするので,mutを付けています.

Emu構造体は,src/emu.rs にあります.

pub struct Emu{
    
    pub memory: [u16; 65536],
    
    // General Register
    pub gr: [u16; 8],
    
    // Program Register
    pub pr: u16,
    
    // Stack Pointer
    pub sp: Vec<u16>,
    
    // Flag Register [OF, SF, ZF]
    fr: [bool; 3], 
}

Comet IIでは基本的に符号なし16bitな処理なので,フィールドも基本的にはそうしています.各種フィールドの使い方はコメント通りです.
フラグレジスタは,当初u8の末尾3bitを使う方針でいましたが,なんか気持悪かった(使わない上位ビットが余っていて)ので,[bool; 3]にしましたが,3つのboolフィールドを持つ構造体を作って使いやすいメソッドを付けたほうが良い気がしてきました.

次のexampleの数行では,各種レジスタやメモリーにexample用の値を設定しています.今回の例ではLD命令を使ってレジスタ間での値のロードをしようとしています.具体的にはGR2に入っている0xdeadをGR1にロードします.LD命令のレジスタ間のオペコードは0x14なので,0x1412で LD GR1 GR2 を表しています.

値の設定後は,fetchメソッドでProgram Registerが指すmemoryから値を取ってきて,executeメソッドに渡して実行します.よくあるパイプラインの処理では,fetch -> decode -> executeとかですが,decodeな処理を書こうとすると冗長な気がした(たぶん正確にここらへんの区別がわかってないからかもしれない)ので,一気にexecuteという表現にしてしまっています. では,命令の実行部分を見てみましょう.executeメソッドの内部では,命令の上位1byteを見て愚直にmatchで分岐させています.分岐後の呼び出す関数は,無駄にsrc/instruction に小分けにして書いてあります.例としてsrc/instruction/ld.rs のLD命令を見てみましょう.

pub fn ld_r1_r2(emu: &mut Emu, code: u16) {

    let r1 = ((code & 0xf0) >> 4) as usize;
    let r2 = (code & 0xf) as usize;

    emu.gr[r1] = emu.gr[r2];

    let v = emu.gr[r1];
    emu.set_fr(ZF, v == 0);
    emu.set_fr(SF, is_set_msb(v));
    emu.set_fr(OF, false);

}

引数に設定されているcodeはfetchで取り出してきた2byteで,その中から1つ目,2つ目のレジスタに対応する値を取り出してr1, r2に格納します(上位1byte使っていないのが気になるのでu8とかに変えても良い気がしてきた).注意点としてこれをgrレジスタ配列の添字として使うので,u8等ではダメで,usizeに変換する必要があります. LD命令では,フラグレジスタを変更する必要があるので,各種ZF, SF, OFを設定しています.is_set_msb関数はsrc/util.rsで定義していて最上位bitが立っているかどうかのboolを返す関数となっています. そして,executeメソッド後ではgr[1]が0xdeadのgr[2]で書き換わっているのでprintln!で出力してみるとgr[1]の方で0xdeadが出力されるという感じです.

一応,各種命令のソースコードと共に単体テストを書いていますが,機械語自体にミスがあったりするかもしれないし,ハンドアセンブルするのも慣れてきてしまってはいますが,楽するためにもCASL2アセンブルするプログラムを作ったほうが良いという結論になり作っています.本当はそれ含めて公開する予定でしたが,間に合いませんでした.これはまた次の機会に公開したいと思います.

皆様にとって良い一年になりますように

2018-01-12 追記:

上記で言ってたCASL2アセンブラが完成しました.相変わらず突貫工事なのでミスはありそうですが,とりあえずツールとして動くレベルになりました.それと同時にいろいろ改造してコマンドラインツールとしてアセンブルとエミュレートが実行できるようになりました.

git.alicemacs.com

あとはRustでのテストやドキュメントの書き方とか含めリファクタリングをしながら理解を深めていこうかなと考えています.