CPUの創りかたは前から持っていて、実際に作ってみたかったんだけど、
電子工作は得意ではないので、その前にエミュレータを書いてみた。
著者の方はエミュレータを公開してくれているけど、自分Windows持ってないので本で紹介されている画面とかから
このエミュレータの機能を推測しつつcで書いてみました。
UIはGTK+で適当に作ったのでバグありだけどw
↑の画面は一部修正前のやつなので、Program Counterが0x10までいってるけど、今は修正しますた。
エミュレータを書いた一つの理由はCPUのエミュレーションをしてみたかったってのもあります。
そもそも、CPUは基本的に以下の4個の処理を繰り返すのでUI書かなければTD4のエミュレーションは比較的簡単なんじゃねという感じです。
1.メモリから命令を読み込む
2.解釈する
3.実行する
4.結果を保存する
CPUの処理は上の4個なんだけど、その他にレジスタとかメモリ、I/Oポートもc言語で表現してあげればエミュレートできるはずなので、
それらをどうやって表現するかがポイント。
TD4のレジスタはAレジスタとBレジスタで両方共4bit。これを構造体にして、こんな感じにしました。
// TD4 has two registers which A register and B register. // Register size is 4 bit. struct td4_acc_registers { u_int8_t reg_a; u_int8_t reg_b; };
フラグ系はキャリーフラグしかないのでこんな感じで、
// TD4 has only one flag register. struct td4_flag_registers { u_int8_t carry; };
I/Oポートはこれ。どっちも4bitしか使いません。
struct td4_io_port {
u_int8_t in_port;
u_int8_t out_port;
};
あとは、メモリとプログラムカウンター(x86でいうところのIP)ですが、これらは単にu_int8_tな変数です。
ほんとは名称をPCとかにするべきだったんですが、なんとなくipにしました。
メモリは16byteまでなので、こういう定義を入れてます。
// Memory space is 16 bytes #define ADDRESS_SPACE_SIZE 16
最後にこれらをまとめて、こんな構造体をつくりました。
struct td4_state { struct td4_acc_registers *acc; struct td4_flag_registers *flags; u_int8_t ip; u_int8_t memory[ADDRESS_SPACE_SIZE]; struct td4_io_port *io; };
命令の読み込み、解釈、実行を制御してるのはこんな関数です。
void *decoder(struct td4_state *state) { int ret; while (1) { ret = parse_opecode(state, fetch(state)); if (ret) inrement_ip(state); if (get_ip(state) >= ADDRESS_SPACE_SIZE) break; } return NULL; }
parse_opecode()は命令の解釈と実行で、1番目の引数はレジスタとかの構造体、2番目の引数はメモリの内容です。
fetch()がipが指す場所のメモリの内容を返します。
inline u_int8_t fetch(struct td4_state *state) { return state->memory[state->ip]; }
次の、inrement_ip()のところのif文はかっこ悪いんですがwww、jmp、jnc命令以外の場合は、ipをインクリメントしてます。
jmp、jncはipの値を既に変えているので、ここでは変更しませんよということです。
最後のif文はjmp、jncで明示的に無限ループになるようなプログラムでない限り、全命令実行後にプログラムを終了できるようにするためです。
ちなみに、関数がvoidのポインタを返してるのは、なんとなくスレッドを使おうと思ってた時期があった時の名残りです。
I/O処理とdecoder()をスレッドにしようとか思ってたんですが、今はやってません。
次に、命令のデコード部分に進みます。
// OPCODE for TD4. struct opcode { u_int8_t op; u_int8_t (*func)(struct td4_state *state, u_int8_t im); }; // OPCODE for TD4. static struct opcode opcodes[] = { // ADD functions { 0x00, add_a }, // 0000: ADD A, Im { 0x05, add_b }, // 0101: AAD B, Im // MOV functions // Moving imediation data to A or B register. { 0x03, mov_a }, // 0011: MOV A, Im { 0x07, mov_b }, // 0111: MOV B, Im // Mov data from register to register. { 0x01, mov_b2a }, // 0001: MOV A, B { 0x04, mov_a2b }, // 0100: MOV B, A // JMP function. { 0x0f, jmp }, // 1111: JMP Im // JMP if a condition is true. { 0x0e, jnc }, // 1110: JNC Im // IN functions. { 0x02, in_a }, // 0010: IN A { 0x06, in_b }, // 0110: IN B // OUT functions. { 0x09, out_b }, // 1001: OUT B { 0x0b, out_im }, // 1011: OUT Im // NOP // it same as ADD A, 0 };
まずは、こんな感じで命令とそれに対応する関数を設定する構造体を作りました。
これはTD4は命令4bit、イミディエートデータ4bitなのでこうやっても問題ないよねーとw
でも、これはあくまでも命令と関数の対応付けだけです。
実際は、命令がなにか調べてそれに対応する関数を呼ぶようにしないといけませんね。
そうすると、ifとかswitchで分岐したりが必要になると思うんですが、
先に書いたように、TD4は命令長bitなので命令をインデックス番号にしたテーブル作ったほうが早いよなと思い、以下のような構造体をさらに作りました。
struct opcode_table { struct opcode *op; }; static struct opcode_table *op_table;
そんで、このop_tableを以下の関数で初期化して、命令と処理を行う関数の対応を付けます。
void init_opcode_table(void) { int i; op_table = xmalloc(sizeof(struct opcode_table *) * OPCODE_MAX_VALUE); for (i = 0; i < OPCODE_MAX_VALUE; i++) op_table[i].op = NULL; for (i = 0; i < sizeof(opcodes) / sizeof(opcodes[0]); i++) op_table[opcodes[i].op].op = &opcodes[i]; }
こうすれば、命令をインデックスとしてop_tableにアクセスすれば分岐無しで実行したい関数を一発で呼べます。
使わない命令があるので、一部は無駄になりますが、格好良く言うと空間効率と時間効率どっちをとるかで、時間効率を取った訳ですヽ(*´∀`)ノ
こうすると、デコーダ部分の処理は以下の数行で完成します。
bool parse_opecode(struct td4_state *state, u_int8_t data) { u_int8_t op, im, ret; ret = false; op = data >> 4; im = data & 0x0f; dump_operand(op, im); // op should already be between 0 to 0xf. // it doesn't need to check its range. ret = op_table[op].op->func(state, im); return ret; }
TD4は8bit中の上位4bitを命令に、下位4bitをイミディエートデータ(Im)にしているので、
適当にビット演算すれば命令とImを取得できます。dump_operand()はデバッグ用です。
op = data >> 4; im = data & 0x0f;
そして、opをインデックスにして命令に応じた関数を呼び出します。
opは最大でも0x0fにしかならないので、境界チェックすらしてません。
// op should already be between 0 to 0xf. // it doesn't need to check its range. ret = op_table[op].op->func(state, im);
例えば、AレジスタにImを追加する命令だとこんな感じです。
static u_int8_t add(struct td4_state *state, u_int8_t reg, u_int8_t im) { // clear carry flag before execute opecode. set_carry_flag(state, 0); u_int8_t ret = 0; ret = reg + im; if (ret > 0x0f) { ret &= 0x0f; set_carry_flag(state, 1); } return ret; } static u_int8_t add_a(struct td4_state *state, u_int8_t im) { state->acc->reg_a = add(state, state->acc->reg_a, im); return 1; }
一部、気になるところはあるんですが、動いてるからそのうちリファクタリングすれば良いかといことで、現状こんな形です。
最初にadd_a()が呼ばれて、実際の処理はadd()にお任せしてます。
最初にキャリーフラグを0にします。そして、Aレジスタの値にImを足して、
その結果が0x0fを越えたら、上位4bitを捨てる&キャリーフラグを立ててあげます。
set_carry_flag()はご想像どおりの処理です。
inline void set_carry_flag(struct td4_state *state, u_int8_t flg) { state->flags->carry = flg; }
ほかに、jnc命令はこんな関数です。
static u_int8_t jnc(struct td4_state *state, u_int8_t im) { if (!get_carry_flag(state)) set_ip(state, im); // clear carry flag after execute opecode. set_carry_flag(state, 0); return 0; }
単純に、キャリーフラグが立ってなければ、IP(プログラムカウンター)の値を、imで指定されたところに変更します。
set_ip()、get_carry_flag()もご想像どおりの実装ですwww
inline void set_ip(struct td4_state *state, u_int8_t val) { state->ip = val; } inline u_int8_t get_carry_flag(struct td4_state *state) { return state->flags->carry; }
I/Oポートだと、こんな感じにしてます。単にデータを読み書きするだけで、
それに対するI/Oポート側の処理は別に行えるようにしてます。
#これが、最初にスレッドにしようと思った理由です。
#I/Oポートを見張ってるスレッドが値の変更に応じて何かすると。
static u_int8_t in_a(struct td4_state *state, u_int8_t im) { // clear carry flag before execute opecode. set_carry_flag(state, 0); state->acc->reg_a = state->io->in_port & 0x0f; return 1; } static u_int8_t out_im(struct td4_state *state, u_int8_t im) { // clear carry flag before execute opecode. set_carry_flag(state, 0); state->io->out_port = im; return 1; }
と、こんな感じで全命令を作ってあげればTD4のエミュレータ完成ですヽ(*´∀`)ノ キャッホーイ!!っ
x86のエミュレーションはシャレにならないくらい難しいと思いますが、CPUのエミュレーションは
命令の解釈・実行とレジスタとかをうまくコンピュータ言語で表現すればできるので(当たり前だけど)、
機能が少ないCPUとかのエミュレータを書くのはCPUの勉強に良いんじゃないかなーと思ったりしました∩( ・ω・)∩ ばんじゃーい