前回までの4回でRabbit4000のオペコードマップに色を塗って「理解」したつもり。かなりな大拡張ですがアセンブラレベルでは「ほぼほぼ」Z80の上位互換にみえないこともないです(バイナリ互換ではないです。)今回からRabbit4000上でアセンブリ言語ルーチンを動作させていきたいと思います。敷居が低いんだ、これが。
※「うさちゃんと一緒」投稿順indexはこちら
※Rabbitシリーズのマニュアルは、販売元のDigi社のページからダウンロードできます。
Dynamic C Version 10.72E
使用させていただいとります開発ツールは、上記バージョンのDynamic Cです。統合開発環境といって良いものだと思います。ほとんどのC言語処理系で、外部のアセンブリ言語ソースで記述された関数とのリンクや、Cソース内のインライン・アセンブラが使用可能です。Dynamic Cも例外ではありません。それどころか、アセンブラ記述の敷居がとっても低いです。Cを書いているのか、アセンブラを書いているのかそのうち分からなくなりそうな雰囲気もあり。
通常、アセンブラ関数を書くにあたってはABIの規約とかをいろいろ調べなくてはならないのですが、Dynamic Cは簡単です。要約すると以下の通り。
-
- 呼び出される側で全レジスタを使用可能。レジスタの退避は呼び出し側の責任。
- 引数は全てスタック上に置かれる。ただし、第1引数のみレジスタでも渡される。第1引数が16ビット以下の整数や通常のポインタであればHL、32ビットの数値(ファー・ポインタでない)ならばBCDE(Z80のBC、DEを結合)、ファー・ポインタ(20/24ビットのアドレス空間を直接させる)ならばPXが使われる。
- 戻り値は上記ルールで、HL、BCDE、PXが使われる
独立したアセンブラ関数の記述
フル・アセンブラの関数を記述することができるのですが、例えば gas のように.s とか、インテルアセンブラのように .asm とか拡張子を変える必要はないです。.c ファイルの中にアセンブラをそのまま記述します。キーワード、 #asm と #endasmで挟めばアセンブラ記述部分となります。
関数名はグローバルなラベルなので、2個のコロンで定義します。コロン1個だとローカルラベルです。以下は addSという関数の例です。C言語からは以下の関数プロトタイプを宣言すれば呼び出し可能となります。
int addS(int);
以下は第1引数に333を足して、戻り値として返すだけの関数です。
独立したアセンブラ関数は拡張メモリであるxmemに置くように指定することもでき、限られた容量のルートセグメントを節約することも可能みたいです。ただしそのためにはZ80には無いロングコール、ロングリターンを駆使することになるのでまた今度。
インライン・アセンブラ関数の記述
同じ #asm #endasmをCのコード内で使えば、インライン・アセンブラ関数となります。gas のようにCの変数とアセンブラ部分のレジスタとの関係を独特の記法で記述するような必要はありません。「普通にCのローカル変数にアクセス」できます。スタックに積まれている引数にも簡単にアクセスできます。ここでRabbit4000で拡張された命令が暗躍?します。
以下はCの関数の中身全てをアセンブラで書いてみたものです。ld 命令のソース、(sp+@sp+arg2)が拡張された記法です。命令的には(sp+displacement)なのですが、Dynamic Cは@spと書くことでスタックスペースの大きさを得ることができ、それに引数名を加えてやれば実際のディスプレースメント値になるようになっているのでした。便利。
ただし、SPを操作してしまうと位置がズレてしまうので、関数内でSPを操作(PUSH/POP)する必要があるならば、自分でズレた分を補正するか、デフォルトではONになっていないIXレジスタをフレームポインタに使うコンパイルモードを使うかする必要があるようです。IXをフレームポインタにするモードでは、CALL/RETでIXを自動で設定してくれるみたいです。でも当然オーバヘッドはありと。
なお、インライン関数の中に「Cのステートメント」を記述するという荒業を使うこともできます。これにはいろいろと使い道がありそうなのですが、今回はパスです。
今回実験のソース全文
アセンブラで書いた関数は3つ。
-
- add1、インラインの最小構成、引数1個を+1して返却
- addA、インライン、引数を2個とり、2個を加えて返却
- addS、独立型、引数を1個とり、即値333を加えて返却
#class auto int add1(int arg) { #asm inc hl #endasm } int addA(int arg1, int arg2) { #asm ex de, hl ld hl, (sp+@sp+arg2) add hl, de #endasm } int addS(int); void main() { int src, src2, dst; printf("Rabbit4000 ASM Training.\n"); src = 999; dst = add1(src); printf("add1: %d -> %d\n", src, dst); src = 1000; src2 = 123; dst = addA(src, src2); printf("addA: %d + %d -> %d\n", src, src2, dst); src = 6666; dst = addS(src); printf("addS: %d + 333 -> %d\n", src, dst); } #asm addS:: ex de, hl ld hl, 333 add hl, de ret #endasm
ビルドして実行
うさちゃんRabbit4000のRAMをターゲットにビルドして、実行させてみたときのStdioへの出力が以下に。
各関数、ちゃんと使命を果たして居るようです。
うさちゃんでアセンブラするのは簡単。ホントか?