第三世代OSASKの仮想CPUの仕様
- (by K, 2013.02.27)
- (osask.netに書こうと思ったんだけどパスワードを忘れて新規ページが作れない。メモを見つけるまでこっちに書いておく)
目的
- 今回のバージョンでは性能はとりあえず追求しない(速度、サイズとも)
- 相応の速度が出ればそれでよい
- ネイティブコードには到底かなわなくてよい
- 互換性のための代償はしょうがない
- その代わりネイティブコードとバイトコードの混在を許していく方向で
- バイナリの中に一部の関数の機種依存コードがアーカイブされていて、条件が一致してユーザが許可していればそれを使う、みたいな
- シンプルさを重視
- 4bitや8bitのCPUから64bitやそれ以上のbit数のCPUに幅広く対応
- つまり特定のbit数のアーキテクチャに最適化しない
- 一方でハードウェアへの要求を小さくするために、割り込み、ページング、セグメンテーションを利用しない
- 将来FPGAでCPUの自作をするために、ハードルを小さくしておこうと思っている
- これでもそれなりには高速に動作する環境を構築できると思っている
- キャッシュメモリもCPUとしては不要だが、特定のアドレスに「速いメモリ」がつながっているのは、是非ほしい
仕様
- 整数レジスタ 256bit 64本
- アドレスレジスタ 64本
- フラグレジスタはない。
- 割り込み命令も持たない。
- ただし割り込み要求が来ているかどうかを調べるポーリング命令はある。
- 割り込みが来ていれば分岐する。
- MOV系の命令のみ、メモリオペランドがある。
- MOV系の命令のみ、即値を指定できる。
- JMP命令はなく、インストラクションポインタへのMOVで代用する。
- ほとんどの演算命令は三項形式である。(例)reg0=reg1+reg2;
- CMPとSETccが合成されたような命令がある。
- CMPcc(reg0, reg1, reg2);
- reg1とreg2を比較してccが真ならreg0は-1になる、偽なら0になる。
- つまり整数レジスタをフラグレジスタの代用にしているともいえる。
- 条件分岐命令は用意されていない。代わりに条件プリフィクスはある。ARMを想起すると分かりやすい。
- 条件プリフィクスはコンディションを指定する代わりに整数レジスタを指定する。整数レジスタのbit0が1であれば後続の命令は実行される。
- メモリアドレッシングにおいては、かならず一つのアドレスレジスタをベース値として指定しなければならない。
- アドレスレジスタから整数レジスタへのMOVはできないし、逆方向もできない。アドレスの差を計算してそれを整数レジスタに入れることはできる。C言語でのintとポインタの関係とほとんど同じ。
- メモリには型があり、たとえばアドレス値を格納しているメモリからMOVで整数レジスタに読み込む手段は存在しない。
- ADDやSUBやシフト命令でキャリーフラグを得る方法がないが、それだと多倍長演算がやりにくくてたまらない。
- これは後日考える。まあ256bit以内の演算であればそもそも多倍長なんて使う必要はないが。
- 演算命令:
- NOTやNEGはない。それらは-1のXORや、0からのSUBで代用できる。INCやDECもない。
- ADD, SUB, OR, XOR, AND, TEST, MUL, DIV, MOD, SHL, SHR, SAR, CMPcc
- 整数レジスタは全て符号付にしたので、SHRはいらないかも。
- PUSHやPOPなど、スタックを操作する命令はない。メモリにレジスタの値を書いたり読んだりすることはできるので、必要ならそれでやることになる。
- 実はそもそもスタックという概念がない。関数をコールする命令もない。あるのはMOV命令を使ったJMPだけである。どこに帰ってきてほしいのかはスタックで教えないで、適当なレジスタで教える。たとえば「この関数は処理終了後にADRREG3で指定されたアドレスへ分岐します(=returnします)」みたいになっている。つまり関数コールの帰り先が呼び出し命令の次の命令になるという基本ルールはない。
- メモリアドレスの単位はバイトではない。ビットである。そもそもgg02にバイトという単位は現れない。
補足
- レジスタ本数が異様に多いのは、命令セット上、演算系の命令で定数を指定できないためである。そのため常に0を入れておくレジスタや常に1を入れておくレジスタがほしくなるだろう。そういう定数のために使われるレジスタを考えたら、16本では不足することが予想される。
- NULLポインタなど比較用のアドレスもレジスタに入れておかなければならない
- 定数レジスタ属性というものを設定する予定(バイトコードからネイティブコードに変換する際にヒントとして利用)
- レジスタのbit長が256bitもあるが、それぞれの命令には(整数演算であっても)有効数字というものがあり、もしこれが16bitを指定していれば、下位16bitが正しく計算されることを保障し、より上位については未定義になる。これによりプログラムは必要な有効数字で計算をすればよく、過剰な演算量になることはない。
- 整数レジスタは、AXがALとAHに分割されるみたいな機能はない。ARMと同じである。
- レジスタが2本しかなくても書ける処理はこのアーキテクチャで2本だけ使って書けばよいし、8bitで十分なら8bitを有効数字指定すればよい。そうすれば8bit-CPUでも効率よく動くだろう。もちろん32bitや64bitでも高速に動くだろう。
- 一方で64bitの演算をしたいならそう書けばいい。レジスタを10本使いたければそう書けばいい。おそらくx86(32bit)環境においては、レジスタの上位bitはメモリで代用されるし、レジスタ10本はないのであふれた分もメモリで代用される。
- しかしx64などではこれはすべて実レジスタに載って高速に動くだろう。それでいいのだ。同じバイナリのままで、64bit化の恩恵が受けられると見てもいいし、逆に64bit用プログラムが32bitや(なんなら8bitでも)動くという見方もできる。
- 割り込みがないってどういうこと?
- 何かの操作の途中で割り込みが入ると面倒になることがある。その場合私たちはその範囲をCLI~STIで挟む。もしこの手の操作が頻発したらどうだろう。つまりCLIが終わってSTIしたと思ったらまた次のCLIがすぐに始まる、みたいなケースだ。
- これならずっとCLIしておいて、STIするタイミングで割り込みが来ていないかをポーリングするほうが単純だ。それに割り込みがなくなれば、FPGAでCPUを作るときに単純化しやすい。
- レジスタ番号48~63は定数用のレジスタである。定数レジスタは、ネイティブコードに変換されるときに即値化されるかもしれない。つまりコードがその箇所に差し掛かるときは、常に一意な値をとることをプログラマが保障しなければいけない。
- スタックがないせいでいろいろ不便を強いる。たとえば再帰処理を書きたいときはどうする?
- スタック操作命令がないだけであって、ポインタ操作などはできるから、スタック的なものは容易に作れる。
- 不便であることは認める。
- このほかにもいろいろと不便なことがあることは想定されるが、それは将来的に、短縮形を検討する。短縮形を含んだgg02から短縮形を含まないgg02に変換するコンバータを作る。こうすればバイナリ生成器は短縮形をサポートする必要がなくなる。
- なぜ汎用レジスタではなく専用レジスタ?
- アドレスレジスタと整数レジスタを汎用レジスタとして実装するのが現代では普通である。しかしgg02ではポインタと整数に対する演算体系があまりにも違うために、これらをはっきりと区別することにした。全て汎用レジスタにしておいて、現在どのレジスタが整数値を保持しており、どのレジスタがアドレスを保持しているのかを属性設定させるような仕様も考えたが、複雑になりすぎた。結局レジスタを分けてしまったほうが単純だと考えた。
- レジスタを分けたおかげで、レジスタ幅とアドレス幅をそろえる必要もなくなった。
考察
- Z80用のバイナリ生成器を作ることは十分に考えられるが、その場合、256bitはサポートしないかもしれない。せいぜい64bitか。またレジスタも64本はサポートしないで16+8本くらいかもしれない(レジスタ番号0-15とレジスタ番号48-56)。そのような仕様でかまわない。これはサブセットということになる。
- レジスタをたくさん使ったり、長いbit幅の演算を使ったものについてはサブセット版ではバイナリ生成ができないことになるが、これは仕方ない。サブセットとはそういうものである。むしろ8bitと64bitとで同じアセンブラ文法で書けていることのほうが偉大だと僕は考える。そしてサブセットの範囲内で書きさえすれば、サブセットでもフルセットでもどちらでも動く。