page0043
の編集
http://osecpu.osask.jp/wiki/?page0043
[
トップ
] [
編集
|
差分
|
バックアップ
|
添付
|
リロード
] [
新規
|
一覧
|
単語検索
|
最終更新
|
ヘルプ
]
-- 雛形とするページ --
BracketName
FormattingRules
FrontPage
Fulyn
Fulyn-v2
Fulyn_Samples
Help
InterWiki
InterWikiName
InterWikiSandBox
K
KOR_PIT8254
KWVM.NET
Liva
MANA
MenuBar
OSECPU_FPGA
PG_MANA
PHP
PukiWiki
PukiWiki/1.4
PukiWiki/1.4/Manual
PukiWiki/1.4/Manual/Plugin
PukiWiki/1.4/Manual/Plugin/A-D
PukiWiki/1.4/Manual/Plugin/E-G
PukiWiki/1.4/Manual/Plugin/H-K
PukiWiki/1.4/Manual/Plugin/L-N
PukiWiki/1.4/Manual/Plugin/O-R
PukiWiki/1.4/Manual/Plugin/S-U
PukiWiki/1.4/Manual/Plugin/V-Z
RecentDeleted
SandBox
WikiEngines
WikiName
WikiWikiWeb
YukiWiki
hikalium
hikarupsp
hikarupsp_ELCHNOS
hikarupsp_ELCHNOS_IDE
hikarupsp_FrontEndCode
hikarupsp_WebCPU-VM
hikarupsp_WebCPU-VM_internal
hikarupsp_study_hh4
impressions
impressions0000
jpag0000
jpag0001
jpag0002
jpag0003
jpag0004
jpag0005
lambdalice
mandel59
members
memo0000
memo0001
memo0002
memo0003
memo0004
memo0005
memo0006
memo0007
memo0008
memo0009
memo0010
osask
osecpu4android
page0000
page0001
page0002
page0003
page0004
page0005
page0006
page0007
page0008
page0009
page0010
page0011
page0012
page0013
page0014
page0015
page0016
page0017
page0018
page0019
page0020
page0021
page0022
page0023
page0024
page0025
page0026
page0027
page0028
page0029
page0030
page0031
page0032
page0033
page0034
page0035
page0036
page0037
page0038
page0039
page0040
page0041
page0042
page0043
page0044
page0045
page0046
page0047
page0048
page0049
page0050
page0051
page0052
page0053
page0054
page0055
page0056
page0057
page0058
page0059
page0060
page0061
page0062
page0063
page0064
page0065
page0066
page0067
page0068
page0069
page0070
page0071
page0072
page0073
page0074
page0075
page0076
page0077
page0078
page0079
page0080
page0081
page0082
page0083
page0084
page0085
page0086
page0087
page0088
page0089
page0090
page0091
page0092
page0093
page0094
page0095
page0096
page0097
page0098
page0099
page0100
page0101
page0102
page0103
page0104
page0105
page0106
page0107
page0108
page0109
pagenames
populars
seccamp2013
seccamp2014
seccamp2017
ttwilb
ttwilb-asmi
yao
* OSECPUのバイトコードの詳細仕様 -(by [[K]], 2013.06.21) ** (0) はじめに -これは要するに[[page0032]]の詳細版です。 ** (1) 整数レジスタ -OSECPUは32bitの signed int な整数レジスタを64本持っています。実装仕様としては64bitで演算する場合も許されているので、最低でも精度が32bit分ある、ということだと思ってください。 -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でさえも、みんなフラグレジスタというものを持っていました。 --参考: http://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%83%AC%E3%82%B8%E3%82%B9%E3%82%BF -しかし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); -4バイト命令です。reg1とreg2がOR演算されて、結果がreg0に格納されます。演算結果に応じてフラグレジスタが変化するみたいな副作用はありません(そもそもフラグレジスタがないわけだけど)。 -他にも以下のような命令がこの形式になっています。 --11 XOR --12 AND --14 ADD --15 SUB --16 MUL 符号付き乗算 --18 SHL 左シフト --19 SAR 右シフト(符号付き) --1A DIV 符号付き除算 --1B MOD 符号付き剰余 -符号なしの演算は一切サポートされていません。OSECPUでは整数レジスタはすべて符号付きの32ビットだと仮定しているためです。 -INCやDECのような命令は持っていません。 -二項演算命令も持っていません。ADD(R00, R01);みたいなことがやりたいのであれば、ADD(R00, R00, R01);と書く必要があります。 -キャリーやボロー(桁あふれや桁借り)やオーバーフローを検出することはできないので、(必要なら)めんどうでも別の方法で検査する必要があります。 -OSECPUでは定数を用いた演算命令を用意していません。もしR00に1を加えたいのであれば、ADD(R00, R00,1);とは書けないので、この1をレジスタR01とかに入れておいて、ADD(R00, R00, R01);としなければいけません。これは少々不便です。 -これに関連しますが、整数レジスタを定数の代わりに使う場合、R3Fレジスタを利用することができます。 LIMM(R3F, 1); ADD(R00, R00, R3F); -R01などではなくR3Fを使うメリットですが、OSECPUはR3Fが使われると「これは定数に展開していいのだな」と判断し、R3F部分を定数に展開してJITコンパイルします。結果としてR01を経由するよりも高速に実行されることになります。 -このようにR3Fは定数即値を指定するための整数レジスタということです。R3Fに値を代入できるのはLIMM命令だけで、演算結果などを保持することはできません。またR3Fを指定できるのは、これらの命令群のreg2の部分だけで、reg1やreg0の部分にはR3Fを指定することができません。 --例外として、SUB,SHL,SAR,DIV,MODに関してはreg1にR3Fを使用してもかまいません。しかしその場合はreg2にはR3Fが使用できません。 -もしかしたらADD(R00, R00, R00);のように、reg0とreg1やreg2に同じものを指定してしまって平気だろうかと心配になるかもしれません。でも心配無用です。大丈夫です。 --この場合、R00 = R00 + R00; ですから、R00は問題なく2倍になります。 ** (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 CMPG ( reg1 > reg2 が成立すればtrue) --26 TSTZ ( reg1 & reg2 の演算結果が0ならtrue) --27 TSTNZ ( reg1 & reg2 の演算結果が0でなければtrue) -三項演算命令と同様にreg2に対してはR3Fの指定が可能です。reg1に対しては指定できません。 -そしてなんとreg0に対してもR3Fを指定することができるのですが、この場合は定数値 代入という意味ではなくて、特別な慣用句の指定を意味します。それについては後述します。とりあえずは「reg0にはR3Fを指定できない、指定したら意味が変わる」くらいに思っていてください。 -これはx86限定の豆知識ですが、20~25の命令については、(5)で値を演算した直後で、かつ、20~25の命令のreg2がR3Fでしかもそれが0なら、少々高速化されます。 R00--; if (R00 > 0) goto ...; // これは高速化される. R00--; if (R00 >= 1) goto ...; // これは高速化されない. --何が言いたいのかというと、可能ならできるだけ0と比較しましょうということです。 --まあ些細なことですけどね。 -基本的な整数演算命令はこれでおしまいです。少ないですね。 ** (7) 何もしない命令 [00] NOP(); -1バイト命令です。何もしない命令です。JITコンパイラはこの命令を翻訳しません(実CPUに対するNOP命令になるわけではない)。 -ここからしばらくは制御系の命令を紹介します。 ** (8) ラベル定義命令 [01] [opt] [imm32] LB(opt, imm32); -6バイト命令です。OSECPUでは分岐命令を実行する際には、絶対アドレスや相対アドレスなどを利用せずに、ラベル番号を指定する方式になっています。ということで、分岐先はこの命令を使ってラベル番号を定義しておかないといけません。imm32がラベル番号に相当します。 -当然ですが、同じラベル番号を複数回使うことはできません。使えるのはひとつのアプリの中では1回だけです。 --DLLとアプリとでラベル番号がぶつかってしまうことに関しては問題ありません。 -optですが、今のところ指定できるのは0か1だけです。0は普通のラベル属性で、分岐先指定できます。1はpublic属性を意味して、分岐先に指定できるだけではなく、ポインタレジスタに代入したり、メモリに格納したりできます。逆に言えば、0属性のラベルは、ポインタレジスタやメモリに格納することができません。その代わり消費リソースは少なくてすみますので、なんでも全部1属性にするとかは勘弁してください。 --ポインタレジスタについては後述します。 -ラベル番号は本来は32ビットの任意の整数(負の数もOK)なのですが、現状の実装では0~4095までしか処理できません。すみません手抜きです。 ** (9) 無条件分岐命令 [03] [3F] [imm32] PLIMM(P3F, imm32); -6バイト命令です。OSECPUではP3Fというポインタレジスタに特別な意味があり、これに対してラベル番号を代入するとそこへ無条件分岐します。いわゆるgotoです。 --しつこいですがポインタレジスタについては後述します。とりあえず分岐先を保持できるレジスタだと思ってください。整数レジスタRxxとは別の存在なので、P02を使ったらR02が使えないとかそういうことはありません。 -PLIMMというのはLIMMのポインタレジスタ版で、ラベルの値をポインタレジスタに代入することができます。・・・とにかく詳細は後述します。 ** (10) 条件分岐命令など [04] [Rxx] [03] [3F] [imm32] CND(Rxx); PLIMM(P3F, imm32); -これは条件分岐命令です。よく見ると無条件分岐命令の前にCNDプリフィクス命令がついているだけです。CND命令は2バイトで、PLIMM命令は6バイトなので、このフレーズは合計8バイトです。 -CNDプリフィクスには後続の1命令を条件実行命令化する作用があって、つまり後続のPLIMM命令が実行されたりされなかったりするのです。 -CNDプリフィクスはRxxの下位1bitしか参照しません。それが1だと後続の命令をそのまま実行します。0だと後続の命令は実行されません。 --この仕様は注意を要するかもしれません。Cなどでは値が非零であればtrueとみなすという仕様ですが、それとは違うわけです。たとえばRxxの値が2だったりすると、これはfalseとみなされてPLIMMは実行されません。 --もしCみたいに非零がどうかで条件分岐したいのであれば、 TSTNZ(R30, Rxx, Rxx); CND(R30); PLIMM(P3F, imm32); --みたいにするといいと思います。つまりRxxが非零であるかどうかを適当な方法で検査してその結果を整数レジスタに入れて、そしてその値でCNDするというわけです。 -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) ループの構成法 -まず、以下のようにすれば簡単に無限ループが構成できます。 LB(0, 123); // 123である必然性はない. ... PLIMM(P3F, 123); -これで...の中は無限に繰り返されることになります。 ~ -次に上記のLB命令の直後に適当な条件式を加えて、それが成立した場合に、PLIMMの直後へ分岐させてみます。 LB(0, 123); CMPE(R3F, R00, R01); CND(R3F); PLIMM(P3F, 124); ... PLIMM(P3F, 123); LB(0, 124); -これはつまり while(R00 != R01) { ... } というループです。ループはいる前に初期値を設定したり、ループの末尾からジャンプする前に更新式を入れれば、forループにもなります。 -途中でbreakしたいときは、いつでも PLIMM(P3F, 124); すればいいことになります。 ~ -ループ末尾のジャンプ命令を、条件付きに変更すると、do { ... } while (R00 != R01); にもできます。 LB(0, 123); ... CMPNE(R3F, R00, R01); CND(R3F); PLIMM(P3F, 123); ** (13) 間接分岐命令 -間接分岐命令というのは、P3Fにラベル番号を直接入れるのではなくて、レジスタを使って分岐先を指定する方法です。 -たとえばこういうことができます。 PLIMM(P01, 1); PCP(P3F, P01); -このPCPは P3F = P01; を実行するための命令で、ポインタコピーです。命令フォーマットは、 [1E] [preg0] [preg1] -となっていて、 preg0 = preg1; になります。なお、P3Fをpreg1に指定することはできません。PCPは3バイト命令です。 -PCPでP3Fに代入すると、それが間接分岐命令になるというわけです。 ** (14) 関数の呼び出し -OSECPUにおいて、関数の呼び出しとは、R30~R3Bに適当な引数を格納して(さらに必要ならP30~P3Bにも適当な引数を格納して)、目的の関数へただPLIMM(P3F, ...);するだけです。もしくはPCP(P3F, Pxx); するだけです。 -しかしこれでは呼び出し元へ帰ってくることができません。これじゃあCALLではなくてJMPです。 -ということで、関数呼び出しの際には必ずどこに帰ってきてほしいのかをP30に代入してから呼び出します。 ... PLIMM(P30, 3); PLIMM(P3F, 関数のラベル番号); LB(1, 3); ... -ここでは帰ってくる場所のラベルを3にしましたが、もちろん4でも10でもなんでもいいです。 -ちなみに関数のほうでは、処理が終わって呼び出し元にreturnしたくなったら、PCP(P3F, P30); を実行しているだけです。 -こういうことになっているので、ポインタ引数を渡したいときはP30は使えなくてP31~P3Bを使うことになります。 ** (15) 関数の構成法 -関数の記述は基本的に次の形式でやってください。 [01] [01] [imm32] [FE] [01] [00] [3C] [00] [20] [20] [00] [00] [00] ... 関数の処理内容 ... [3D] [00] [20] [20] [00] [00] [00] [1E] [3F] [30] -imm32の部分は関数のラベル番号です。 ~ -[01] [01] [imm32] : これはラベル宣言命令なので特に説明はなくてもわかるでしょう。関数を表すラベルの属性は、レジスタに代入することがなくても1にすることになっています。 -[FE] [01] [00] : これは DEBUG-INFO-1 と呼ばれている3バイトのリマーク命令で、詳細は「(17) APIの呼び出し(1)」で確認してください。 -[3C] [00] [20] [20] [00] [00] [00] : この7バイト命令が、R00~R1FとP00~P1FとP30の退避処理を行っています。20の部分を変更したり、00の部分を変更したら、なにかいろいろできそうな気がしてくると思いますが、現在のバージョンでは他の数字の組み合わせを想定できていないので、どうかこのままで使ってください。関数宣言以外のところでこの命令を使うことも想定されていません。 -[3D] [00] [20] [20] [00] [00] [00] : この7バイト命令は上記で退避したレジスタを復元します。関数終了部分以外では使わないでください。この命令は、関数の末尾にしか置けません。したがって、関数処理の途中で関数から抜けたくなったら、関数処理の末尾へジャンプする必要があります。 -[1E] [3F] [30] : PCP(P3F, P30); です。これで呼び出し元に帰ることができます。 ** (16) 条件処理の構成法 ** (17) APIの呼び出し(1) -ここではR30~R3Bしか引数に使わないAPIの呼び出し方を紹介したいと思います。いきなり全部説明しても混乱するだけなので。 -この方法で呼び出せるAPIは次の10個です。この表に出てくる123というラベル番号はもちろん仮のものです。 drawPoint #02: [FE] [05] [01] [0002(32bit)] R30=0xff44; R31=mode; R32=x; R33=y; R34=col; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] drawLine #03: [FE] [05] [01] [0003(32bit)] R30=0xff45; R31=mode; R32=x0; R33=y0; R34=x1; R35=y1; R36=col; 以下略 fillRect #04: [FE] [05] [01] [0004(32bit)] R30=0xff46; R31=mode; R32=xsiz; R33=ysiz; R34=x0; R35=y0; R36=col; 以下略 fillOval #05: [FE] [05] [01] [0005(32bit)] R30=0xff47; R31=mode; R32=xsiz; R33=ysiz; R34=x0; R35=y0; R36=col; 以下略 exit #08: [FE] [05] [01] [0008(32bit)] R30=0xff06; R31=i; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] sleep #09: [FE] [05] [01] [0009(32bit)] R30=0xff42; R31=mode; R32=msec; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] inkey #0d: [FE] [05] [01] [000d(32bit)] R30=0xff43; R31=mode; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] openWin #10: [FE] [05] [01] [0010(32bit)] R30=0xff40; R31=xsiz; R32=ysiz; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] flushWin #11: [FE] [05] [01] [0011(32bit)] R30=0xff41; R31=xsiz; R32=ysiz; R33=x0; R34=y0; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] rand #13: [FE] [05] [01] [0013(32bit)] R30=0xff49; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] -まず前後の [FE] [05] [01] [00??(32bit)] と [FE] [01] [00] を完全に無視してください(あとで説明します)。これらさえなければ、もう知っている命令ばかりです。基本的にはR30に機能番号を入れて、R31以降にパラメータを入れて、それでP28に格納されているラベルへ関数呼び出ししているだけです。 -P28の値はOS(もしくはVM)が設定した状態でアプリを起動しますので、その値を壊さないようにしてください。壊してしまったら(=どこかに控えることもしないで上書きしてしまったら)、もうAPIを呼び出す手段は残っていません。 -inkeyとrandについては、結果がR30に格納されて戻ってきますので、CP命令で適当なレジスタに代入してください。 -さっき無視してもらったFEで始まる命令群ですが、これはリマーク命令なので、本当に無視してバイトコード内に含めないという選択肢もありです。仮にそうしても、おそらく動作に問題はありません。・・・ってこれで説明を終わるとさすがにひどいので、ちゃんと説明します。 ~ -リマーク命令というのは、本来の実行には直接関係のない補助情報です。必ず [FE] [len] で始まります。lenの部分は1バイトで、ここで追加情報の長さをバイト単位で表しています。 -[FE] [01] [00] は通称 DEBUG-INFO-1 と呼ばれている3バイトのリマーク命令で、 LB(1, ...); の直後に置きます。これは「実行しているモジュールが切り替わった(もしくは切り替わったかもしれない)」ことを示しています。どういうことかというと、この文脈では、API呼び出ししてOSやライブラリに制御が移った後、ここへ PCP(P3F, P30); で戻ってくるはずなのです。 -この DEBUG-INFO-1 は、もしエラーが起きた場合に、エラーがアプリの中で起きたのか、システムの中で起きたのか、そういうのを管理するためのもので、デバッグレジスタに「今実行しているのはアプリ内」という設定をするためのコードをもし入れるとしたら、それはここですよー、というヒントをOSECPUに教えているのです。 -裏返せば、もし絶対にバグがないのならこんな情報はあってもなくても同じことです。というかないほうがむしろ高速ですね。・・・しかしOSECPUはセキュアなOSを目指していることもあって、これはぜひとも省略してほしくないのです。 LB(1, ...); のあとには是非 [FE] [01] [00] をおいてあげましょう。 --これを置き忘れると、エラー扱いになって実行を拒否するモードを実装する可能性も少しあります。・・・すみません、ちょっと脅かしてみただけです(笑)。なんといってもこれは所詮リマーク命令なので、なくてもいい存在なのです。 -次は [FE] [05] [01] [imm32] の7バイトのリマーク命令です。これは「以下の記述はAPIの呼び出しだよ」というヒントになっています。これがないと、どこから引数代入のための命令が始まっているのかの頭出しが面倒極まりなくて、それで書いています。imm32にはAPIの表向きの機能番号を書きます。R30に入っている番号は内部の汚い番号です。 -このリマーク命令の後には、R30からレジスタ番号順にLIMM命令かCP命令が並んでないといけません。他の命令でR3xに代入してはいけませんし、他の演算命令もダメです。そういうことがしたいならしてもいいですが、そのときはこのリマーク命令をつけないでください。期待したフォーマットと違うと、osectolsが混乱してしまいます。 -値を返すAPIの場合は [FE] [01] [00] の直後に CP(Rxx, R30); のコードも必要です。これを全部含めてフォーマットがそろっていることを [FE] [05] [01] [imm32] は保証しているのです。 -なんかめんどくさいなーと思ったら(確かにめんどくさい)、 [FE] [05] [01] [imm32] は取っちゃっていいと思います。 ** (18) ポインタレジスタ -ここからしばらくはメモリアクセス関係の命令を紹介します。 ~ -OSECPUはメモリアドレスを指し示すためのポインタレジスタを64本持っています。中のビット数は実装依存です。 -P00~P3F と表記します。 -P00やP01など番号の若いレジスタは、実際のCPUの実レジスタに割り当てるように推奨されているので、P00への代入やP00の値の参照は、P20に対する操作と比べて高速になることが多いです。 --結局どうなるかは処理系依存で、保障はされていません。 -ということで、特にこだわりがないのならP00やP01を使いましょう。 --ちなみにx86版では、P00~P02までが実レジスタに割り当てられていて、残りはメモリを使ってレジスタをエミュレーションしています。 ~ -これらのレジスタはすべて対等で汎用的に使われるというわけではなく、ある程度の使い方が決まっています。これに逆らってはいけないということはないですが、ライブラリなどで食い違うといろいろ面倒かもしれません。 --P00 ( 1本) : ベースポインタです。つまり汎用ではありません。今はベースポインタのサポートが不十分なので、使えないということと同義です。 --P01~P1F (31本) : 最も汎用的なポインタレジスタで、通常は関数ごとにローカルとして扱えます。つまり関数を呼び出しても破壊されたりはしません。 --P20~P27 ( 8本) : 汎用ですが、関数ごとにローカルというわけではなく、グローバル変数的に使うことを想定しています。関数呼び出しによって変更される可能性もあります。 --P28~P2F ( 8本) : これも汎用ですが、関数ごとにローカルではなく、グローバル変数的に使われます。主にOSが用途を決定しています。これに対してP20~P27はアプリが自由に用途を決定できます。 --P30~P3B (12本) : 基本的にはこれらも汎用なのですが、関数の引数を渡したり、返値を入れたりするためにも使われるレジスタで、値が破壊されやすいです。ASKAでは複雑な数式を計算しなければいけなくなると、P3BやP3Aをテンポラリとして勝手に使い、値を破壊してしまうこともあります。P39やP38にまで手をつけることだってあります。しかし最悪でもP30までで、P2Fに手出しすることはありません。 --P3C~P3E ( 3本) : 将来の拡張のためにリザーブされています。 --P3F ( 1本) : 「(9) 無条件分岐命令」などで説明した特別な用途のためのポインタレジスタです。汎用には使えません。 -全体として、OSECPUのレジスタはかなり多いほうだと思います。これはOSECPUがメモリ操作を苦手としていて、できるだけレジスタだけで主要な演算が完結できるようにという設計方針によるものです。 ** (19) データ記述命令 -プログラム内にデータを置くことができます。これらのデータはいわゆる.dataに置かれるもので、プログラムの実行中に書き換えることも許されます。 -この命令そのものは何もしないので、プログラムの実行コード中に混在させて構いません(データを実行してしまうような事故は起きません)。 -(つづく) ** (20) ラベル番号代入命令 [03] [Pxx] [imm32] PLIMM(Pxx, imm32); -6バイト命令です。Pxxには0x00から0x3fのレジスタ番号を指定します。imm32はラベル番号の定数即値を4バイトのビックエンディアンで指定します。 ** (21) メモリへのアクセス命令 -まず、以下に示す命令は、Pxxがデータをポイントしているときにのみ有効で、それ以外の状況で使用すれば、即刻セキュリティ違反でアプリの実行は停止させられます。 --プログラムの分岐先などを指している場合はダメだということです。 -また型が一致しない場合もセキュリティ違反になります。 [08] [Rxx] [typ32] [Pxx] [00] LMEM(Rxx, typ32, Pxx, 0); -これはメモリからデータを読んでRxxに格納する命令で、8バイト命令です。末尾の00は仕様上は他の値にもできるのですが、現在はその機能は未実装なので、00のみ使えます。 -基本的にこの命令以外でメモリからデータを読み込む命令はありません。 -つまりこの命令でレジスタに値を持ってきて、そしてレジスタを使って演算していくことになります。 -メモリアドレッシングはPxxの単独指定しかありません。これは一般的なCPUと比較すれば非常に貧弱です。PADDなどを使ってほしい位置まで動かしたポインタレジスタを使って、アクセスすることになります。 ~ [09] [Rxx] [typ32] [Pxx] [00] SMEM(Rxx, typ32, Pxx, 0); -こちらはレジスタの値をメモリに書くための命令で、やはり8バイト命令です。 -基本的にこの命令以外でメモリへデータを書き込む命令はありません。 ** (22) ポインタ演算命令 -ポインタレジスタには整数を加えることができます。これは配列に有効な演算で、1を足せば次の要素を指しますし、-1を足せば前の要素を指します。もちろん2や-3を加えることもできます。 [0E] [preg0] [typ32] [preg1] [Rxx] PADD(preg0, typ32, preg1, Rxx); -8バイト命令です。 preg0 = preg1 + Rxx; です。計算結果が配列の外になってもかまいませんが、そのようなポインタは逆方向の値を加算して配列の中に戻らない限り、LMEMやSMEMは実行できません。 -この命令の前にLIMM(R3F, ...);を置くのであれば、Rxxの部分でR3Fを指定することが可能です。 ~ -同じ配列を指し示す2つのポインタレジスタに対しては、差を取ることができます。 [0F] [Rxx] [typ32] [preg0] [preg1] PDIF(Rxx, typ32, preg0, preg1); -8バイト命令です。 Rxx = preg0 - preg1; です。ポインタレジスタが異なる配列に属しているときは、この演算はセキュリティ保護エラーとなりアプリが停止します。 -同じ配列に属するというのは、単に型が一致しているだけではダメで、同じタイミングでアロケートされたものである必要があります。 ** (23) ポインタ比較命令 -2つのポインタに対して、PDIFが計算可能であるような組み合わせであれば、PCMP系の命令が実行できて、結果に応じてRxxが-1か0になります。 -(つづく) ** (24) データの型 ** (25) PALMEM命令など -たとえばP01が配列の先頭を指していれば、 PADD(P30, T_UINT32, P01, R00); LMEM0(R01, T_UINT32, P30); -とすることで、配列のR00番目の値をR01にとってくることができます( R00 = P01[R00]; )。これはよく使うので結構便利なフレーズです。 -この方法の副作用としては、P30というレジスタを一時的に使うことで、値が壊れてしまうことです。まあでもOSECPUはレジスタがたくさんあるので、この副作用はそんなには深刻ではありません。 -しかしそれでも副作用がないのならないほうがいいので、こんな命令を用意しています。 PALMEM0(R01, T_UINT32, P01, R00); [0E] [3F] [T_UINT32] [P01] [R00] [08] [R01] [T_UINT32] [3F] [00] -もちろんP01やR00やR01の部分は他のレジスタに置き換えてもOKです。 -バイトコードのほうを見ると、これは結局PADD命令とLMEM命令でしありません。しかしなんとテンポラリレジスタにP3Fを指定した形になっています。もちろんP3Fのこんな用法は許されていません。許されていないからこそ、JITコンパイラはこれがPALMEM命令であると認識することができます。 ~ -同様にPASMEMやPAPLMEMやPAPSMEM命令があります。 ** (26) malloc命令 [32] [Pxx] [reg0] [reg1] -メモリをヒープ領域からアロケートします。reg0には要求している型を入れます。たとえばT_SINT32を要求しているのなら6を代入しておいてください。つまり(他の命令とは異なり)型を定数ではなくレジスタで指定しているのです。 -reg1にはその型をいくつ分ほしいのかを指定します。やはりレジスタで。 -そしてPxxにポインタが返ります。 -たとえば、T_SINT32が100個分の配列を確保したいのであれば、こうします。 [02] [30] [00000006] [02] [31] [00000064] [32] [01] [30] [31] -これでP01にその配列の最初の要素を指したポインタが代入されます。 ~ -mallocで確保したメモリ域を使い終わったら、システムに返す必要があります。OSECPUではガーベージコレクションのようなものは一切ありません。 [33] [Pxx] [3F] [3F] -このとき指定するPxxはmallocで得たものをそのまま使ってください。 -なお、アプリが終了すればmallocで確保したメモリは全て自動で回収されるので、終了直前に律儀にfreeして回る必要はありません。 ** (27) talloc命令 [30] [Pxx] [reg0] [reg1] -メモリをスタック領域からアロケートすることを除けば、上記のmalloc命令と同じです。 -解放については [31] [3F] [3F] [3F] -を使います。関数の中でスタック域に確保したメモリは関数から出る前に''責任を持って解放しなければいけません''。またその際の解法順序は、LIFO(もしくはFILO)でなければいけません。他の関数に開放してもらうように頼むことは''できません''。 ** (28) APIの呼び出し(2) -ポインタレジスタを引数に含むAPIについて、呼び出し方を紹介します。 -(つづく) ** (29) 擬似構造体 -OSECPUは現在構造体をサポートできていません。開発者側の時間的な制約によって、構造体のサポートは少なくとも1年はかかるでしょう。OSECPUにおいて構造体のサポートは大変深い意味を持っていて、だからこそ構造体をバイトコードレベルでサポートするのですが(たとえばx86やARMには構造体をサポートする命令などない)、それゆえに設計には細心の注意を要して、すぐには作れそうにないのです。 -しかしだからといって、構造体的なことが全くできないわけではありません。配列があれば構造体の代わりはできるのです。 -構造体というのは、雑な定義でよければ、いくつかのメンバ変数を持った存在だということができます。 struct Abc { int i, a[10]; unsigned char c, d; }; -これを配列で代用することを考えます。まず配列は要素が全て同じ型でなければいけないので、この中で一番強い型を探します。この場合はintです。つまりintはunsigned charの全ての値を表現できるので代用できると考えるわけです。メモリはもちろんムダになりますが、そこには眼をつむります。 -結局、T_SINT32が13個あればいいのです。だからタイプT_SINT32で要素数13の配列を作ってそれを使ってください。それだけです。 -もしAbcの配列を作る必要があれば、その個数を掛け算してください。 Abc abc[100]; の代用なら1300でいいわけです。 ~ -この方法には限界があります。それは、ポインタ型はintと互換性がないので、構造体内にintとポインタが入っているとうまく表現できないということです。それについてはどうしようもないので、構造体を二つに分けます。 struct Node { int data; Node *left, *right; }; -たとえばこんなものがあったとしたら、これを2つの構造体に分けてしまいます。 struct NodeInt { // int系のメンバを集めたもの. int data; }; struct NodePtr { NodeInt *leftInt, *rightInt; NodePtr *leftPtr, *rightPtr; }; -こうしてしまった上で、配列を二つ作ります。 NodeInt *nodeInt = malloc(...); NodePtr *nodePtr = malloc(...); -こうしてしまえば、node[3].dataにアクセスしたいときはnodeInt[3].dataを、node[3].leftにアクセスしたいときはnodePtr[3].leftIntとnodePtr[3].leftPtrの両方にアクセスすればいい事になります。 ~ -もしくはもっといい方法もあります。ポインタなんて結局は配列の中の要素をさすのですから、ポインタ値ではなくて添え字を覚えるのです。この方法ならみんなintになります。 struct Node { int data; int leftIdx, rightIdx; }; ** (30) -(つづく) * こめんと欄 -(6) 整数比較命令 25 CMPLでなくCMPGだと思います。 -- [[hikarupsp]] SIZE(10){2013-08-04 (日) 11:53:12} -(19) データ記述命令のオペコードは多分[34][type32][length32][data...]だと思います。 -- [[hikarupsp]] SIZE(10){2013-08-05 (月) 06:12:55} -そのとおりです。直しました。>CMPG -- ''K'' SIZE(10){2013-08-05 (月) 11:01:40} #comment
タイムスタンプを変更しない
* OSECPUのバイトコードの詳細仕様 -(by [[K]], 2013.06.21) ** (0) はじめに -これは要するに[[page0032]]の詳細版です。 ** (1) 整数レジスタ -OSECPUは32bitの signed int な整数レジスタを64本持っています。実装仕様としては64bitで演算する場合も許されているので、最低でも精度が32bit分ある、ということだと思ってください。 -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でさえも、みんなフラグレジスタというものを持っていました。 --参考: http://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%83%AC%E3%82%B8%E3%82%B9%E3%82%BF -しかし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); -4バイト命令です。reg1とreg2がOR演算されて、結果がreg0に格納されます。演算結果に応じてフラグレジスタが変化するみたいな副作用はありません(そもそもフラグレジスタがないわけだけど)。 -他にも以下のような命令がこの形式になっています。 --11 XOR --12 AND --14 ADD --15 SUB --16 MUL 符号付き乗算 --18 SHL 左シフト --19 SAR 右シフト(符号付き) --1A DIV 符号付き除算 --1B MOD 符号付き剰余 -符号なしの演算は一切サポートされていません。OSECPUでは整数レジスタはすべて符号付きの32ビットだと仮定しているためです。 -INCやDECのような命令は持っていません。 -二項演算命令も持っていません。ADD(R00, R01);みたいなことがやりたいのであれば、ADD(R00, R00, R01);と書く必要があります。 -キャリーやボロー(桁あふれや桁借り)やオーバーフローを検出することはできないので、(必要なら)めんどうでも別の方法で検査する必要があります。 -OSECPUでは定数を用いた演算命令を用意していません。もしR00に1を加えたいのであれば、ADD(R00, R00,1);とは書けないので、この1をレジスタR01とかに入れておいて、ADD(R00, R00, R01);としなければいけません。これは少々不便です。 -これに関連しますが、整数レジスタを定数の代わりに使う場合、R3Fレジスタを利用することができます。 LIMM(R3F, 1); ADD(R00, R00, R3F); -R01などではなくR3Fを使うメリットですが、OSECPUはR3Fが使われると「これは定数に展開していいのだな」と判断し、R3F部分を定数に展開してJITコンパイルします。結果としてR01を経由するよりも高速に実行されることになります。 -このようにR3Fは定数即値を指定するための整数レジスタということです。R3Fに値を代入できるのはLIMM命令だけで、演算結果などを保持することはできません。またR3Fを指定できるのは、これらの命令群のreg2の部分だけで、reg1やreg0の部分にはR3Fを指定することができません。 --例外として、SUB,SHL,SAR,DIV,MODに関してはreg1にR3Fを使用してもかまいません。しかしその場合はreg2にはR3Fが使用できません。 -もしかしたらADD(R00, R00, R00);のように、reg0とreg1やreg2に同じものを指定してしまって平気だろうかと心配になるかもしれません。でも心配無用です。大丈夫です。 --この場合、R00 = R00 + R00; ですから、R00は問題なく2倍になります。 ** (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 CMPG ( reg1 > reg2 が成立すればtrue) --26 TSTZ ( reg1 & reg2 の演算結果が0ならtrue) --27 TSTNZ ( reg1 & reg2 の演算結果が0でなければtrue) -三項演算命令と同様にreg2に対してはR3Fの指定が可能です。reg1に対しては指定できません。 -そしてなんとreg0に対してもR3Fを指定することができるのですが、この場合は定数値 代入という意味ではなくて、特別な慣用句の指定を意味します。それについては後述します。とりあえずは「reg0にはR3Fを指定できない、指定したら意味が変わる」くらいに思っていてください。 -これはx86限定の豆知識ですが、20~25の命令については、(5)で値を演算した直後で、かつ、20~25の命令のreg2がR3Fでしかもそれが0なら、少々高速化されます。 R00--; if (R00 > 0) goto ...; // これは高速化される. R00--; if (R00 >= 1) goto ...; // これは高速化されない. --何が言いたいのかというと、可能ならできるだけ0と比較しましょうということです。 --まあ些細なことですけどね。 -基本的な整数演算命令はこれでおしまいです。少ないですね。 ** (7) 何もしない命令 [00] NOP(); -1バイト命令です。何もしない命令です。JITコンパイラはこの命令を翻訳しません(実CPUに対するNOP命令になるわけではない)。 -ここからしばらくは制御系の命令を紹介します。 ** (8) ラベル定義命令 [01] [opt] [imm32] LB(opt, imm32); -6バイト命令です。OSECPUでは分岐命令を実行する際には、絶対アドレスや相対アドレスなどを利用せずに、ラベル番号を指定する方式になっています。ということで、分岐先はこの命令を使ってラベル番号を定義しておかないといけません。imm32がラベル番号に相当します。 -当然ですが、同じラベル番号を複数回使うことはできません。使えるのはひとつのアプリの中では1回だけです。 --DLLとアプリとでラベル番号がぶつかってしまうことに関しては問題ありません。 -optですが、今のところ指定できるのは0か1だけです。0は普通のラベル属性で、分岐先指定できます。1はpublic属性を意味して、分岐先に指定できるだけではなく、ポインタレジスタに代入したり、メモリに格納したりできます。逆に言えば、0属性のラベルは、ポインタレジスタやメモリに格納することができません。その代わり消費リソースは少なくてすみますので、なんでも全部1属性にするとかは勘弁してください。 --ポインタレジスタについては後述します。 -ラベル番号は本来は32ビットの任意の整数(負の数もOK)なのですが、現状の実装では0~4095までしか処理できません。すみません手抜きです。 ** (9) 無条件分岐命令 [03] [3F] [imm32] PLIMM(P3F, imm32); -6バイト命令です。OSECPUではP3Fというポインタレジスタに特別な意味があり、これに対してラベル番号を代入するとそこへ無条件分岐します。いわゆるgotoです。 --しつこいですがポインタレジスタについては後述します。とりあえず分岐先を保持できるレジスタだと思ってください。整数レジスタRxxとは別の存在なので、P02を使ったらR02が使えないとかそういうことはありません。 -PLIMMというのはLIMMのポインタレジスタ版で、ラベルの値をポインタレジスタに代入することができます。・・・とにかく詳細は後述します。 ** (10) 条件分岐命令など [04] [Rxx] [03] [3F] [imm32] CND(Rxx); PLIMM(P3F, imm32); -これは条件分岐命令です。よく見ると無条件分岐命令の前にCNDプリフィクス命令がついているだけです。CND命令は2バイトで、PLIMM命令は6バイトなので、このフレーズは合計8バイトです。 -CNDプリフィクスには後続の1命令を条件実行命令化する作用があって、つまり後続のPLIMM命令が実行されたりされなかったりするのです。 -CNDプリフィクスはRxxの下位1bitしか参照しません。それが1だと後続の命令をそのまま実行します。0だと後続の命令は実行されません。 --この仕様は注意を要するかもしれません。Cなどでは値が非零であればtrueとみなすという仕様ですが、それとは違うわけです。たとえばRxxの値が2だったりすると、これはfalseとみなされてPLIMMは実行されません。 --もしCみたいに非零がどうかで条件分岐したいのであれば、 TSTNZ(R30, Rxx, Rxx); CND(R30); PLIMM(P3F, imm32); --みたいにするといいと思います。つまりRxxが非零であるかどうかを適当な方法で検査してその結果を整数レジスタに入れて、そしてその値でCNDするというわけです。 -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) ループの構成法 -まず、以下のようにすれば簡単に無限ループが構成できます。 LB(0, 123); // 123である必然性はない. ... PLIMM(P3F, 123); -これで...の中は無限に繰り返されることになります。 ~ -次に上記のLB命令の直後に適当な条件式を加えて、それが成立した場合に、PLIMMの直後へ分岐させてみます。 LB(0, 123); CMPE(R3F, R00, R01); CND(R3F); PLIMM(P3F, 124); ... PLIMM(P3F, 123); LB(0, 124); -これはつまり while(R00 != R01) { ... } というループです。ループはいる前に初期値を設定したり、ループの末尾からジャンプする前に更新式を入れれば、forループにもなります。 -途中でbreakしたいときは、いつでも PLIMM(P3F, 124); すればいいことになります。 ~ -ループ末尾のジャンプ命令を、条件付きに変更すると、do { ... } while (R00 != R01); にもできます。 LB(0, 123); ... CMPNE(R3F, R00, R01); CND(R3F); PLIMM(P3F, 123); ** (13) 間接分岐命令 -間接分岐命令というのは、P3Fにラベル番号を直接入れるのではなくて、レジスタを使って分岐先を指定する方法です。 -たとえばこういうことができます。 PLIMM(P01, 1); PCP(P3F, P01); -このPCPは P3F = P01; を実行するための命令で、ポインタコピーです。命令フォーマットは、 [1E] [preg0] [preg1] -となっていて、 preg0 = preg1; になります。なお、P3Fをpreg1に指定することはできません。PCPは3バイト命令です。 -PCPでP3Fに代入すると、それが間接分岐命令になるというわけです。 ** (14) 関数の呼び出し -OSECPUにおいて、関数の呼び出しとは、R30~R3Bに適当な引数を格納して(さらに必要ならP30~P3Bにも適当な引数を格納して)、目的の関数へただPLIMM(P3F, ...);するだけです。もしくはPCP(P3F, Pxx); するだけです。 -しかしこれでは呼び出し元へ帰ってくることができません。これじゃあCALLではなくてJMPです。 -ということで、関数呼び出しの際には必ずどこに帰ってきてほしいのかをP30に代入してから呼び出します。 ... PLIMM(P30, 3); PLIMM(P3F, 関数のラベル番号); LB(1, 3); ... -ここでは帰ってくる場所のラベルを3にしましたが、もちろん4でも10でもなんでもいいです。 -ちなみに関数のほうでは、処理が終わって呼び出し元にreturnしたくなったら、PCP(P3F, P30); を実行しているだけです。 -こういうことになっているので、ポインタ引数を渡したいときはP30は使えなくてP31~P3Bを使うことになります。 ** (15) 関数の構成法 -関数の記述は基本的に次の形式でやってください。 [01] [01] [imm32] [FE] [01] [00] [3C] [00] [20] [20] [00] [00] [00] ... 関数の処理内容 ... [3D] [00] [20] [20] [00] [00] [00] [1E] [3F] [30] -imm32の部分は関数のラベル番号です。 ~ -[01] [01] [imm32] : これはラベル宣言命令なので特に説明はなくてもわかるでしょう。関数を表すラベルの属性は、レジスタに代入することがなくても1にすることになっています。 -[FE] [01] [00] : これは DEBUG-INFO-1 と呼ばれている3バイトのリマーク命令で、詳細は「(17) APIの呼び出し(1)」で確認してください。 -[3C] [00] [20] [20] [00] [00] [00] : この7バイト命令が、R00~R1FとP00~P1FとP30の退避処理を行っています。20の部分を変更したり、00の部分を変更したら、なにかいろいろできそうな気がしてくると思いますが、現在のバージョンでは他の数字の組み合わせを想定できていないので、どうかこのままで使ってください。関数宣言以外のところでこの命令を使うことも想定されていません。 -[3D] [00] [20] [20] [00] [00] [00] : この7バイト命令は上記で退避したレジスタを復元します。関数終了部分以外では使わないでください。この命令は、関数の末尾にしか置けません。したがって、関数処理の途中で関数から抜けたくなったら、関数処理の末尾へジャンプする必要があります。 -[1E] [3F] [30] : PCP(P3F, P30); です。これで呼び出し元に帰ることができます。 ** (16) 条件処理の構成法 ** (17) APIの呼び出し(1) -ここではR30~R3Bしか引数に使わないAPIの呼び出し方を紹介したいと思います。いきなり全部説明しても混乱するだけなので。 -この方法で呼び出せるAPIは次の10個です。この表に出てくる123というラベル番号はもちろん仮のものです。 drawPoint #02: [FE] [05] [01] [0002(32bit)] R30=0xff44; R31=mode; R32=x; R33=y; R34=col; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] drawLine #03: [FE] [05] [01] [0003(32bit)] R30=0xff45; R31=mode; R32=x0; R33=y0; R34=x1; R35=y1; R36=col; 以下略 fillRect #04: [FE] [05] [01] [0004(32bit)] R30=0xff46; R31=mode; R32=xsiz; R33=ysiz; R34=x0; R35=y0; R36=col; 以下略 fillOval #05: [FE] [05] [01] [0005(32bit)] R30=0xff47; R31=mode; R32=xsiz; R33=ysiz; R34=x0; R35=y0; R36=col; 以下略 exit #08: [FE] [05] [01] [0008(32bit)] R30=0xff06; R31=i; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] sleep #09: [FE] [05] [01] [0009(32bit)] R30=0xff42; R31=mode; R32=msec; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] inkey #0d: [FE] [05] [01] [000d(32bit)] R30=0xff43; R31=mode; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] openWin #10: [FE] [05] [01] [0010(32bit)] R30=0xff40; R31=xsiz; R32=ysiz; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] flushWin #11: [FE] [05] [01] [0011(32bit)] R30=0xff41; R31=xsiz; R32=ysiz; R33=x0; R34=y0; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] rand #13: [FE] [05] [01] [0013(32bit)] R30=0xff49; PLIMM(P30, 123); PCP(P3F, P28); LB(1, 123); [FE] [01] [00] -まず前後の [FE] [05] [01] [00??(32bit)] と [FE] [01] [00] を完全に無視してください(あとで説明します)。これらさえなければ、もう知っている命令ばかりです。基本的にはR30に機能番号を入れて、R31以降にパラメータを入れて、それでP28に格納されているラベルへ関数呼び出ししているだけです。 -P28の値はOS(もしくはVM)が設定した状態でアプリを起動しますので、その値を壊さないようにしてください。壊してしまったら(=どこかに控えることもしないで上書きしてしまったら)、もうAPIを呼び出す手段は残っていません。 -inkeyとrandについては、結果がR30に格納されて戻ってきますので、CP命令で適当なレジスタに代入してください。 -さっき無視してもらったFEで始まる命令群ですが、これはリマーク命令なので、本当に無視してバイトコード内に含めないという選択肢もありです。仮にそうしても、おそらく動作に問題はありません。・・・ってこれで説明を終わるとさすがにひどいので、ちゃんと説明します。 ~ -リマーク命令というのは、本来の実行には直接関係のない補助情報です。必ず [FE] [len] で始まります。lenの部分は1バイトで、ここで追加情報の長さをバイト単位で表しています。 -[FE] [01] [00] は通称 DEBUG-INFO-1 と呼ばれている3バイトのリマーク命令で、 LB(1, ...); の直後に置きます。これは「実行しているモジュールが切り替わった(もしくは切り替わったかもしれない)」ことを示しています。どういうことかというと、この文脈では、API呼び出ししてOSやライブラリに制御が移った後、ここへ PCP(P3F, P30); で戻ってくるはずなのです。 -この DEBUG-INFO-1 は、もしエラーが起きた場合に、エラーがアプリの中で起きたのか、システムの中で起きたのか、そういうのを管理するためのもので、デバッグレジスタに「今実行しているのはアプリ内」という設定をするためのコードをもし入れるとしたら、それはここですよー、というヒントをOSECPUに教えているのです。 -裏返せば、もし絶対にバグがないのならこんな情報はあってもなくても同じことです。というかないほうがむしろ高速ですね。・・・しかしOSECPUはセキュアなOSを目指していることもあって、これはぜひとも省略してほしくないのです。 LB(1, ...); のあとには是非 [FE] [01] [00] をおいてあげましょう。 --これを置き忘れると、エラー扱いになって実行を拒否するモードを実装する可能性も少しあります。・・・すみません、ちょっと脅かしてみただけです(笑)。なんといってもこれは所詮リマーク命令なので、なくてもいい存在なのです。 -次は [FE] [05] [01] [imm32] の7バイトのリマーク命令です。これは「以下の記述はAPIの呼び出しだよ」というヒントになっています。これがないと、どこから引数代入のための命令が始まっているのかの頭出しが面倒極まりなくて、それで書いています。imm32にはAPIの表向きの機能番号を書きます。R30に入っている番号は内部の汚い番号です。 -このリマーク命令の後には、R30からレジスタ番号順にLIMM命令かCP命令が並んでないといけません。他の命令でR3xに代入してはいけませんし、他の演算命令もダメです。そういうことがしたいならしてもいいですが、そのときはこのリマーク命令をつけないでください。期待したフォーマットと違うと、osectolsが混乱してしまいます。 -値を返すAPIの場合は [FE] [01] [00] の直後に CP(Rxx, R30); のコードも必要です。これを全部含めてフォーマットがそろっていることを [FE] [05] [01] [imm32] は保証しているのです。 -なんかめんどくさいなーと思ったら(確かにめんどくさい)、 [FE] [05] [01] [imm32] は取っちゃっていいと思います。 ** (18) ポインタレジスタ -ここからしばらくはメモリアクセス関係の命令を紹介します。 ~ -OSECPUはメモリアドレスを指し示すためのポインタレジスタを64本持っています。中のビット数は実装依存です。 -P00~P3F と表記します。 -P00やP01など番号の若いレジスタは、実際のCPUの実レジスタに割り当てるように推奨されているので、P00への代入やP00の値の参照は、P20に対する操作と比べて高速になることが多いです。 --結局どうなるかは処理系依存で、保障はされていません。 -ということで、特にこだわりがないのならP00やP01を使いましょう。 --ちなみにx86版では、P00~P02までが実レジスタに割り当てられていて、残りはメモリを使ってレジスタをエミュレーションしています。 ~ -これらのレジスタはすべて対等で汎用的に使われるというわけではなく、ある程度の使い方が決まっています。これに逆らってはいけないということはないですが、ライブラリなどで食い違うといろいろ面倒かもしれません。 --P00 ( 1本) : ベースポインタです。つまり汎用ではありません。今はベースポインタのサポートが不十分なので、使えないということと同義です。 --P01~P1F (31本) : 最も汎用的なポインタレジスタで、通常は関数ごとにローカルとして扱えます。つまり関数を呼び出しても破壊されたりはしません。 --P20~P27 ( 8本) : 汎用ですが、関数ごとにローカルというわけではなく、グローバル変数的に使うことを想定しています。関数呼び出しによって変更される可能性もあります。 --P28~P2F ( 8本) : これも汎用ですが、関数ごとにローカルではなく、グローバル変数的に使われます。主にOSが用途を決定しています。これに対してP20~P27はアプリが自由に用途を決定できます。 --P30~P3B (12本) : 基本的にはこれらも汎用なのですが、関数の引数を渡したり、返値を入れたりするためにも使われるレジスタで、値が破壊されやすいです。ASKAでは複雑な数式を計算しなければいけなくなると、P3BやP3Aをテンポラリとして勝手に使い、値を破壊してしまうこともあります。P39やP38にまで手をつけることだってあります。しかし最悪でもP30までで、P2Fに手出しすることはありません。 --P3C~P3E ( 3本) : 将来の拡張のためにリザーブされています。 --P3F ( 1本) : 「(9) 無条件分岐命令」などで説明した特別な用途のためのポインタレジスタです。汎用には使えません。 -全体として、OSECPUのレジスタはかなり多いほうだと思います。これはOSECPUがメモリ操作を苦手としていて、できるだけレジスタだけで主要な演算が完結できるようにという設計方針によるものです。 ** (19) データ記述命令 -プログラム内にデータを置くことができます。これらのデータはいわゆる.dataに置かれるもので、プログラムの実行中に書き換えることも許されます。 -この命令そのものは何もしないので、プログラムの実行コード中に混在させて構いません(データを実行してしまうような事故は起きません)。 -(つづく) ** (20) ラベル番号代入命令 [03] [Pxx] [imm32] PLIMM(Pxx, imm32); -6バイト命令です。Pxxには0x00から0x3fのレジスタ番号を指定します。imm32はラベル番号の定数即値を4バイトのビックエンディアンで指定します。 ** (21) メモリへのアクセス命令 -まず、以下に示す命令は、Pxxがデータをポイントしているときにのみ有効で、それ以外の状況で使用すれば、即刻セキュリティ違反でアプリの実行は停止させられます。 --プログラムの分岐先などを指している場合はダメだということです。 -また型が一致しない場合もセキュリティ違反になります。 [08] [Rxx] [typ32] [Pxx] [00] LMEM(Rxx, typ32, Pxx, 0); -これはメモリからデータを読んでRxxに格納する命令で、8バイト命令です。末尾の00は仕様上は他の値にもできるのですが、現在はその機能は未実装なので、00のみ使えます。 -基本的にこの命令以外でメモリからデータを読み込む命令はありません。 -つまりこの命令でレジスタに値を持ってきて、そしてレジスタを使って演算していくことになります。 -メモリアドレッシングはPxxの単独指定しかありません。これは一般的なCPUと比較すれば非常に貧弱です。PADDなどを使ってほしい位置まで動かしたポインタレジスタを使って、アクセスすることになります。 ~ [09] [Rxx] [typ32] [Pxx] [00] SMEM(Rxx, typ32, Pxx, 0); -こちらはレジスタの値をメモリに書くための命令で、やはり8バイト命令です。 -基本的にこの命令以外でメモリへデータを書き込む命令はありません。 ** (22) ポインタ演算命令 -ポインタレジスタには整数を加えることができます。これは配列に有効な演算で、1を足せば次の要素を指しますし、-1を足せば前の要素を指します。もちろん2や-3を加えることもできます。 [0E] [preg0] [typ32] [preg1] [Rxx] PADD(preg0, typ32, preg1, Rxx); -8バイト命令です。 preg0 = preg1 + Rxx; です。計算結果が配列の外になってもかまいませんが、そのようなポインタは逆方向の値を加算して配列の中に戻らない限り、LMEMやSMEMは実行できません。 -この命令の前にLIMM(R3F, ...);を置くのであれば、Rxxの部分でR3Fを指定することが可能です。 ~ -同じ配列を指し示す2つのポインタレジスタに対しては、差を取ることができます。 [0F] [Rxx] [typ32] [preg0] [preg1] PDIF(Rxx, typ32, preg0, preg1); -8バイト命令です。 Rxx = preg0 - preg1; です。ポインタレジスタが異なる配列に属しているときは、この演算はセキュリティ保護エラーとなりアプリが停止します。 -同じ配列に属するというのは、単に型が一致しているだけではダメで、同じタイミングでアロケートされたものである必要があります。 ** (23) ポインタ比較命令 -2つのポインタに対して、PDIFが計算可能であるような組み合わせであれば、PCMP系の命令が実行できて、結果に応じてRxxが-1か0になります。 -(つづく) ** (24) データの型 ** (25) PALMEM命令など -たとえばP01が配列の先頭を指していれば、 PADD(P30, T_UINT32, P01, R00); LMEM0(R01, T_UINT32, P30); -とすることで、配列のR00番目の値をR01にとってくることができます( R00 = P01[R00]; )。これはよく使うので結構便利なフレーズです。 -この方法の副作用としては、P30というレジスタを一時的に使うことで、値が壊れてしまうことです。まあでもOSECPUはレジスタがたくさんあるので、この副作用はそんなには深刻ではありません。 -しかしそれでも副作用がないのならないほうがいいので、こんな命令を用意しています。 PALMEM0(R01, T_UINT32, P01, R00); [0E] [3F] [T_UINT32] [P01] [R00] [08] [R01] [T_UINT32] [3F] [00] -もちろんP01やR00やR01の部分は他のレジスタに置き換えてもOKです。 -バイトコードのほうを見ると、これは結局PADD命令とLMEM命令でしありません。しかしなんとテンポラリレジスタにP3Fを指定した形になっています。もちろんP3Fのこんな用法は許されていません。許されていないからこそ、JITコンパイラはこれがPALMEM命令であると認識することができます。 ~ -同様にPASMEMやPAPLMEMやPAPSMEM命令があります。 ** (26) malloc命令 [32] [Pxx] [reg0] [reg1] -メモリをヒープ領域からアロケートします。reg0には要求している型を入れます。たとえばT_SINT32を要求しているのなら6を代入しておいてください。つまり(他の命令とは異なり)型を定数ではなくレジスタで指定しているのです。 -reg1にはその型をいくつ分ほしいのかを指定します。やはりレジスタで。 -そしてPxxにポインタが返ります。 -たとえば、T_SINT32が100個分の配列を確保したいのであれば、こうします。 [02] [30] [00000006] [02] [31] [00000064] [32] [01] [30] [31] -これでP01にその配列の最初の要素を指したポインタが代入されます。 ~ -mallocで確保したメモリ域を使い終わったら、システムに返す必要があります。OSECPUではガーベージコレクションのようなものは一切ありません。 [33] [Pxx] [3F] [3F] -このとき指定するPxxはmallocで得たものをそのまま使ってください。 -なお、アプリが終了すればmallocで確保したメモリは全て自動で回収されるので、終了直前に律儀にfreeして回る必要はありません。 ** (27) talloc命令 [30] [Pxx] [reg0] [reg1] -メモリをスタック領域からアロケートすることを除けば、上記のmalloc命令と同じです。 -解放については [31] [3F] [3F] [3F] -を使います。関数の中でスタック域に確保したメモリは関数から出る前に''責任を持って解放しなければいけません''。またその際の解法順序は、LIFO(もしくはFILO)でなければいけません。他の関数に開放してもらうように頼むことは''できません''。 ** (28) APIの呼び出し(2) -ポインタレジスタを引数に含むAPIについて、呼び出し方を紹介します。 -(つづく) ** (29) 擬似構造体 -OSECPUは現在構造体をサポートできていません。開発者側の時間的な制約によって、構造体のサポートは少なくとも1年はかかるでしょう。OSECPUにおいて構造体のサポートは大変深い意味を持っていて、だからこそ構造体をバイトコードレベルでサポートするのですが(たとえばx86やARMには構造体をサポートする命令などない)、それゆえに設計には細心の注意を要して、すぐには作れそうにないのです。 -しかしだからといって、構造体的なことが全くできないわけではありません。配列があれば構造体の代わりはできるのです。 -構造体というのは、雑な定義でよければ、いくつかのメンバ変数を持った存在だということができます。 struct Abc { int i, a[10]; unsigned char c, d; }; -これを配列で代用することを考えます。まず配列は要素が全て同じ型でなければいけないので、この中で一番強い型を探します。この場合はintです。つまりintはunsigned charの全ての値を表現できるので代用できると考えるわけです。メモリはもちろんムダになりますが、そこには眼をつむります。 -結局、T_SINT32が13個あればいいのです。だからタイプT_SINT32で要素数13の配列を作ってそれを使ってください。それだけです。 -もしAbcの配列を作る必要があれば、その個数を掛け算してください。 Abc abc[100]; の代用なら1300でいいわけです。 ~ -この方法には限界があります。それは、ポインタ型はintと互換性がないので、構造体内にintとポインタが入っているとうまく表現できないということです。それについてはどうしようもないので、構造体を二つに分けます。 struct Node { int data; Node *left, *right; }; -たとえばこんなものがあったとしたら、これを2つの構造体に分けてしまいます。 struct NodeInt { // int系のメンバを集めたもの. int data; }; struct NodePtr { NodeInt *leftInt, *rightInt; NodePtr *leftPtr, *rightPtr; }; -こうしてしまった上で、配列を二つ作ります。 NodeInt *nodeInt = malloc(...); NodePtr *nodePtr = malloc(...); -こうしてしまえば、node[3].dataにアクセスしたいときはnodeInt[3].dataを、node[3].leftにアクセスしたいときはnodePtr[3].leftIntとnodePtr[3].leftPtrの両方にアクセスすればいい事になります。 ~ -もしくはもっといい方法もあります。ポインタなんて結局は配列の中の要素をさすのですから、ポインタ値ではなくて添え字を覚えるのです。この方法ならみんなintになります。 struct Node { int data; int leftIdx, rightIdx; }; ** (30) -(つづく) * こめんと欄 -(6) 整数比較命令 25 CMPLでなくCMPGだと思います。 -- [[hikarupsp]] SIZE(10){2013-08-04 (日) 11:53:12} -(19) データ記述命令のオペコードは多分[34][type32][length32][data...]だと思います。 -- [[hikarupsp]] SIZE(10){2013-08-05 (月) 06:12:55} -そのとおりです。直しました。>CMPG -- ''K'' SIZE(10){2013-08-05 (月) 11:01:40} #comment
テキスト整形のルールを表示する