C言語で書いたプログラムからアセンブリ言語で書いたプログラムを利用する方法として、インライン・アセンブラがあります。これは高級言語のソースのなかに記述できるのが強みですが、いろいろと制約があり、必ずしもアセンブリ言語のよさを100%引き出せるわけではありません。
インライン・アセンブラと異なり、別のソースファイルにアセンブリ・ルーチンを置くならばルーチン全部を丸々アセンブリ言語で記述することができます。この場合、プログラマはプロセッサの能力をほぼ完全に引き出すことができます。
無論、ただの昼飯があるわけではなく、プログラマはCコンパイラが生成するコードの呼び出し規則に従ってプログラムを書かなければなりません。ここではC言語から呼ぶことのできるアセンブリ・ルーチンの書き方を説明します。なお、概略のみ説明しますので、完全な説明は以下の文献を参照してください。
C言語の関数名の頭にアンダースコアをつけたものがアセンブリ・ルーチンのラベルとなります。例えば次のようなC言語の関数を考えてください
// C言語の関数 voo void foo(void);
この場合、アセンブリ・ルーチンのラベルは次のように頭にアンダースコアをつけたものになります。
_foo:
C++言語の関数名は中間オブジェクトに落としたときにコンパイラが変更しています。そこで次のように宣言することでアセンブリ・ルーチンを呼ぶことができます。この機能は本来C++言語からC言語のコンパイル済みルーチンを呼ぶためのものですが、アセンブリ・ルーチンを呼ぶことにも使えます。
// C++言語の関数 foo void "C" foo(void);
Cコンパイラは関数呼び出し時に規則にのっとってスタック上に戻り情報などを退避します。アセンブリ・ルーチンを組むときもこの規則に沿ったほうが話が簡単になります。
構築するデータ構造を理解するのはやや面倒ですが、規則に沿うこと自体は簡単です。このためのマクロがasm_sprt.hに定義されているからです
#include <asm_sprt.h> // アセンブリ言語サポート・ヘッダ ... _foo: full_entry(0); ... // アセンブリ言語による関数記述 ... full_exit;
full_entryがそのためのマクロです。このマクロはスタック・フレームを構築するものです。引数は0を与えていますが、この意味は後で説明します。full_entryは関数内部のデータ構造を整えますが、関数に渡される引数にかかわらずこのマクロを使うことができます。
full_exitはfull_entryに対応する脱出マクロです。このマクロは関数からの戻りも実行しますのでRTS命令は不要です。
一時的に使う変数やレジスタの退避にはスタック上の一時領域を使います。領域の確保はfull_entryマクロの引数で指定します。nワードの領域を確保するにはfull_entryに引数nを渡します。このとき、呼ばれた関数内部で自由に使用できる領域はdm(i5-2)からdm(i5-n-1)までです。例えばnが4ならばdm(i5-2)からdm(i5-5)までを一時領域として使用できます。
asm_sprt.h には次の三つのマクロが定義されています。
引数はスタック経由で渡します。呼ばれたルーチンからこれらの引数を参照するには、I5レジスタを基点としてアクセスします。このとき、第一引数はI5+1、第二引数はI5+2に位置します。例として次の関数プロトタイプを見てください。
int foo ( int param1, int * param2, int * param3(void), int param4 );
このプロトタイプを持つ関数内部では与えられた引数を次のようにアクセスすることができます。
引数 | 位置 | サイズ | 説明 |
---|---|---|---|
param1 | I5+1 | 1 | int型 |
param2 | I5+2 | 1 | intへのポインタ型 |
param3 | I5+3 | 2 | 関数へのポインタ型 |
param4 | I5+5 | 1 | int型 |
関数へのポインタ型だけ引数のサイズが2ワードであることに注意してください。VisualDSP++のCコンパイラは関数へのポインタを2ワードで表します。したがって関数は24ビットのアドレス空間のどこにでも置くことができます。反面、データへのポインタは1ワードですので、アドレス空間の最初の1ページにしかデータを配置できません。いずれにせよADSP-2191を遊びで使う分にはデータ空間が足りないということはないでしょう。むしろ関数へのポインタ型のサイズをうっかり1と計算しないように注意してください。
引数の取得に関しては次の二つのマクロが asm_sprt.h で定義されています。
戻り値は、値のサイズによって返し方が違います。
Cコンパイラは一部のレジスタの使い方を厳しく決めています。そのためレジスタの使い方には制限があり、それを守らなければプログラムは正常に動作しません。以下でコンパイラがレジスタをどのように使うかという観点から種類わけを行います。
以下の表にADSP-2191のレジスタをコンパイラの用法に従って色分けして示します。色はレジスタの使い方を示します。
レジスタ名 | ||||||
---|---|---|---|---|---|---|
AX0 | AX1 | AY0 | AY1 | AR | AF | |
MX0 | MX1 | MY0 | MY1 | MR2 | MR1 | MR0 |
SI | SE | SB | SR2 | SR1 | SR0 | |
I0 | L0 | B0 | M0 | DMPG1 | ASTAT | MSTAT |
I1 | L1 | B1 | M1 | CNTR | STACKA | |
I2 | L2 | B2 | M2 | ICNTL | STACKP | |
I3 | L3 | B3 | M3 | IJPG | CCODE | |
I4 | L4 | B4 | M4 | DMPG2 | IMASK | |
I5 | L5 | B5 | M5 | IRPTL | ||
I6 | L6 | B6 | M6 | LPSTACKA | ||
I7 | L7 | B7 | M7 | LPSTACKP |
コンパイラに対してI2, I3, M0レジスタを使わないように指示することができます。指示はコンパイラ・オプションを使い、構文は次のようになります。
-reserve I2,M0
この例ではI2とM0を使用しないように指定しています。コンパイラはL2、B2を使いませんからアセンブリ・プログラムはI2を使って永続的な循環バッファを作ることができます。永続的とは、アセンブリ・ルーチンの外でこのバッファが破壊されないことを意味します。
ただし、I2, I3, M0はそれぞれ保護レジスタですから、ライブラリ・ルーチンの中で使用されていることはありえます。したがって、-reserveオプションで永続的なバッファを組む場合、割り込みルーチン内ではそれを利用できないことになります。ライブラリの中で割り込みがかかるかもしれないからです。