* lbstk ver.0.03の説明 -(by [[K]], 2013.04.15) ** lbstkとは? -簡単に言うと、OSECPUのアセンブラを支援するためのツールで、これがあると煩雑なラベル管理を肩代わりしてくれます。osectolsの中に含まれます。 --x86などの一般的なCPUの開発ツールでいうと、リンカに相当すると思われます。 -語源はlabel-stackです。 -前バージョンのlbstk01やlbstk02と比較すると、内部仕様が変更され、より使いやすい方向に進化しました。 --仕様が変わったので互換性はありません。 //-C言語の標準ライブラリだけで書いた場合は210行くらいの非常に簡単なものです。できれば将来OSECPUアプリとして移植したいと思っています。 -できればOSECPUに移植したいと思っています。 -できれば将来OSECPUアプリとして移植したいと思っています。 -最初のバージョンはosecpu022aにバンドルされています。 ** きっかけ -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の外に作ることになりました。 ** 仕組み -上記の関数呼び出しを次のように書くことにしました。 lbstk2(0,1); PLIMM(P1E, lbstk1(0,0)); JMP(12); LB0(lbstk1(0,0)); lbstk3(0); -これは次のような意味を持ちます。 --lbstk2(j, sz); : 新規にラベル用の連続した番号をsz個発行し、その番号のうち一番小さいものをスタックにつみます。使用するスタックはj番目のスタックです(lbstk03ではスタックを4本持っていて、0~3を指定できます)。 ---上記例でj=0としたのは、関数コール用のスタックとしてj=0を使っているからです。 --この命令自身は、ラベルの発行を促す以上の意味を持たないので、出力ソースには現れません(消滅します)。 --lbstk1(j, ofs) : スタック[j]から一番上のラベル番号を取得します。popはしません。peekです。その数値にofsを加えラベル番号とし、lbstk1(j, ofs)の記述と置換して出力します。 --lbstl3(j); : スタック[j]から一番上のラベル番号をpopし、値を読み捨てます。この命令もスタック状態を制御する以上の意味を持たないので、出力ソースには現れません(消滅します)。 -このような機構により、以下のように完全に同一の記述を何度書いても問題なくなりました。~ lbstk2(0,1); PLIMM(P1E, lbstk1(0,0)); JMP(12); LB0(lbstk1(0,0)); lbstk3(0); lbstk2(0,1); PLIMM(P1E, lbstk1(0,0)); JMP(12); LB0(lbstk1(0,0)); lbstk3(0); -ということで、これはマクロ化されて、 CALL(12); とだけ書くことになっています。 #define CALL(label) lbstk2(0,1); PLIMM(P1E, lbstk1(0,0)); JMP(label); LB0(lbstk1(0,0)); lbstk3(0) -このスタック操作は、ソースの書き換えの際にラベル番号を求めるために仮想的に行うものであって、アプリの実行時に行うわけではありません。 ** 応用 -osecpu_asm.hには以下のような記述があります。 #define LOOP() lbstk2(0,2); LB0(lbstk1(0,0)) #define CONTINUE lbstk1(0,0) #define BREAK lbstk1(0,1) #define ENDLOOP0() lbstk3(0) /* BREAKを使わない場合用 */ #define ENDLOOP1() LB0(lbstk1(0,1)); lbstk3(0) /* BREAKを使う場合用 */ -これがあると以下のような表記が可能になります。 #define sum R00 #define i R01 LIMM(sum, 0); LIMM(i, 0); LOOP(); ADD2(sum, i); ADDI(i, i, 1); CMPIJNE(i, 10001, CONTINUE); ENDLOOP0(); /* BREAKを使わない場合用 */ -このようにループを書く際にラベル番号を気にしないで済みます。またこのループは入れ子にしていても問題なく機能します。 -なおこのマクロによるループは、ENDLOOPしても次のループを開始しません(そのままループから抜け出てしまいます)。繰り返しを意図するときは、JMP(CONTINUE);をENDLOOPの直前に入れてください。 ** 発展 -他にlbstk0(n);とlbstk4(n);とlbstk5(i)という表記をサポートしています。これは関数内でネストしないローカルなラベルにうまくラベル番号を振るためのものです。 -まずlbstk0(n); はlbstkが発行するラベル番号の開始番号を指定するためのものです。この番号よりも小さな数字でグローバルラベルを定義します。グローバルラベルというのは、関数の外からも参照可能なラベル番号のことです(publicな関数の呼び出しポイントなど)。 -lbstk4(n);は関数の先頭で宣言されるべきもので、関数内でいくつのローカルラベルを使うかを規定します。このローカルラベルには、先のlbstk2(j, sz);で発行する分を含めません。その分についてはlbstkが自動で数えてうまくやってくれます。 -lbstk5(i)はi番目のローカルラベル番号を返します。iはlbstk5(n);で指定したnよりも小さくなければいけません。0から(n-1)が有効です。 -これらについてはosecpu_asm.hで以下のように整備されています。 #define GLOBALLABELS(n) lbstk0(n) #define LOCALLABELS(n) lbstk4(n) #define LOCAL(i) lbstk5(i) ** こめんと欄 -このページにこめんと欄はありません。このページの内容にコメントしたいときは[[impressions]]にお願いします。