このページは、ロシアのSOLiDさんが製作した、強力な最適化と高速なコンパイル速度を誇る、SOLiD Cでプログラムを行うにあたって必要になる点について書いたものです。
SOLiD CはロシアのサークルNovatecさんのWebページから入手することができます。
MSX C Magazineに戻る
Tatsu's MSX Researchesにもどる
MSX-C Version 1.1,MSX-DOS TOOLSマニュアル(アスキー)
SOLiD CはMSX-Cとの共通点が多く、MSX-C用の書籍が役に立つこともあります。
なぜ、筆者はSOLiD Cを推奨しているのでしょうか?
大きく分けて、次の2点が挙げられます。
これらの特徴から、筆者としては、ゲームや、比較的簡単なツールにSOLiD Cを利用するのが有用と考え、こうして、使い方のコツを紹介するに至りました。
MSX-DOS2上で、SOLiD Cを利用する際に、コマンドラインからは、うまく起動するけど、同じ事をバッチファイルから行おうとすると、うまく動作しなかったり、*** Not enough memoryエラーが出て、そもそも起動しなかったりということが起こることはないでしょうか?
その場合、BUFFERS命令を利用して、システムが利用するディスクバッファの量を減らすと、正常動作することがあります。
私の環境(FS-A1ST+MEGA-SCSI+540M MO、MSX-DOS2)では、BUFFERS=19としていたところを10にすることで、動作が安定してきました。
低水準入出力の_seek関数(内部関数)のソースファイルでは、long型へのポインタが引き数として登場しますが、SOLiD Cでは、long型は使えません。
実際は、単なるポインタなので、int型の配列へのポインタを渡すことになります。
ああ、まぎらわしい。
SOLiD Cでは、シンボル名は、MSX-Cと同様に、大文字と小文字を区別しないようです。
文字数に関しては、アセンブルの段階で、出力するオブジェクトに、Extended REL format(AS.COMのオプションに-xを指定する)を利用することで、8文字以上のシンボルを使うことができるようになります。
SOLiD Cでは、アセンブラであるASに-xオプションを付けない場合に生成される通常のオブジェクトファイルで8文字、-xオプションを付けたExtended REL formatのオブジェクトファイルだとそれ以上の文字数のシンボル名を付けることができます。
では、Extended REL formatのオブジェクトファイル内の8文字を超えるシンボル名(例.interlace)を持ったシンボルを通常のオブジェクトファイルから呼び出す(interlac)とどうなるでしょうか?
逆に、通常のオブジェクトファイルのライブラリ(例.interlac)をExtender REL formatのオブジェクトファイルから先頭の8文字を同じくして呼び出す(interlace)とどうなるのでしょうか?
答えは、どちらもリンカでシンボル名が見つからないと言うことで、エラーとなります。
リンカは、通常のオブジェクトファイルとExtended REL formatのオブジェクトファイルのシンボルを相互に検索する際に8文字目までで検索すると言うように、オブジェクトのフォーマットによって扱うファイル名の長さを変えると言うわけではないようです。
まあ、このような特別扱いを許すと先頭の8文字までは同じシンボル名であるが、9文字目以降が異なるモジュールが2個以上あるときに、どちらのモジュールをリンクすればよいか分からなくなるので、オブジェクトのフォーマットに関わらず、シンボル名を等しく扱うのは当然と言えますが。
この結果からシンボル名の文字数でリンク時にエラーを起こさないためには、ユーザーレベルでは、利用するオブジェクトファイルのフォーマットを合わせておいた方がよいでしょう。
となると、扱えるシンボル名の文字数が多いExtended REL formatで合わせておいた方がよいでしょう。
さて、コンパイル用バッチファイルのアセンブラを起動する行のコマンドラインを確かめますか。
ちなみに、標準ライブラリは通常のオブジェクトファイルなので、setverify関数はオブジェクトファイル内では、setverifというシンボル名に、他の処理系で、fcloseallと言う名前の関数は、fclosallと名前が短縮されたりしています。
とりあえず、ヘッダファイルにプリプロセッサ文#defineでも書いて、ごまかしましょう。
(おいおい、これじゃMSX-Cだよ)
SOLiD Cでは、MSX-C同様、引数の型を検知する機構は持ってないようなので、char型を渡す際には、呼び出しの際に、引数の型を明示する必要があります。
アセンブラでの関数の書き方の項でも述べますが、第1引数はchar型とその他の型で、利用するレジスタが異なるので、引数の型を明示しないと、正常な動作が期待できなくなります。
SOLiD Cのchar型はMSX-C同様、unsignedです。
MSX-Cと異なり、unsigned charやsigned charという書き方も通りますが、無視されます。
SOLiD Cのライブラリは、コンパイラのアーカイブに付属するテスト用の小さいライブラリ(SLIB.IRL)とライブラリのアーカイブに入っている一通りのライブラリ(CLIB.IRL)があります。これらの多くは重複しており、putc()関数やputs()関数のように、内部では全く異なる処理をしているものがあります。
そのため、2つのライブラリを同時にリンクすると、ある関数はCLIB.IRLの物が必要にも関わらず、SLIB.IRLの関数をリンクしてしまい、プログラムが正常に動作しないということが考えられます。
しかし、SLIB.IRLの中には、CLIB.IRLに入っていない、MSXに依存したプログラムを組むのに、便利な関数があるので、これらの関数と、CLIB.IRLの関数を組み合わせることで、幅広いプログラムができます。
そこで、SLIB.IRL固有の関数のモジュール名とその中に含まれる関数をリストアップしますので、これらのモジュールを抜き出して、独立したライブラリにすると便利でしょう。
モジュール名 | 含まれる関数 |
---|---|
TIME | time |
HOOK | nexthook,sethook,reshook |
VDPSTA | vdpstat |
VDP | vdp |
DIEI | di,ei |
VPEEK | vpeek |
VPOKE | vpoke |
SCREEN | screen |
OUT | outp |
INP | inp |
CHKCTL | _sense |
KILBAT | kilbat |
SOLiD Cの標準ライブラリは、フルアセンブラで記述された、高速・高機能・コンパクトなものですが、いくつかのバグがあるようです。
という訳で、私が見つけたバグをここに書いておきます。
これらのバグは、自分で直して、作者に報告しているので、そのうちバグフィックス版が出る可能性はありますが、とりあえず、それまでは、これらの点に注意すると良いでしょう。
2000/04/25現在では出てないようなので、とりあえずは、私が修正したライブラリを使ってみてください。。
これらのバグに関しては、まだ解決していないので、プログラムを組む際は注意して下さい。
SOLiD Cの関数には、引数の型や個数が固定されている、固定パラメータ関数と、引数の型や個数が不定の可変パラメータ関数があります。
固定パラメータ関数では、頻繁に使うであろう引数をレジスタを使って渡すので、アセンブラでの関数記述が簡単になり、実行速度が速くなったり、スタックから引数を取り出すオーバーヘッドがなくなるので、その分、できた関数のプログラムががコンパクトになるという利点があります。
SOLiD Cの関数定義はデフォルトでは、固定パラメータ関数となり、パフォーマンスの向上が図られています。
一方、可変パラメータ関数は、引数の型や個数が不定の場合に使われます。
可変パラメータ関数を記述する際は、次のようにします。
int sample(.)
と、宣言して、呼び出す側に、可変パラメータ関数であることを明示します。実パラメータ | アドレス |
---|---|
1番目 | 第2引数のアドレス |
2番目 | 第2引数のアドレス+2 |
n番目 | 第2引数のアドレス+(n-1)*2 |
実際の書き方を次に示しますが、可変パラメータは、int型もしくはポインタ型の配列と捉えると理解しやすいのではないでしょうか。
例)
int sample(.);
int sample(nargs,args)
int nargs;
int args;
{
int i,*p;
for (i = 0,p = &args;i < nargs;i++){
printf("Parameter No.%d %d\n",i+1,*p);
p++;
}
}
可変パラメータ関数に渡せる引数は6個までで、7個を超えるとコンパイラが正常なコードを生成してくれません。
可変パラメータ関数の実験で偶然見つけました。
ちなみに、固定パラメータ関数ではこのバグはありません。
(2000/5/3追記)
printfファミリー関数以外で可変パラメータ関数を使う場面はそうないと思うのですが、注意するに越したことはないでしょう。
SOLiD Cでは、1ファイル内に1個の関数がある拡張子.RELのオブジェクトファイルをOL.COMで結合した拡張子.IRLのファイルをライブラリとして利用しています。
1つのファイル内では、呼出順によって、ライブラリの中の配列を考える必要があります。あるモジュールが別のモジュールを呼び出すとき、呼び出すモジュールは、呼びされるモジュールより前に無くてはなりません。
ここでいう前とは、OL.COMのLコマンドで、ライブラリファイル内のオブジェクトファイル名・シンボル名表示を行った時に、先に表示される方が前となります。
たとえば、CLIB.IRLというファイルの中に、puts関数とputc関数があり、puts関数がputc関数を呼び出している場合は、puts関数がputc関数の前にある必要があります。
もし、これが逆だと、リンカはputc関数を検索することができず、エラーが出ます。
標準関数にバグがあったりして、ライブラリを差し替えたい場合は、どうすればよいのでしょうか?
OL.COMは、モジュール1個だけを差しかえるということはできません。
しかし、リンカで、同名のモジュールがあった場合は、先(この場合は、LD.COMのコマンドラインで左に位置するファイル)に登場したライブラリ内のモジュールがリンクされるので、この性質を利用して、ライブラリを乗っ取ることができます。
この項では、話を簡単にするため、1個の関数=1モジュールとして、話を進めます。
たとえば、CLIB.IRLとRLIB.IRLの両方に、fgets関数があって、CLIB.IRLの方のfgets関数にバグがある場合は、次の順にリンクすることで、RLIB.IRLの方のfgets関数をリンクすることができ、バグを回避することができます。
OL TEST,RLIB/s,CLIB/s/gXMAIN
ライブラリ内の配列を図にしたのが次の図です。
太字になっているのがリンクされるモジュールです。
RLIB.IRL | CLIB.IRL |
---|---|
fgets こちらがリンクされる | fgets このファイルが検索される時には、既に、fgetsがリンクされている |
では、CLIB.IRLにfgets関数とバグがあるfgetc関数があり、RLIB.IRL関数には、バグのないfgetc関数のみがあり、プログラムTEST.RELでは、fgets関数を呼び出していて、fgets関数がfgetc関数を呼び出している場合は、どうなるでしょうか?
この場合、下のようなコマンドラインを書いても、バグのあるfgetc関数がリンクされてしまいます。
OL TEST,RLIB/s,CLIB/s/gXMAIN
なぜ、このようになるか、先程と同様に、下の図で説明しましょう。
TEST.REL | RLIB.IRL | CLIB.IRL |
---|---|---|
fgetc この時点では、fgetcは呼び出されていないので、リンクされない。 | fgets このファイルが検索されたあとから、fgetcの検索を始める。 | |
fgetc ここで、初めてfgetcが見つかり、リンクされる。 |
このため、他のライブラリ内のモジュールから呼び出されるモジュールを差しかえる場合は、差しかえるモジュールを呼び出すモジュールも差しかえるモジュールより前にリンクされる必要があります。
先ほどの例では、fgets関数もRLIB.IRLの中でかつ、fgetc関数より前にリンクされるようにする必要があります。
これを図示したのが下の図です。
TEST.REL | RLIB.IRL | CLIB.IRL |
---|---|---|
fgets このファイルが検索されたあとから、fgetcの検索を始める。 | fgets この時点では、既にfgets関数はリンクされている。 | |
fgetc ここで、初めてfgetcが見つかり、リンクされる。 | fgetc この時点では、既にfgetcはリンクされている。 |
SOLiD Cでグローバル変数の配列を定義する場合にextern記憶クラスを利用して配列の定義前に宣言する場合、他のC言語と異なり、配列のサイズまで宣言の際に記述しないとconflict difinitionエラーが出ることになります。
extern char buf[]; /* 配列サイズが宣言されてないためエラーになる */
char buf[128];
extern char buf[128];
char buf[128];
ちなみに、このエラーはコンパイラの第1パスCC1を-xオプション付きで実行してMSX-Cとの互換性を厳密に保つ設定にしても起こるので、SOLiD CはMSX-Cの完全互換ではないと言えます。
ソースファイルを複数に分けてプログラムを製作する場合に、遭遇する可能性があるので、注意する必要があるでしょう。
SOLiD Cでは、引数の数を固定した固定パラメータ関数の引数は、MSX-Cと同様にレジスタとスタックを介して渡されます。
パラメータは、1番目の引数がchar型の場合はAレジスタに、その他の型の場合はHLレジスタに入ります。
同様に、2番目の引数は、DEレジスタに、3番目の引数はBCレジスタに入ります。
char型の場合は、下位バイト(E,Cレジスタ)のみが有効になります。
4番目以降は、スタック上に逆順(つまり、80系の標準的表現で、いわゆるリトルエンディアンって奴)で積まれ、char型のパラメータは、下位バイトに置かれます。
積まれかたは、言語中のデータ長、型の表現と同じです。
関数呼び出し sub((short int)1,'A',(unsigned short int)-3,(char *)0xfbf0);によって、パラメータは次に示されるように設定されます。
HL | 0001H |
DE | 0041H |
BC | 0FFFDH |
(SP+2) | 0F0H |
(SP+3) | 0FBH |
関数から呼び出し側に戻る際、ポインタや、short int型では、HLレジスタに値を入れることで、返り値を返す事が出来ます。
char型では、Aレジスタに値を入れることで、返り値を返す事が出来ます。
アセンブラで関数を記述する際には、アセンブラでは、global宣言で外部シンボル使用の為の宣言を行う必要があります。その際、関数名の後ろに、'_'が必要となります。
MSX-Cを使ってた人にとっては、@と_の違いなので、ここを書きかえれば、比較的簡単にライブラリの移植ができると思います。
例)func関数をアセンブラで書く場合
cseg ;リンカに対し、プログラムであることを示す。
PUBLIC func_ ;関数名+'_'
func_:
ld hl,0
ret
また、レジスタに関してですが、IXレジスタは、保存するようにしてください。
ファイル関連のライブラリが、BDOSコールの際にIXレジスタを保存していたので、何らかの用途に利用しているようです。
SOLiD Cのもう1つの関数呼び出し形式である可変パラメータ関数では、MSX-Cと同様に引数はレジスタとスタックを介して渡されます。
パラメータは、スタック上に逆順で積まれ、引数の数がHLレジスタに入ります。
char型の場合は、下位バイトのみが有効になります。
関数呼び出し sub((short int)1,'A',(unsigned short int)-3,(char *)0xfbf0);によって、パラメータは次に示されるように設定されます。
(SP+2) | 01H |
(SP+3) | 00H |
(SP+4) | 41H |
(SP+5) | 不定 |
(SP+6) | FDH |
(SP+7) | FFH |
(SP+8) | 0F0H |
(SP+9) | 0FBH |
このほかの返り値や関数名の記法、パブリックシンボル宣言、IXレジスタの保存といったところは固定パラメータ関数と同じです。
MSXのタイマー割り込みは、RAMのページ1(4000H-7FFFH)以降に置かないと正常に動作しません。他にも、指定されたアドレスに置かないと正常動作しないルーチンやデータがMSXでプログラムしている場合には存在します。
そこで、任意のアドレスにプログラムを置くことが必要になるのですが、C言語で普通にプログラムを組んでいる場合は使用するアセンブラがリロケータブルアセンブラなので、コードが配置されるアドレスはソースファイルを書く段階で正確には分かりません。
これでは、試行錯誤しないと任意のモジュールを特定のアドレスに置くことができません。
SOLiD C(と言うよりAS & LD)でモジュールをアドレスに置くには、ASに用意されている擬似命令ASEGとORGを使ってM80の用語ではプログラムカウンタモードと呼ばれるものを絶対(アドレス)モードに設定してORG命令を使って絶対アドレスで配置するアドレスを指定する必要があります。
実際の例をここに示します。
aseg ;絶対モードに設定
org 4000H ;4000H以降に以下のプログラムを配置する
intprog:
di
xor a
ld (intflag),a
ei
ret
cseg ;ここからはコード相対モード
このプログラムではラベルintprogからのプログラムを4000H以降に配置します。
リンク自体は他のプログラムと同様にリンクすることができます。
Cで書かれたプログラム本体は、コード相対モードで書かれたアセンブリ言語のプログラムと同様にコードセグメントとして扱うことができるので、リンカの/Pオプションを使って配置アドレスを指定する事ができます。
また、Cのグローバル変数やstatic宣言された変数はデータ相対モードで書かれたアセンブリ言語のデータと同様にデータセグメントとして扱うことができるので、リンカの/Dオプションを使って配置アドレスを指定する事ができます。
リンカの/P,/Dオプションは1回しか指定できないので、MSX-C & M80 & L80のようにC言語で書かれたモジュールをばらばらに配置することはできないようです。
$MEMRYというグローバルシンボルがリンカldでリンクされるモジュールの中にあるとldは($MEMRY)$MEMRYのあるアドレスから2バイトに(ファイルがロードされる先頭アドレス+ファイルサイズ)を入れてくれます。
実際に使う際は、次のプログラムをアセンブリ言語で書いてリンクするようにします。
$MEMRY:: DS 2
SOLID Cではヒープの先頭アドレスとして$MEMRYを使っています。
これを使って、$MEMRYをうまく再設定してやれば、ファイルサイズは必要最小限で、ヒープを高位のアドレスにもっていくことができます。
音源ドライバのデータのような割り込みルーチンから参照されるデータだけを高位のアドレスに配置するのに使えると思います。
SOLiD Cで使われるアセンブラのASはM80との互換性があります。
自分が使っている限りでは、次の擬似命令が利用できました。
擬似命令 | 意味 |
---|---|
CSEG | コードセグメント宣言 |
DSEG | データセグメント宣言 |
ASEG | アブソリュートセグメント宣言 |
ORG | ロケーションカウンタ値の変更 |
EQU | シンボル割り当て |
PUBLIC,:: | パブリックシンボルの定義 |
EXTRN,## | 外部パブリックシンボルの定義 |
DB,DEFB | バイト定義 |
DW,DEFW | ワード定義 |
.Z80 | Z80命令モード選択 |
MACRO ENDM |
この間がマクロとして展開される。 |