十数年前、自分の中でFPGAがブームになり、MIPS互換のCPUなどを作ったりしていました。
独自仕様のCPUも作ってみたかったのですが、コンパイラを作ることも既存のものをポーティングすることもできなかったので諦めました。
それ以来、いつかはコンパイラを書いてみたいと思っていました。
年齢を重ねて、プログラムを書けなくなる(興味を失うことも含む)前に、書いてみることにしました。
書き始めたのは3年以上前で、ある程度作っては飽きて放置、しばらくして再開、を繰り返し、やっと形になったので公開します。
ターゲットプロセッサはコンパイラを作るのが楽になるように決めた独自仕様です。
レジスタ(整数/浮動小数点数兼用)
汎用レジスタ: 64ビットx16本
R0: ゼロレジスタ
R1-8: 演算ワーク
R9: 返値
R10-13: 引数
R14: グローバルポインタ
R15: スタックポインタ
PC: プログラムカウンタ
フラグ: 割込み許可ビットのみ
インストラクションセット
ST/LD/LDUのszは1,2,4,8のいずれか
LI Rd,imm Rdのbit15-0にimmを符号拡張してロード
LUI Rd,imm Rdのbit31-16にimmをロード
LWI Rd,imm Rdのbit63-16にimmをロード(ISA64のみ)
LEA Rd,ofs(Rs) ofs(Rs)の実効アドレスをRdにロード
ST.sz Rs,ofs(Rd) Rsの下位szバイトをofs(Rd)にストア
LD.sz Rd,ofs(Rs) ofs(Rs)からszバイトを符号拡張してRdの下位にロード
LDU.sz Rd,ofs(Rs) ofs(Rs)からszバイトを0拡張してRdの下位にロード
MOV Rd,Rs Rd=Rs
ADDI Rd,imm Rd+=imm(符号拡張)
ADD Rd,Rs Rd+=Rs
SUB Rd,Rs Rd-=Rs
MUL Rd,Rs Rd*=Rs
DIV Rd,Rs Rd/=Rs
REM Rd,Rs Rd%=Rs
MULU Rd,Rs Rd*=Rs(符号無し)
DIVU Rd,Rs Rd/=Rs(符号無し)
REMU Rd,Rs Rd%=Rs(符号無し)
ADDF Rd,Rs Rd+=Rs(浮動小数点数)
SUBF Rd,Rs Rd-=Rs(浮動小数点数)
MULF Rd,Rs Rd*=Rs(浮動小数点数)
DIVF Rd,Rs Rd/=Rs(浮動小数点数)
NEGF Rd Rd=-Rd(浮動小数点数)
CVTF Rd Rdを整数とみなし、浮動小数点数に変換
CVTI Rd Rdを浮動小数点数とみなし、整数に変換
COM Rd 1の補数
NEG Rd 2の補数
AND Rd,Rs Rd&=Rs
OR Rd,Rs Rd|=Rs
XOR Rd,Rs Rd^=Rs
ANDI Rd,imm Rd&=imm
ORI Rd,imm Rd|=imm
XORI Rd,imm Rd^=imm
SLL Rd,imm Rdをimmビット左シフト
SRL Rd,imm Rdをimmビット算術右シフト
SRA Rd,imm Rdをimmビット論理右シフト
SLLV Rd,Rn RdをRnビット左シフト
SRLV Rd,Rn RdをRnビット算術右シフト
SRAV Rd,Rn RdをRnビット論理右シフト
SEQ Rd,Rs Rd==RsをRdにセット
SNE Rd,Rs Rd!=RsをRdにセット
SLT Rd,Rs Rd<RsをRdにセット
SLE Rd,Rs Rd<=RsをRdにセット
SGT Rd,Rs Rd>RsをRdにセット
SGE Rd,Rs Rd>=RsをRdにセット
SLTU Rd,Rs Rd<RsをRdにセット(符号無し)
SLEU Rd,Rs Rd<=RsをRdにセット(符号無し)
SGTU Rd,Rs Rd>RsをRdにセット(符号無し)
SGEU Rd,Rs Rd>=RsをRdにセット(符号無し)
SEQF Rd,Rs Rd==RsをRdにセット(浮動小数点数)
SNEF Rd,Rs Rd!=RsをRdにセット(浮動小数点数)
SLTF Rd,Rs Rd<RsをRdにセット(浮動小数点数)
SLEF Rd,Rs Rd<=RsをRdにセット(浮動小数点数)
SGTF Rd,Rs Rd>RsをRdにセット(浮動小数点数)
SGEF Rd,Rs Rd>=RsをRdにセット(浮動小数点数)
BRA label 分岐
BEQ Rs,Rt,label Rs==Rtなら分岐
BNE Rs,Rt,label Rs!=Rtなら分岐
BEQF Rs,Rt,label Rs==Rtなら分岐(浮動小数点数)
BNEF Rs,Rt,label Rs!=Rtなら分岐(浮動小数点数)
CALL label コール
CALLV Rs Rsをアドレスとみなしてコール
RET リターン
RETI 割込みからのリターン
EI 割込み許可
DI 割込み禁止
SLEEP 割込みがあるまで停止
NOP ノーオペレーション
このプロセッサを使ったPCの仕様は以下のようにしました。
メモリ(2Mバイト)
0x000000 リセット時に実行する命令
0x000004-0x00001f 割込み(1-7)時に実行する命令
0x000020-0x03ffff テキストエリア
0x040000-0x13ffff データおよび変数エリア
0x140000-0x1dffef VRAM(640x480,16bit color)
0x1dfff0-0x1dffff I/O
0x1e0000-0x1fffff スタックエリア
入力
ofs
0 キーデータ
1 キーモディファイア
4 ファイル入力
5 EOF
出力
ofs
0 コンソール
2 PSGアドレス
3 PSGデータ
4 LBA bit7-0
5 LBA bit15-8
6 LBA bit23-16
PSG
PSGアドレスbit7-3で32個のオペレータの中から1つを選び、bit2-0で以下の機能を選ぶ
その後PSGデータにデータを書き込む
0 加算値下位
1 加算値上位(0xffだとノイズ)
2 ボリューム左
3 ボリューム右
4 波形選択(矩形波デューティ50%/矩形波デューティ12.5%/鋸波/三角波)
開発方針
Cで書かれたソースの多くをコンパイルできることを目標にしますが、以下の制限を設けます。
ビットフィールド無し
キーワードvolatile,register,autoは無視
演算用レジスタは8本で、演算中に保持する値がこれを超える複雑な式はコンパイルエラーとする
また、intとlongは32ビット、long long,float,doubleは64ビットです。
Cコンパイラを書くときは(セルフホスティングが目的などの理由で)Cで書くことが多いようですが、今回は楽ができるC++17で書きました。
また、短時間しか走らないプログラムなので、一度確保したメモリは終了まで解放しない方針で書きました。
これも、解放を考えなければとても楽だからです。
使用したライブラリ
コンパイラがあっても、標準ライブラリ的なものがないと実用になりません。
既存のものはどうしてもOSに依存するのですが、多少編集すれば依存を無くせるものを見つけました。
https://git.kernel.org/pub/scm/libs/klibc/klibc.git
それでもstdio周りは辛いので、ChaNさんのものを使わせていただきました。
Embedded String Functions/FatFs Module/TJpgDec Module
http://elm-chan.org/fsw.html
JPEGデコーダはサンプルのビューアに使用しています。
フォントは下記のものを使わせていただきました。
M+ BITMAP FONTS
http://mplus-fonts.sourceforge.jp/mplus-bitmap-fonts/index.html
オブジェクトはアセンブルリスト
実験的なコンパイラなので、オブジェクトファイルのフォーマットはアセンブルリストの形式になっています。
tjpgdライブラリをコンパイルした例です。
#define BYTECLIP(v) Clip8[(unsigned int)(v) & 0x3FF]
static const uint8_t Clip8[1024] = {
...
};
...
const int CVACC = (sizeof (int) > 2) ? 1024 : 128;
...
int yy, cb, cr;
のように定義されていて、次の式:
*pix++ = BYTECLIP(yy + ((int)(1.402 * CVACC) * cr) / CVACC);
は以下のようにコンパイルされるのが簡単に確認できます。
;#L838 *pix++ = Clip8[(unsigned int)(yy + ((int)(1.402 * CVACC) * cr) / CVACC) & 0x3FF];
T 00001F64 d 2F1E80C0 lea r1,-32576(r14) ;Clip8
T 00001F68 - 3A2F0050 ld.4 r2,80(r15) ;yy
T 00001F6C - 3A3F0048 ld.4 r3,72(r15) ;cr
T 00001F70 - 1B40059B li r4,1435
T 00001F74 - 42340000 mul r3,r4
T 00001F78 - 1F30000A sra r3,10
T 00001F7C - 40230000 add r2,r3
T 00001F80 - 182003FF andi r2,1023
T 00001F84 - 40120000 add r1,r2
T 00001F88 - 3C110000 ldu.1 r1,0(r1)
T 00001F8C - 3B2F0030 ld.8 r2,48(r15) ;pix
T 00001F90 - 14200001 addi r2,1
T 00001F94 - 332F0030 st.8 r2,48(r15) ;pix
T 00001F98 - 3012FFFF st.1 r1,-1(r2)
各行は、セクション、オフセット、リンカへの指定、バイナリ、ニーモニックとなっています。
上の例のように、一つの式の範囲内では割とまともに最適化します。
ライブラリの代わりにリンクポインタを実装
通常の開発ツールではコンパイルしたオブジェクトファイルをarなどでまとめてライブラリを作り、利用する側は-lオプションをつけてリンク指定します。
このコンパイラでは複数のソースをコンパイルするときに-aオプションをつけるとリンクパスファイルを生成します。
これは、以下のようにシンボルとそれが定義されているファイルパスを並べただけのファイルです。
jd_decomp tjpgd3/src/tjpgd.o
jd_prepare tjpgd3/src/tjpgd.o
これを利用する側は-lオプションで指定するとあたかもライブラリのようにリンクできる仕組みです。
割込みハンドラ
特殊な機能として、関数名がinterrupt[1-7]だと割込みハンドラに設定されます。
サンプルの使い方
テキスト/グラフィックの描画デモ、マンデルブロ集合、MIDIプレイヤー、JPEGビューアのサンプルです。
ホームディレクトリにfat.dmgという名前のディスクイメージを用意します。
ディスクイメージはFATでフォーマットし、ルートディレクトリにMIDIとJPEGという名前のディレクトリを作ります。
MIDIディレクトリには拡張子が.midのSMF0フォーマットのファイルを、JPEGディレクトリには拡張子が.jpgのファイルを入れ、アンマウントしておきます。
cckp.xcodeprojを開いてリリース版をビルドします。デフォルトのビルドディレクトリにコンパイラが生成されます。
次にトップディレクトリでmakeするとa.srecができるので、vmkp.xcodeprojを開いて実行し、vmkpでa.srecを開いて実行します。
vmkpのオマケ機能として、関数単位のタイムプロファイリングができます。
以下はJPEGビューアを実行した例です。
28.3% mcu_output (tjpgd.o)
18.1% block_idct (tjpgd.o)
16.2% memset (memset.o)
13.1% huffext (tjpgd.o)
11.4% mcu_load (tjpgd.o)
6.3% bitext (tjpgd.o)
4.0% outfunc (viewer.o)
1.6% disk_read (diskio.o)
0.4% jd_decomp (tjpgd.o)
0.3% infunc (viewer.o)
0.1% f_read (ff.o)
0.1% memcpy (memcpy.o)
https://github.com/kwhr0/cckp
macOS11.0以上