OSECPUのバイトコードの詳細仕様
(0) はじめに
(1) 整数レジスタ
- OSECPUは32bitの signed int な整数レジスタを64本持っています。
- R00~R3F と表記します。
- R00やR01など番号の若いレジスタは、実際のCPUの実レジスタに割り当てるように推奨されているので、R00への代入やR00の値の参照は、R20に対する操作と比べて数倍高速になることが多いです。
- 結局どうなるかは処理系依存で、保障はされていません。
- ということで、特にこだわりがないのならR00やR01を使いましょう。
- ちなみにx86版では、R00~R02までが実レジスタに割り当てられていて、残りはメモリを使ってレジスタをエミュレーションしています。
- これらのレジスタはすべて対等で汎用的に使われるというわけではなく、ある程度の使い方が決まっています。これに逆らってはいけないということはないですが、ライブラリなどで食い違うといろいろ面倒かもしれません。
- R00~R1F (32本) : 最も汎用的な整数レジスタで、通常は関数ごとにローカルとして扱えます。つまり関数を呼び出しても破壊されたりはしません。
- R20~R27 ( 8本) : 汎用ですが、関数ごとにローカルというわけではなく、グローバル変数的に使うことを想定しています。関数呼び出しによって変更される可能性もあります。
- R28~R2F ( 8本) : これも汎用ですが、関数ごとにローカルではなく、グローバル変数的に使われます。主にOSが用途を決定しています。これに対してR20~R27はアプリが自由に用途を決定できます。
- R30~R3B (12本) : 基本的にはこれらも汎用なのですが、関数の引数を渡したり、返値を入れたりするためにも使われるレジスタで、値が破壊されやすいです。ASKAでは複雑な数式を計算しなければいけなくなると、R3BやR3Aをテンポラリとして勝手に使い、値を破壊してしまうこともあります。R39やR38にまで手をつけることだってあります。しかし最悪でもR30までで、R2Fに手出しすることはありません。
- R3C~R3E ( 3本) : 将来の拡張のためにリザーブされています。
- R3F ( 1本) : 後で説明する特別な用途のための整数レジスタです。汎用には使えません。
- 全体として、OSECPUのレジスタはかなり多いほうだと思います。これはOSECPUがメモリ操作を苦手としていて、できるだけレジスタだけで主要な演算が完結できるようにという設計方針によるものです。
(2) フラグレジスタ
- x86でもARMでも、さらに6502やZ80でも、みんなフラグレジスタというものを持っていました。
- しかしOSECPUにはフラグレジスタはありません。MIPSの仕様に似ています。
- フラグレジスタがない代わりにCMPcc命令の結果に応じて任意の整数レジスタを0か-1に変更することができます。つまり普通のレジスタをフラグレジスタの代わりにしてしまったようなものです。
- これで設定された値を後述のCNDプリフィクス命令(04 Rxx)で使えば、条件分岐や条件付き代入などができます。
(3) 定数即値代入命令
[02] [Rxx] [imm32]
LIMM(Rxx, imm32);
- 6バイト命令です。Rxxには0x00から0x3fのレジスタ番号を指定します。imm32は定数即値を4バイトのビックエンディアンで指定します。
(4) 単純代入命令
[10] [reg0] [reg1] [FF]
CP(reg0, reg1);
- 4バイト命令です。Rxxフィールドが複数ある命令では、ここではreg0, reg1のように区別して表記することにします。
- reg1の値がreg0へコピーされます。
- reg0にR3Fを指定することはできません。reg1にもR3Fを指定してはいけません。
- この命令はバイトコード的には、OR命令でreg2にFFを指定した形式になっています。
- メモリの内容をコピーすることはこの命令ではできません。
(5) 三項演算命令
[10] [reg0] [reg1] [reg2]
OR(reg0, reg1, reg2);
(6) 整数比較命令
[20] [reg0] [reg1] [reg2]
CMPE(reg0, reg1, reg2);
- 4バイト命令です。reg1とreg2を比較して等しいかどうかを判定します。trueならreg0には-1が代入されます。falseなら0が代入されます。
- 他にも以下のような命令がこの形式になっています。
- 21 CMPNE (等しくなければtrue)
- 22 CMPL ( reg1 < reg2 が成立すればtrue)
- 23 CMPGE ( reg1 >= reg2 が成立すればtrue)
- 24 CMPLE ( reg1 <= reg2 が成立すればtrue)
- 25 CMPL ( reg1 > reg2 が成立すればtrue)
- 26 TSTZ ( reg1 & reg2 の演算結果が0ならtrue)
- 27 TSTNZ ( reg1 & reg2 の演算結果が0でなければtrue)
- 三項演算命令と同様にreg2に対してはR3Fの指定が可能です。reg1に対しては指定できません。
- そしてなんとreg0に対してもR3Fを指定することができるのですが、この場合は定数値
代入という意味ではなくて、特別な慣用句の指定を意味します。それについては後述します。とりあえずは「reg0にはR3Fを指定できない、指定したら意味が変わる」くらいに思っていてください。
- 基本的な整数演算命令はこれでおしまいです。少ないですね。
(7) 何もしない命令
[00]
NOP();
- 何もしない命令です。JITコンパイラはこの命令を翻訳しません(実CPUに対するNOP命令になるわけではない)。
- ここからしばらくは制御系の命令を紹介します。
(8) ラベル定義命令
[01] [opt] [imm32]
LB(opt, imm32);
- OSECPUでは分岐命令を実行する際には、絶対アドレスや相対アドレスなどを利用せずに、ラベル番号を指定する方式になっています。ということで、分岐先はこの命令を使ってラベル番号を定義しておかないといけません。imm32がラベル番号に相当します。
- 当然ですが、同じラベル番号を複数回使うことはできません。使えるのはひとつのアプリの中では1回だけです。
- DLLとアプリとでラベル番号がぶつかってしまうことに関しては問題ありません。
- optですが、今のところ指定できるのは0か1だけです。0は普通のラベル属性で、分岐先指定できます。1はpublic属性を意味して、分岐先に指定できるだけではなく、ポインタレジスタに代入したり、メモリに格納したりできます。逆に言えば、0属性のラベルは、ポインタレジスタやメモリに格納することができません。その代わり消費リソースは少なくてすみますので、なんでも全部1属性にするとかは勘弁してください。
- ラベル番号は本来は32ビットの任意の整数(負の数もOK)なのですが、現状の実装では0~4095までしか処理できません。すみません手抜きです。
(9) 無条件分岐命令
[03] [3F] [imm32]
PLIMM(P3F, imm32);
- OSECPUではP3Fというポインタレジスタに特別な意味があり、これに対してラベル番号を代入するとそこへ無条件分岐します。いわゆるgotoです。
- しつこいですがポインタレジスタについては後述します。とりあえず分岐先を保持できるレジスタだと思ってください。整数レジスタRxxとは別の存在なので、P02を使ったらR02が使えないとかそういうことはありません。
- PLIMMというのはLIMMのポインタレジスタ版で、ラベルの値をポインタレジスタに代入することができます。・・・とにかく詳細は後述します。
(10) 条件分岐命令など
[04] [Rxx] [03] [3F] [imm32]
CND(Rxx); PLIMM(P3F, imm32);
- これは条件分岐命令です。よく見ると無条件分岐命令の前にCNDプリフィクス命令がついているだけです。
- CNDプリフィクスには後続の1命令を条件実行命令化する作用があって、つまり後続のPLIMM命令が実行されたりされなかったりするのです。
- CNDプリフィクスはRxxの下位1bitしか参照しません。それが1だと後続の命令をそのまま実行します。0だと後続の命令は実行されません。
- CNDプリフィクスは他の命令、たとえばLIMMやADDの前につけることもできます。しかしCNDプリフィクスの前にCNDプリフィクスをつけることはできません。R3Fへの代入のためのLIMM命令にはCNDプリフィクスをつけられません(その代わり、その直後の演算命令にはつけられますので定数を含む命令の条件実効命令化は可能です)。
- 他にもいくつかCNDプリフィクスがつけられない命令があります。
- CNDプリフィクスのRxxに定数即値としてのR3Fを指定することはできません。CNDプリフィクスにR3Fを指定することそのものは不可能ではないのですが、その場合意味が変わります。
(11) 条件分岐慣用句
- OSECPU用アプリをいくつか作ってみてわかったことですが、
CMPxx(Rxx, ...); CND(Rxx); PLIMM(P3F, ...);
- のパターンが頻出します。比較して条件分岐ということです。そしてこの後でRxxの値を参照することもありません。純粋に条件分岐のためだけに一時的にRxxに値を入れているだけです。
- 一方でJITコンパイラは、CMP系の命令のときに、結構苦労して0や-1の値を生成してRxxに代入しています。CND命令の部分では一手間かけて最下位ビットを抽出しています。これは明らかに無駄です。結局正確な値なんていらなくて、直後の条件分岐さえ間違わずに実行してくれればそれでいいわけです。
- そうであれば、もっと単純なコードを生成できるので、そのことをJITコンパイラに教えてやりたいと思いました。ということで、以下のような特別な慣用句が用意されています。
CMPxx(R3F, ...); CND(R3F); PLIMM(P3F, ...);
- つまりRxxの部分が形式的にR3Fになっているのです。このR3Fは定数即値指定のR3Fと同じように見えますが、内部では完全に別なものとして扱われています。したがってCMPxx命令の第3項にR3Fを使用することもできますし、それで混同することはありません。そして以降の命令でR3Fを参照してもこのCMPxxの結果を得ることはできません(次のLIMM(R3F, ...); を実行するまでR3Fの値は保障されません)。
- この慣用句を使えば、条件分岐のためにわざわざ整数レジスタを一個使う必要もなくなります。
- なお、ANDやORを含むような複雑な条件では、この構文は使えません。本当にCMPxx, CND, PLIMMが連続で並んでいて、間に余計な命令が挟まっていないときだけ使えるのです。
if (R00 <= R02 & R02 <= R01) goto 3;
CMPLE(R30, R00, R02); CMPLE(R31, R02, R01); AND(R30, R30, R31); CND(R30); PLIMM(P3F, 3);
- CNDの直後はPLIMM(P3F, ...);でなければいけないので、分岐以外の構文でもこれは使えません。
- しかしそれでもこの構文は便利なので本当によく使います。CND命令を使うところの8割くらいはこれだと思います。
(12) ループの構成法
(13) 間接分岐命令
(14) 関数の呼び出し
(15) 関数の構成法
こめんと欄