MSX C Magazine SOLiD C部屋

 このページは、ロシアの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用の書籍が役に立つこともあります。

御意見、御感想、御希望等ありましたら何でもE-Mailでどうぞ

Message board(Tatsu個人及びMSXのつどいと共用です。)

This page is presented by Tatsuhiko Syoji(rxk16664@nifty.ne.jp)

なぜ、SOLiD Cか?

 なぜ、筆者はSOLiD Cを推奨しているのでしょうか?
 大きく分けて、次の2点が挙げられます。

  1. フリーである
     SOLiD Cは入手にコストがかからないという上で、フリーであり、誰でも、Internetから入手することができます。
     また、利用目的についても同人ソフトなどの商用、フリーソフトのような非商用に関わらず自由です。
  2. 強力なコード生成と高速コンパイル
    C言語徹底比較2の結果を見るように、SOLiD Cは高速でコンパクトなコードを生成します。
     開発中は、何回もコンパイルを行い、バグを取った上で、プログラムを完成させていきますが、コンパイル時間が短いということはその分、開発時間が短くて済みます。

 これらの特徴から、筆者としては、ゲームや、比較的簡単なツールにSOLiD Cを利用するのが有用と考え、こうして、使い方のコツを紹介するに至りました。


C言語

DOS2上で気をつけること

 MSX-DOS2上で、SOLiD Cを利用する際に、コマンドラインからは、うまく起動するけど、同じ事をバッチファイルから行おうとすると、うまく動作しなかったり、*** Not enough memoryエラーが出て、そもそも起動しなかったりということが起こることはないでしょうか?
 その場合、BUFFERS命令を利用して、システムが利用するディスクバッファの量を減らすと、正常動作することがあります。
 私の環境(FS-A1ST+MEGA-SCSI+540M MO、MSX-DOS2)では、BUFFERS=19としていたところを10にすることで、動作が安定してきました。

LONG型の真実

 低水準入出力の_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型とその他の型で、利用するレジスタが異なるので、引数の型を明示しないと、正常な動作が期待できなくなります。

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現在では出てないようなので、とりあえずは、私が修正したライブラリを使ってみてください。。

未解決のバグ

 これらのバグに関しては、まだ解決していないので、プログラムを組む際は注意して下さい。

可変パラメータ関数の書き方(C言語編)

 SOLiD Cの関数には、引数の型や個数が固定されている、固定パラメータ関数と、引数の型や個数が不定の可変パラメータ関数があります。
 固定パラメータ関数では、頻繁に使うであろう引数をレジスタを使って渡すので、アセンブラでの関数記述が簡単になり、実行速度が速くなったり、スタックから引数を取り出すオーバーヘッドがなくなるので、その分、できた関数のプログラムががコンパクトになるという利点があります。
 SOLiD Cの関数定義はデフォルトでは、固定パラメータ関数となり、パフォーマンスの向上が図られています。
 一方、可変パラメータ関数は、引数の型や個数が不定の場合に使われます。

 可変パラメータ関数を記述する際は、次のようにします。

  1. まず、本体定義の前に、int sample(.)と、宣言して、呼び出す側に、可変パラメータ関数であることを明示します。
     この宣言を行わないと、関数は固定パラメータ宣言として扱われます。
  2. 次に、実際の関数の本体定義では、第1引数に、実際の引数の数がint型で、第2引数に、1番目のパラメータ本体が入るので、2番目以降のパラメータは、次のアドレスに入るので、ポインタを使ってアクセスします。
     実パラメータ(呼び出す側から見たパラメータ)とそのアドレスの関係について下の表に挙げます。
実パラメータ アドレス
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.IRLCLIB.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.RELRLIB.IRLCLIB.IRL
fgetc
この時点では、fgetcは呼び出されていないので、リンクされない。
fgets
このファイルが検索されたあとから、fgetcの検索を始める。
fgetc
ここで、初めてfgetcが見つかり、リンクされる。

 このため、他のライブラリ内のモジュールから呼び出されるモジュールを差しかえる場合は、差しかえるモジュールを呼び出すモジュールも差しかえるモジュールより前にリンクされる必要があります。
 先ほどの例では、fgets関数もRLIB.IRLの中でかつ、fgetc関数より前にリンクされるようにする必要があります。
 これを図示したのが下の図です。

TEST.RELRLIB.IRLCLIB.IRL
fgets
このファイルが検索されたあとから、fgetcの検索を始める。
fgets
この時点では、既にfgets関数はリンクされている。
fgetc
ここで、初めてfgetcが見つかり、リンクされる。
fgetc
この時点では、既にfgetcはリンクされている。

extern

 SOLiD Cでグローバル変数の配列を定義する場合にextern記憶クラスを利用して配列の定義前に宣言する場合、他のC言語と異なり、配列のサイズまで宣言の際に記述しないとconflict difinitionエラーが出ることになります。

NG


extern char buf[]; /* 配列サイズが宣言されてないためエラーになる */

char buf[128];

OK


extern char buf[128];

char buf[128];

 ちなみに、このエラーはコンパイラの第1パスCC1を-xオプション付きで実行してMSX-Cとの互換性を厳密に保つ設定にしても起こるので、SOLiD CはMSX-Cの完全互換ではないと言えます。
 ソースファイルを複数に分けてプログラムを製作する場合に、遭遇する可能性があるので、注意する必要があるでしょう。


アセンブラ

アセンブラでの関数の書き方(その1、固定パラメータ関数)

 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レジスタを保存していたので、何らかの用途に利用しているようです。

アセンブラでの関数の書き方(その2、可変パラメータ関数)

 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をうまく再設定してやれば、ファイルサイズは必要最小限で、ヒープを高位のアドレスにもっていくことができます。
 音源ドライバのデータのような割り込みルーチンから参照されるデータだけを高位のアドレスに配置するのに使えると思います。

ASの使い方

 SOLiD Cで使われるアセンブラのASはM80との互換性があります。
 自分が使っている限りでは、次の擬似命令が利用できました。

擬似命令 意味
CSEG コードセグメント宣言
DSEG データセグメント宣言
ASEG アブソリュートセグメント宣言
ORG ロケーションカウンタ値の変更
EQU シンボル割り当て
PUBLIC,:: パブリックシンボルの定義
EXTRN,## 外部パブリックシンボルの定義
DB,DEFB バイト定義
DW,DEFW ワード定義
.Z80 Z80命令モード選択
MACRO
ENDM
この間がマクロとして展開される。