kharcs #1
(0) これはなに?
- (kharcsページは、kharc開発でわかったことを整理して説明するためのページ群です。)
- 今回説明したいこと:
- [1]独自の仮想マシンの命令セットを考える
- [2]そいつのアセンブラを設計する
- [3]サンプルコードを書いてみる
- [4]エミュレータを作ってサンプルコードを実行してみる
(1)
- 開発のきっかけはいろいろありましたが、まあそれは本題ではないのでここでは省略します。
- 簡単なCPUを設計してみました。レジスタマシンです。
- レジスタ:
- 整数レジスタ(32bit): R0~R7
- アドレスレジスタ(32bit): A0~A7
- 浮動小数点レジスタ(64bit): F0~F7 (しかし今回の実装ではこれは出てきません)
- 一般的なCPUではレジスタは「汎用レジスタ」とされ、整数用とアドレス用に分かれてはいませんが、このkharcでは用途別にレジスタを分けてみました。
- (ちなみに今回の実装の範囲では、この整数レジスタとアドレスレジスタを分けたことは、実装の手間が増えただけで、メリットらしいものはありませんでした。)
- レジスタの別名:
- SP: A7の別名。スタックポインタ用。
- RP: A6の別名。return-pc。リターン用のプログラムカウンタ。
- 演算系の命令(四則演算やビット演算など)はレジスタ間、レジスタ・即値間のみとして、メモリを指定しないことにします。
- メモリ上のスタックに対する命令はなしにしました。何かをPUSHしたければ、 SP-=4; [SP]=Rx; とすればいいです。POPしたければ、 Rx=[SP]; SP+=4; とすればいいです。
- CALLも戻り先をスタックには積まずに、リターンアドレスをRPに入れておくだけにします。これを必要に応じてメモリ上のスタックに積むのはプログラム側の仕事です。
- こうすることで、メモリアクセスを伴う命令はload/store命令だけに限定されます。まあRISC風にしただけのことですが。
(2)
- kharcのアセンブラは、C言語っぽい構文を採用してみました(単なる趣味です)。引数はカッコでくくります。文末にはセミコロンを付けます。
- ラベルの名前解決が面倒だったので、ラベルはすべて番号にしました。カッコいい「名前が自由に使えるラベル」は、アセンブラよりも上のレイヤで使ってもらいます。
- このアセンブラはバックエンドとして動けばいいので、頑張らないことにしました。
| MovRI(r,imm); | R0~R7に即値定数immを代入、Rx=imm; |
| MovAI(a,imm); | A0~A7に即値定数immを代入、Ax=imm; |
| LodRMd(r,base,disp); | Rxの値をメモリから読み込みます(load)、base=ベースレジスタAx, disp=即値ディスプレイスメント、Rx=[Ax+disp]; |
| StoRMd(r,base,disp); | Rxの値をメモリに書き込みます(store)、[Ax+disp]=Rx; |
| LodAMd(a,base,disp); | Ax=[Ax+disp]; |
| StoAMd(a,base,disp); | [Ax+disp]=Ax; |
| AddRI(r,imm); | Rxに即値定数immを加算、Rx+=imm; |
| AddAI(a,imm); | Ax+=imm; |
| AddRR(r0,r1); | r0+=r1; |
| SubRI(r,imm); | Rx-=imm; |
| SubAI(a,imm); | Ax-=imm; |
| SubRR(r0,r1); | r0-=r1; |
| CmpJleRII(r,imm,lab); | Rxを即値定数immと比較して結果に応じてラベル番号labにジャンプする、if(Rx<=imm)goto lab; |
| CmpJccRII(r,imm,lab); | 他にもJe/Jne/Jl/Jge/Jgがあるが、書くのが面倒なので省略 |
| LbI(lab); | ラベル宣言、JmpやCalの飛び先指定に使用 |
| JmpI(lab); | 無条件分岐、goto lab; |
| JmpA(a); | 無条件分岐、goto Ax; |
| CalII(lab,tmp); | サブルーチンのCALL命令、tmpは内部処理用のラベル番号(欄外に補足説明あり)、call lab; |
| Ret(); | サブルーチンからのリターン、return; |
- 補足説明:
- CalI(lab,tmp); は実際はマクロになっていて、 MovAI(RP,tmp); JmpI(lab); LbI(tmp); と展開されます。だからtmpが必要なのです。
- Ret(); もマクロで、 JmpA(RP); になっています。
- load/storeでのメモリアドレッシングでは、ベースレジスタAxは絶対に必要で、命令としては省略させない想定です(Axに0を入れておけば、0アドレスからの指定はできます)。
(3)
- サンプルコードとして、再帰でフィボナッチ数を求めるプログラムを書きました。あとでベンチマークするので、そこそこ重い処理にしたいのです。
- まず、C言語で書くとこんな感じです。
int fib(int i)
{
if (i <= 1) return i;
return fib(i - 2) + fib(i - 1);
}
int main()
{
printf("fib(46)=%d\n", fib(46));
return 0;
}
- これをkharcアセンブラで書くとこうなります。
LbI(1); // fib.
SubAI(SP,16);
StoAMd(RP,SP,8); // RPを保存.
LodRMd(R0,SP,16); // R0=引数.
CmpJleRII(R0,1,5);
LbI(2);
SubRI(R0,2); // fib(i-2)
StoRMd(R0,SP,0);
CalII(1,3);
StoRMd(R0,SP,12); // 結果を保存.
LodRMd(R0,SP,16); // fib(i-1)
SubRI(R0,1);
StoRMd(R0,SP,0);
CalII(1,4);
LodRMd(R1,SP,12); // 結果の加算.
AddRR(R0,R1);
LbI(5);
LodAMd(RP,SP,8); // RPの復元.
AddAI(SP,16);
Ret();
LbI(6); // main
SubAI(SP,8);
MovRI(R0,46); // fib(46)
StoRMd(R0,SP,0);
CalII(1,7);
// この時点のR0に答えが入っている.
(4)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define kharcMain_bgn() int R0,R1,RP; vJmp: switch (PC) {
#define kharcMain_end() }
#define LbI(l) lb##l: case l:
#define MovRI(r,i) r=i
#define MovAI(a,i) a=i
#define LodRMd(r,b,d) r=*(int*)&m0[b+d]
#define StoRMd(r,b,d) *(int*)&m0[b+d]=r
#define LodAMd(a,b,d) a=*(int*)&m0[b+d]
#define StoAMd(a,b,d) *(int*)&m0[b+d]=a
#define CmpJleRII(r,i,l) if(r<=i)goto lb##l
#define SubAI(a,i) a-=i
#define AddAI(a,i) a+=i
#define SubRI(r,i) r-=i
#define AddRI(r,i) r+=i
#define AddRR(r0,r1) r0+=r1
#define CalII(l,m) MovAI(RP,m); goto lb##l; LbI(m)
#define Ret() PC=RP; goto vJmp
int kharcMain(int PC, int SP, char *m0)
{
kharcMain_bgn();
(上記のコード)
kharcMain_end();
return R0;
}
int main()
{
char *mem = malloc(64 * 1024);
int R0 = kharcMain(6, 64 * 1024, mem);
printf("R0=%d\n", R0);
return 0;
}