* lbstk01の説明 -(by [[K]], 2013.03.29) ** lbstk01とは? -簡単に言うと、OSECPUのアセンブラを支援するためのツールで、これがあると煩雑なラベル管理を肩代わりしてくれます。 --x86などの一般的なCPUの開発ツールでいうと、リンカに相当すると思われます。 -語源はlabel-stackです。 -前バージョンのlbstk00と比較すると、ループのBREAKやブロックIFに対応できるようになりました。 -C言語の標準ライブラリだけで書いた場合は210行くらいの非常に簡単なものです。できればOSECPUに移植したいと思っています。 -最初のバージョンはosecpu011aにバンドルされています。 ** きっかけ -OSECPUでは関数を呼ぶときに関数呼び出しの命令があるわけではなく、ただ目的の関数へJMPしています。しかしそれでは当然戻ってこられません。ということで、JMPの直前に、P1Eレジスタに「終わったらここへJMPしてください」というアドレスを格納する命令を置きます。 PLIMM(P1E, 4); JMP(12); LB0(4); --この例では、LB0(12);の関数を呼んでいます。 -もしこの関数を連続で2度呼ぶとしたら、こうなります。 PLIMM(P1E, 4); JMP(12); LB0(4); PLIMM(P1E, 5); JMP(12); LB0(5); -つまり関数呼び出しのたびにラベルを作る必要があって、しかもその番号は重複してはいけないのです。このラベル番号の管理はプログラムが長くなってくると非常に面倒になってきます。 -このような仕様なのには理由があります。OSECPUはラベルのあるところ以外には絶対に分岐できないようになっています。どこでも好きなところに分岐できてしまう普通のCPUとは違います。好きなところに分岐できてしまうことはセキュリティ的に危険ですし(だからx86にはコールゲートなどの仕組みを使って、分岐先を制限する仕組みを持っているわけです)、プログラム上のどこで他から処理が合流するか事前に分かるこの仕様はJITコンパイラにとっては有利です。 -それにこのような仕様だからこそ、OSECPUでは分岐命令はJMP命令(=P00への代入)だけで済んでいて、JITコンパイラがシンプルになっています。 -それならosecpu.exeがもっとがんばってうまく処理すればいいではないかという指摘もあるかと思います(CALLの直後に自動でラベルを発行するとか)。しかしそれはosecpu.exeが複雑になってしまいます。osecpu.exeは移植しやすい規模を維持するべきです。・・・とまあそんなわけで、この問題を解決するためのツールをosecpu.exeの外に作ることになりました。 ** 仕組み -上記の関数呼び出しを次のように書くことにしました。 lbstk1(0,1,2); PLIMM(P1E, lbstk2(0,0)); JMP(12); LB0(lbstk2(0,0)); -これは次のような意味を持ちます。 --lbstk1(j, sz, n); : 新規にラベル用の連続した番号をsz個発行し、その番号のうち一番小さいものをn回スタックにつみます。使用するスタックはj番目のスタックです(lbstk01ではスタックを4本持っていて、0~3を指定できます)。 ---上記例でj=0としたのは、関数コール用のスタックとしてj=0を使っているからです。 ---上記例で2回つむのは、このラベル番号を2回参照するからです。 --この命令自身は、ラベルの発行を促す以上の意味を持たないので、出力ソースには現れません(消滅します)。 --lbstk2(j, ofs) : スタック[j]からラベル番号を一つ取ってきます。その数値にofsを加えラベル番号とし、lbstk2(j, ofs)の記述と置換して出力します。 -このような機構により、以下のように完全に同一の記述を何度書いても問題なくなりました。~ lbstk1(0,1,2); PLIMM(P1E, lbstk2(0,0)); JMP(12); LB0(lbstk2(0,0)); lbstk1(0,1,2); PLIMM(P1E, lbstk2(0,0)); JMP(12); LB0(lbstk2(0,0)); -ということで、これらはマクロ化されて、 CALLS(12); とだけ書くことになっています。 -ということで、これはマクロ化されて、 CALLS(12); とだけ書くことになっています。 #define CALLS(label) lbstk1(0,1,2); PLIMM(P1E, lbstk2(0,0)); JMP(label); LB0(lbstk2(0,0)) -このスタック操作は、ソースの書き換えの際にラベル番号を求めるために仮想的に行うものであって、アプリの実行時に行うわけではありません。 ** 応用 -osecpu_asm.hには以下のような記述があります。 #define LOOP(c,b) lbstk1(0,2,c+b+1+((b!=0)&1)); LB0(lbstk2(0,0)) /* c!=0を想定 */ #define CONTINUE lbstk2(0,0) #define BREAK lbstk2(0,1) #define ENDLOOP0() /* BREAKを使わない場合用、省略可能 */ #define ENDLOOP1() LB0(lbstk2(0,1)) -これがあると以下のような表記が可能になります。 #define sum R00 #define i R01 #define const1 R30 #define const10001 R31 LIMM(const1, 1); LIMM(const10001, 10001); LIMM(i, 0); CP(sum, i); LOOP(1, 0); /* CONTINUEを使う回数, BREAKを使う回数 */ ADD2(sum, i); ADD2(i, const1); CMPJNE(i, const10001, CONTINUE); -このようにループを書く際にラベル番号を気にしないで済みます。またこのループは入れ子にしていても問題なく機能します。 -なおこのマクロによるループは、ENDLOOPしても次のループを開始しません(そのままループから抜け出てしまいます)。繰り返しを意図するときは、JMP(CONTINUE);をENDLOOPの直前に入れてください。 --このような仕様になっているのは、暗黙のCONTINUEを生まないためです。 ** 発展 -他にlbstk0(n);とlbstk3(n);とlbstk4(i)という表記をサポートしています。これは関数内でネストしないローカルなラベルにうまくラベル番号を振るためのものです。 -まずlbstk0(n); はlbstk00.exeが発行するラベル番号の開始番号を指定するためのものです。この番号よりも小さな数字でグローバルラベルを定義します。グローバルラベルというのは、関数の外からも参照可能なラベル番号のことです(publicな関数の呼び出しポイントなど)。 -lbstk3(n);は関数の先頭で宣言されるべきもので、関数内でいくつのローカルラベルを使うかを規定します。このローカルラベルには、先のlbstk1(j, sz, n);で発行する分を含めません。その分についてはlbstk00.exeが自動で数えてうまくやってくれます。 -lbstk4(i)はi番目のローカルラベル番号を返します。iはlbstk3(n);で指定したnよりも小さくなければいけません。0から(n-1)が有効です。 -これらについてはosecpu_asm.hで以下のように整備されています。 #define GLOBALLABELS(n) lbstk0(n) #define LOCALLABELS(n) lbstk3(n) #define LOCAL(i) lbstk4(i) ** こめんと欄 -このページにこめんと欄はありません。このページの内容にコメントしたいときは[[impressions]]にお願いします。