この文章は、はじめは、旧本ホームページ MSX C Magazineの、HI-TECH C部屋という、HI-TECH Cに関するコーナーでスタートしました。
その後、HI-TECH C Resource kit #1製作時に加筆・修正し、「HI-TECH Cを256倍(!?)使うドキュメント」として、公開しました。
HI-TECH C Resource kit #1は版を重ねるにつれ、規模が大きくなり、HI-TECH Cを256倍(!?)使うドキュメントのちょっとした加筆・修正程度ではいちいちネットワークにアップロードするとなると時間も電話代もかかり、現実的ではなくなってきました。
そこで、自分のホームページを持ったのを機に、この文章を独立して公開することにした次第です。
本文では基本的に、マニュアルに書いてないことや、英語マニュアルを読むのが面倒で、注意したほうが良い点を中心に書いています。
なお、ここで取り上げるHI-TECH CはFUNET(FTP)やFSW倉庫で配布されているパッケージを取り上げます。開発元のHI-TECH Softwareで配布されているパッケージでも大丈夫だとは思いますが、MSXで利用するには前者のパッケージのほうが良いと思います。
MSX-C Magazine にもどる
Tatsu's MSX Researches トップに戻る
HI-TECH Cは元々、CP/M用に作られたので、ファイルの大きさが128バイト単位に
なるため、テキストファイルの終わりを1AH(EOF)で認識します。ソースファイル・
ヘッダファイルの終わりには、1AH(EOF)を付けなければなりません。
もし、1AH(EOF)を付け忘れると、ソースファイルが正常に認識されず、色々な
不都合が起こります。例えば、P1.COMがillegal character(00H)というエラーを
出したり、更に致命的なところでは、CPP.COMがヘッダファイルのインクルードに
失敗するということもあります。
とくに、他機種でソースファイルを書いて、MSXでコンパイルする人は、
セーブ時にEOFを付ける設定にする等して、気をつけたほうがよいと思います。
HI-TECH Cでは、デフォルトのchar型はMSX-Cとは違い、符号ありのchar型です。
例として、次のプログラムを実行してみましょう。
#include <stdio.h>
int main()
{
char c; /* signed charの変数c */
c = 0xff;
if (c == 0xff){
printf("True\n");
}else{
printf("False\n");
}
return(0);
}
変数cには0xffが入っているにもかかわらず、画面にはFalseと表示されます。
なぜ、こうなるかというと、定数はshort型として扱われるので、if文の中で出てくる式の右辺にある0xffは0xff==255という風に解釈されます。
一方、char型は-128から127までを2の補数で1バイトのメモリに入れているので、0xffは2の補数で-1になります。
その結果、-1 != 255ということで、式の値は偽となります。
この特性に気をつけなければならないのは、MSX-Cからプログラムを移植するときです。MSX-Cでは、char型は、unsigned char型として扱われるので、上記のプログラムの変数cの値は255となり、255 == 255ということで、式の値は真となります。そのため、この特性を考慮してプログラムを書き換えないでそのままコンパイルしてしまうと思わぬ動作不良を起こすことがあります。
他にも、定数とlong型より小さい数を取る型の変数との演算の結果がshort型に収まる範囲を超えることが期待される場合も、定数の後ろにL(long型)、F(浮動小数点数の型)等のマーカーを付け、(例:1234L
)型を明示する必要があるでしょう。
HI-TECH Cのシンボルはアルファベットと、数字、_,$,?といったものが使え、
1文字目に数字が使えないという、オーソドックスなものです。注目したいのは、
('?'). The first character may not be numeric. Upper and
lower case are distinct. The following are all legal and
distinct symbols.
An_identifier
an_identifier
an_identifier1
とあるように、大文字と小文字が区別されるという点です。他機種の
処理系では、当たり前でも、MSXの M80 & L80 を使う処理系(MSX-Cや、
Small-C等)では、実現されないので、素直に喜びましょう。
まず、このコードを見てください。
#define HI_TECH_C 1
#ifdef HI_TECH_C /* HI-TECH C */
#else /* MSX-C */
#pragma nonrec
#endif
このコードでは、#ifdef HI_TECH_Cから#elseまでが有効となり、その後に関しては、無視されるはずです。
しかし、HI-TECH Cではなぜか、#pragma nonrec(MSX-CやLSI-Cの非再帰モード宣言)の部分も解釈されるので、無用な警告を出してくれます。
HI-TECH Cでは、多分#pragma文を他のプリプロセッサ文とは別に、ブロックに関係ない命令として解釈しているので、このような事が起こるのだとは思いますが、せっかくコンパイラ依存の部分を避けたと思ったのに、避けきれないと言うのは気持ちが悪いものです。
HI-TECH Cでプログラムを組んでいると、プログラムには誤りが無いにもかかわらず、CGEN.COMがlarge offset (warning)という警告を出してくることがあります。この警告は何を意味しているのでしょうか?
この謎を解く鍵は、auto変数へのアクセス法にありました。
HI-TECH Cでは、auto変数はスタックからメモリが確保されますが、確保されたメモリへのアクセス方法が、一般の変数と配列変数で異なります。
配列変数では、HLレジスタとDEレジスタでメモリアドレスを計算して、HLレジスタを使ったアドレス指定を使うLD命令でメモリにアクセスします。
これに対して、一般の変数では、IXレジスタとオフセットでアドレスを指定するLD命令で、メモリにアクセスするので、オフセットによるアドレス指定は、128バイト分までしか届きません。
そのため、1つの関数でauto変数が128バイトを超えて、一般の変数が定義されると、
_(変数名) large offset (warning)
という警告が出ます。
例えば、
int i,k[128];
という定義では、k[64]以降は、128バイトを超えますが、kは配列変数なので、large offset 警告は出ません。
しかし、この順番を変えて、
int k[128],i;
という定義を行うと、iは、128バイトを超え、一般変数なので、large offset 警告が出ます。
実際は、auto変数が128バイト以上のスタックから確保されたメモリブロックに存在する場合には、配列変数と同様のアクセス方法が使われるので、問題が出ない場合もありますが、場合によってはプログラムは正しいにもかかわらず、auto変数へのアクセス法で問題が出る可能性もあり得るので、なるべく1つの関数の中のauto変数は128バイト未満に押さえておいたほうが良いでしょう。
HI-TECH Cでは、switch文の式にlong型は書けません。
LSI-C V3.3c試食版ではswitch文の式にlong型を書けたので、同じような気持ちで持っていくとはまってしまいます。
他の処理系からプログラムを持っていく際はこの辺にも気をつける必要がありそうです。
MSXでHI-TECH Cを使っていて、誰もがスピードの遅さなどにイライラすることがあったと思います。
もし、あなたがHDDのような高速な記憶装置と、それを接続するためのMEGA-SCSIなどのようなインターフェイスを持っているとするなら、高速な記憶装置にコンパイラの実行ファイル・ヘッダファイル・ライブラリと言ったコンパイル時に必要なファイルやコンパイル時に使う中間ファイル(ファイル名が$C?.Tとなっているもの)を高速な記憶媒体に移すことで、コンパイルを高速化することができます。
とはいえ、このような充実した環境は簡単にそろえられるものではありませんでした。そこで、サンプルを挙げながらどこを変えるととどのくらい高速化するかについて考察しようと思います。
下に挙げるソースファイルは、入門書に必ず出てくる"Hello,world"の類です。今回の実験では、256K似非RAMを追加した以外はノーマルのFS-A1STをR800CPU,MSX-DOS2で利用した状態で本ファイルをコンパイルした結果をもとに、どこを高速化するとどれだけ速くなるかについて考えてみます。
コンパイル時間に関しては、ストップウォッチを利用して手動で計測したものなので、厳密さには欠けますが、おおよその速度の目安は掴めると思います。
#include <stdio.h>
int main(int argc,char *argv[])
{
printf("This is a sample file.\n");
return(0);
}
環境 | コンパイルにかかった時間 | 短縮した時間 |
すべてのファイルをFDDに置く | 1分42秒 | --- |
上のファイルのうち、テンポラリファイルをRAMDISKに置く | 1分18秒 | 24秒 |
更にライブラリファイル(LIBC.LIB)を似非RAMに置く | 51秒 | 27秒 |
更に実行ファイルをRAMDISKに出力する | 28秒 | 23秒 |
今回のような小さなプログラムのコンパイルでも、記憶媒体を高速なものにするだけで、コンパイル時間がかなり短くなってくるのが分かります。
テンポラリファイルを高速な記憶媒体に置く方法は、プログラムが大きくなるにつれて中間ファイルも大きくなるので、プログラムが大きくなるにつれて有効性が増すのではないかと思われます。
また、ソースファイルを分割する際は、分割数に比例してテンポラリファイルを利用する機会が多くなるので、更に有効性が増すと考えられます。
ライブラリファイルを置く場所や、実行ファイルを出力する場所を高速な記憶媒体にする方法は、オブジェクトファイルをリンクする際に有効となるので、プログラムの規模に関わらずコンパイル速度の高速化が図れるので、ノーマルのA1STのように高速な記憶媒体を使用できる機会が制限される環境で、作成するプログラムが小規模なうちはこちらを優先させるほうが安定したコンパイル速度の向上が図れると思います。
余談ですが、K6 200Mhz,RAM64MのWin98マシンで全部のファイルをHDDに置いてCP/MエミュレータZ/EMを利用して同じサンプルをコンパイルしたときの実行時間が1秒って、反則的なまでに早いですね。
HI-TECH Cでプログラムをしていると、メモリが足りないという旨のエラーメッセージを出してコンパイルができない場合があります。
実際にメモリが足りない場合もありますが、メモリ(TPA)が十分にある場合でも、プログラムの記述ミスによって、このメッセージが出ることもあるようです。
実際にはプログラムの記述ミスなのに、関係なさそうなメッセージが出るので、(実際には内部処理でメモリを不正に浪費していると思われるので、十分関係あるけど。)自分のプログラムは悪くないという錯覚に陥りがちですが、実際はプログラムの方に問題があるということも結構あるので、プログラムをもう一回見直してみてはいかがでしょうか?
HI-TECH CをMSX実機で使っているとメモリ不足に悩まされることがあると思います。
もし、あなたがMSX・FAN誌の付録ディスクなどで、View-Kitを持っているなら、その中のHX.COMを利用することで、必要な宣言だけを行うことができ、その分メモリを節約できることで、メモリ不足が軽減されるのではないでしょうか。
MSX-DOS2に限定される上、専用のテーブルファイルが必要という難点はありますが、誰か挑戦してみてはいかがでしょうか。
HI-TECH Cのライブラリは、(基本的に)1ファイル内に1個の関数がある
OBJファイル(複数の関数が1つのOBJファイルにあってもよいが、メンテナンスの
面で効率が悪い)をLIBR.COMによって結合した物で、LIBR.COMの各コマンドは、
OBJファイル単位で、作用します。
1つのファイル内では、呼出順によって、ライブラリの中の配列を考える
必要があります。(すまん、前は嘘書いてた。)あるモジュールが別の
モジュールを利用するとき、利用するモジュールは、利用されるモジュールより
前に無くてはなりません。
例)outvdp.oがvdpio.oのVDPIOを利用するとき(libr s コマンドの出力イメージ表記)
outvdp.o U VDPIO D _outvdp ←VDPIOを利用する
vdpio.o D VDPIO ←VDPIOを利用される
しかし、LIBR.COMのコマンドの動作はライブラリの中の配列を考えて無い
訳で、(だから間違ったンや←人のせいにするなよ)ライブラリを設計する際に
注意する必要があります。ちなみに、LIBRのRコマンドで、既存のモジュールを
差し替えるときは、モジュールの位置は差し替える前のモジュールの位置と
同じです。この特性はメンテナンスに使います。新規モジュールは
一番後ろになります。
リンカのLINKはコマンドラインの左側から順にライブラリ関数をリンク
しますが、同名のライブラリ関数が複数有る場合は最も左側のファイルに有る
ほうがリンクされます。
例)libf.lib,libc.libにprintf関数が有るとき
LINK -Z -Ptext=0,data,bss -C100H -Ouser.COM CRT.O user.O LIBF.LIB LIBC.LIB
この場合user.comにはlibf.libのprintf関数がリンクされます。
この性質を使うと、libc.libにある標準(的な)関数を自作の関数に
すげ替える事ができます。特殊な使い方をする時や、標準関数に不都合があった
時に使うと良いでしょう。ちなみに、システムでは、LIBF.LIBとLIBC.LIBに
printf,scanf系の関数を入れておき、浮動小数点演算を使うかどうかで、
リンクするモジュールを変えるといったことを行っています。
後ろのライブラリファイル(拡張子.LIB)のライブラリ関数を他の
ライブラリファイルの同名の関数で差し替える時は、差し替える関数の前に
リンクされるファイルが差し替える関数を呼び出す必要があります。
例)libe.lib,libc.libにopen関数が有るとき
LINK -Z -Ptext=0,data,bss -C100H -Ouser.COM CRT.O user.O LIBe.LIB LIBC.LIB
この例で、CRT.O及び、USER.Oの中のモジュールがopen関数を呼び出している
場合は、LIBE.LIBのopen関数が有効になりますが、CRT.O及び、USER.Oの中の
モジュールがopen関数を呼び出しておらず、LIBC.LIBの中にのみあるfopen関数を
呼び出していて、fopen関数がopen関数を呼び出している場合はLIBC.LIBの
open関数を持つモジュールがリンクされます。
しかし、後ろのライブラリファイル(拡張子.LIB)のライブラリ関数を他の
オブジェクトファイルの同名の関数で差し替える時は、差し替える関数の
前にオブジェクトファイル名を書いておけば、差し替える関数の前に
リンクされるファイルが差し替える関数を呼び出す必要はありません。
例)libe.o,libc.libにopen関数が有るとき
LINK -Z -Ptext=0,data,bss -C100H -Ouser.COM CRT.O user.O LIBe.o LIBC.LIB
この例で、CRT.O及び、USER.Oの中のモジュールがopen関数を呼び出している
場合、LIBE.oのopen関数が有効になりますし、CRT.O及び、USER.Oの中の
モジュールがopen関数を呼び出しておらず、LIBC.LIBの中にのみあるfopen関数を
呼び出していて、fopen関数がopen関数を呼び出している場合でもLIBE.Oの
open関数が有効になります。
この性質は標準のライブラリでは、スタートアップのワイルドカード展開
及び、I/Oリダイレクトエミュレーションルーチンを必要に応じてリンクする
際に利用されています。
HITECH-4.PMAの中にある、MSX-C Library互換ライブラリ、LIBGには、色々な
バグがあるようです。例えば、MSXBIOS.Hの記法がおかしく、コンパイル時に
エラーが出たり、引数の処理がおかしく、正常な動作をしない、といったものが
あるようです。特に、これも、MSXのBIOS関連ルーチンに多いようです。
そのため、LIBGを利用する際は、バグの確認や、バグの無い関数を自作したりと
いった事が必要かもしれません。
HI-TECH Cでは、関数の引数は、スタックを介して渡されます。パラメータは、
スタック上に逆順(つまり、80系の標準的表現で、いわゆる
リトルエンディアンって奴)で積まれ、スタックの先頭には戻り番地が
積まれます。char型のパラメータは、下位バイトに置かれます。積まれかたは、
言語中のデータ長、型の表現と同じです。
関数呼び出し sub((short int)1,'A',(unsigned short int)-3,(long int)65536);
によって、パラメータは次に示されるように設定されます。
(SP+2):01H
(SP+3):00H
(SP+4):41H
(SP+5):?? (意味がない)
(SP+6):FDH
(SP+7):FFH
(SP+8):00H
(SP+9):00H
(SP+10):01H
(SP+11):00H
float,double型に関しては未調査ですが、言語中のデータ長、型の表現で
積まれると思います。(未確認)
関数から呼び出し側に戻る際、ポインタや、short int型、char型では、
HLレジスタに値を入れることで、関数値を返す事が出来ます。long型の場合は、
上位2バイトをHLレジスタに、下位2バイトをDEレジスタに入れることで、
関数値を返す事が出来ます。float,double型に関しては未調査です。m(..)m
まあ、アセンブラで、floatやdouble型を使うようなものを作る人は少ないと
思うので、使う人は各自研究してみてください。
char型のパラメーターは下位に置かれ、上の例の(SP+4,5)上位バイトの値は
不定です。この点に注意しないと、char型のパラメーターをアセンブラで
記述した関数に渡す際に思わぬバグの原因になるので、気を付けましょう。
アセンブラで関数を記述する際には、アセンブラでは、global宣言で
外部シンボル使用の為の宣言を行う必要があります。その際、C言語での関数名の前に、
'_'が必要となります。
例)func関数をアセンブラで書く場合
psect text ;リンカに対し、プログラムであることを示す。
global _func ;'_'+関数名
_func: ld hl,0
ret
また、レジスタに関してですが、IX,IYレジスタは、保存するようにして
ください。Cで書かれた関数から、関数を呼び出す際、IX,IYレジスタの値を
保存してない上、呼び出し元の関数が、Cで書かれている場合、呼び出し元の
関数がIXレジスタを使っているためです。この辺は、Cで書いたプログラムのコンパイル後の
アセンブラソースを見るとよいでしょう。
Cで書いた関数では、csvというルーチンを使っているようですが、
IXレジスタ経由での引数のアクセスはスピードが遅いので(ニーモニック表を
見てみよう!)なるべく使わない方が良いでしょう。
csvを使うときは、始めに、csv,cretを外部シンボル宣言(global csv,cret)し、
関数の冒頭で、csvをコール(call csv)し、関数から戻る際に、cretに
ジャンプする(jp cret)必要があります。この時、IX,IY両レジスタは保存されます。
csvを使ったときのパラメータのアクセス法は、ixレジスタの値にオフセットを
加えた値の指すアドレスを使って、アクセスすることになり、一番最初の
パラメータが(ix+6)に入り、その後は、上位アドレスに向かってパラメータが
積まれるという形になっています。
例).関数呼び出し sub((short int)1,'A',(unsigned short int)-3,(long int)65536);
(IX+6):01H
(IX+7):00H
(IX+8):41H
(IX+9):?? (意味がない)
(IX+10):FDH
(IX+11):FFH
(IX+12):00H
(IX+13):00H
(IX+14):01H
(IX+15):00H
ライブラリのソースファイルを見ると、アセンブラで書かれたソースが沢山あります。どうやって、ライブラリになっているかと言うと、これらのファイルはZAS.COMで
アセンブルされて、librで結合されることでライブラリになっています。
この事から分かるように、HI-TECH Cはリロケータブルアセンブラを含んでいます。
では、「私は、C言語は分からないけど、アセンブラは分かるので、
HI-TECH Cをアセンブラとして使いたい」ということはできるのでしょうか?
答えは、もちろんイエスです。では、アセンブラとして、HI-TECH Cを使う場合、
どのようにして、使えばよいかについて、サンプルを挙げて説明します。
今回は、立ち上げると文字列を表示する初歩的なサンプルを使い、
アセンブラのみで、MSX-DOSのCOMファイルを作るにはどうすればよいかについて
説明します。
HI-TECH Cを単なるリロケータブルアセンブラとして使うときは、
最低限ZAS.COMとLINK.COMのみを使える状態にすれば良いです。まあ、
ライブラリアンLIBR.COMやその他のプログラムが有ると便利なので、
フルインストールするにこしたことはありませんが。
まずは、普通に、ソースプログラムを書きます。
プログラム本体を書く前には、'psect text'と書いて、アセンブラに、
ここからプログラムエリアだと教えてやります。データを書く前には、
'psect data'と書いて、アセンブラに、ここからデータエリアだと教えてやります。
また、グローバルシンボルを使うことで、他のモジュールから、モジュール内の
ルーチンを呼び出したり、モジュール内のアドレスを参照したりといったことが
できます。この時は、呼び出すモジュールと呼び出されるモジュールの両方で、
global ラベル名
と書くことで、グローバルシンボルを使うことができます。この宣言をしない
ラベルはソースファイルの中だけで、有効です。要するに、Cのextern宣言、
外部関数の宣言のようなものです。
後は、一般的なアセンブラの記法で、書くことができます。
この規則に則って書いたプログラムが、下のTEST.ASです。
-TEST.AS
psect text ;ここからはプログラムです。
global sample ;グローバルシンボルsampleを定義します。
BDOS equ 0005H
jp sample ;間違えて、MS-DOSで実行しても安心(^^)
sample:
ld de,str
ld c,09H
call BDOS
ret
psect data ;ここからはデータなんです。
str:
defm 'Hello,world.'
defb 0dh,0ah,'$'
end
[EOF]
ソースファイルを書いたら、アセンブル、リンクを行います。
アセンブルには、ZASを使います。基本的には、
zas ソースファイル名
で大丈夫ですが、-oオプションで、出力するリロケータブルオブジェクト
ファイル名を指定することができます。
リンクは、LINKを使います。ここで、注意したいのは、-Cオプションと、
-Pオプションです。まず、-Cオプションは出力する実行ファイルの開始番地を
指定します。MSX-DOSでは、100Hですので、-C100Hと指定します。次に、
-Pオプションですが、どこに、プログラムエリアやデータエリアが置かれるかを
指定するものです。プログラムエリアの場所は、-Pの後にtext=[置きたいアドレス]で
指定します。同様に、データエリアの場所は、data=[置きたいアドレス]で
指定します。ここで、=以下が無い場合は、前に、指定したエリアの後に連続して
配置されます。
出力されるファイル名は、-Oオプションで指定します。出力されるファイルは
メモリ上に配置されるイメージファイル(要するに、MSX-DOSのCOMファイルの
ようなもの)です。
-C,-P,-Oオプションは必ず指定します。
この手順を示したのが、下のMKTEST.BATです。
このサンプルでは、TEST.ASをアセンブルして、TEST.Oを出力し、TEST.Oを
リンクして、MSX-DOSの実行ファイルTEST.COMを作成します。MSX-DOSの
実行ファイルを作成するので、プログラムの配置されるアドレス=実行ファイルの
開始番地=100H、データエリアは、プログラムエリアのすぐ後という事にします。
-MKTEST.BAT
rem アセンブルを行なう。
ZAS -otest.O test.as
rem リンクを行なう。
LINK -Z -Ptext=100H,data -C100H -Otest.COM test.O
[EOF]
さて、どこから、プログラムが実行されるかと言うと、MSX-DOSの場合、
当然100Hからですので、プログラムエリアの先頭番地=実行ファイルの開始番地と
なる必要があります。では、どのプログラムエリアから実行されるでしょうか?
答えは、一番最初に読み込んだ(コマンドラインの一番左の
オブジェクト/ライブラリファイル)の一番最初のプログラムエリアからです。
今回のサンプルでは、jp sampleとあるところからです。
HI-TECH Cのライブラリでは、一番最初のオブジェクトファイル(CRT.O)に
スタートアップルーチンを置いています。他に考えられる使い道としては、
カスタマイザXMARK/さやかにプログラムを対応させると言ったことが挙げられます。
これで、とりあえず、HI-TECH Cを単なるマクロアセンブラとして使うことが
出来るようになりました。ZAS.COM&LINK.COMには他にも全部述べようとすると
イヤになるくらい色々な機能があるので、マニュアルを見ながら、試行錯誤して、
機能を使いこなせるようになってください。
MSXのタイマー割り込みは、RAMのページ1(4000H-7FFFH)以降に置かないと正常に
動作しません。他にも、指定されたアドレスに置かないと正常動作しないルーチンや
データがMSXでプログラムしている場合には存在します。
しかし、HI-TECH Cで普通にプログラムを組んでいる場合は使用するアセンブラが
リロケータブルアセンブラなので、どこに、コードが配置されるかはソースファイルを
書く段階では、分かりません。これでは、試行錯誤しないと任意のモジュールを
特定のアドレスに置くことができません。
ソースファイルを書く段階で、任意のモジュールを特定のアドレスに置くには、
アセンブラのORG擬似命令を使います。ORG擬似命令は、
ORG アドレス
と書くことで、以降に続くプログラムやデータを引数のアドレス以降に置くように
指定できます。
ORG命令を使った例が下のプログラムです。ラベルstr以降はtext psectとして
200H以降に、ラベルstr2以降はtext psectとして180H以降に実行時にメモリに
ロードされます。
また、org命令の効果を無効にする場合には、psect命令を書いたり
(ラベルstr3以降)、org命令でアドレスを指定したりします。ラベルstr3以降は
text psectとしてラベルrealstr3以降に配置されます。
-ここから
psect text
global sample
BDOS equ 0005H
sample: ;ORG命令が無い場合はリンカの-Pオプションで
;指定されたアドレス以降に配置される。
ld de,str2 ;間違えてMS-DOSで実行すると悲惨なことになるかも
ld c,09H
call BDOS
ret
realstr3:
org 200H ;ORG命令でアドレス指定(200H以降)
str:
defm 'ORG 200H'
defb 0dh,0ah,'$'
org 180H ;この先は180H以降に配置される。
str2:
defm 'ORG 180H'
defb 0dh,0ah,'$'
psect text ;ORG命令によるアドレス指定をリセット
str3:
defm 'Where I stay?'
defb 0dh,0ah,'$'
end
-ここまで
このプログラムをtest2.asというファイル名でセーブして、下の手順を経て、
test2.comというファイルでセーブしたとします。
-ここから
ZAS -J -N -otest2.O test2.as
LINK -Z -Ptext=100H -C100H -Otest2.COM test2.o
-ここまで
するとメモリ上ではこのようになります。
0100 11 80 01 0E 09 CD 05 00 C9 57 68 65 72 65 20 49 .....ヘ..ノWhere I 0110 20 73 74 61 79 3F 0D 0A 24 1A 1A 1A 1A 1A 1A 1A stay?..$....... 0120 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A ................ 0130 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A ................ 0140 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A ................ 0150 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A ................ 0160 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A ................ 0170 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A 1A ................ 0180 4F 52 47 20 31 38 30 48 0D 0A 24 14 47 14 48 14 ORG 180H..$.G.H. 0190 49 14 4A 14 4B 14 4C 14 4D 14 4E 14 4F 14 50 14 I.J.K.L.M.N.O.P. 01A0 51 14 52 14 53 14 54 14 55 14 56 14 57 14 58 14 Q.R.S.T.U.V.W.X. 01B0 59 14 5A 14 5B 14 5C 14 5D 14 5E 14 5F 14 60 14 Y.Z.[.\.].^._.`. 01C0 61 14 62 14 63 14 64 14 65 14 66 14 67 14 68 14 a.b.c.d.e.f.g.h. 01D0 FF FF 6A 14 6B 14 6C 14 6D 14 6E 14 6F 14 70 14 ..j.k.l.m.n.o.p. 01E0 71 14 72 14 73 14 74 14 75 14 76 14 77 14 78 14 q.r.s.t.u.v.w.x. 01F0 79 14 7A 14 7B 14 7C 14 7D 14 7E 14 7F 14 80 14 y.z.{.|.}.~..... 0200 4F 52 47 20 32 30 30 48 0D 0A 24 1A 1A 1A 1A 1A ORG 200H..$.....
イメージ的にはPSECTとORG命令を使って、ブロックを形成して、ブロック毎に
リンカが配置を行うと考えると良いと思います。これを利用すると、
アセンブラレベルで配置アドレスに依存するプログラムを組むことができます。
更に、指定するアドレスを重ね合わせることができたりもします。ただ、
混乱を招きやすいので、あまり行なわないほうが良いでしょう。
まあ、わざと重ね合わせてソースを見ても他人には分からないように
したり、ジャンプ命令やコール命令に重ねあわせて、デバッグ用に使うといった
用途は考えられますが。
C言語のプログラムの場合は、#asm命令を利用して、生成されたアセンブラソースに
ORG命令を埋め込むことが考えられます。この場合、PSECT命令でモジュールが
分断されるとORG命令の効力が無くなり、暴走の原因となるので生成された
アセンブラソースを見るなど、十分に気を付ける必要があります。具体的には、
グローバル変数を書く場所に気を付けたり、関数内で静的(static)変数を
書かないと言ったことが挙げられます。
前の項では、org命令を使ってアセンブリ言語で書かれたモジュールを任意のアドレスに置く方法について、書きました。
この方法を使って配置したモジュールのアドレスは、PSECT命令によるアドレス管理とは独立して管理されるため、任意の位置にモジュールを置くことができますが、その反面、PSECT命令によって管理されるモジュールとアドレスが重なっても何の警告も出ないので、暴走の原因となります。
たとえば、C言語で書かれて、PSECT命令により、コードエリアが100Hから始まるモジュールSAMPLEC.Oとアセンブリ言語で書かれてORG命令により、4000H以降にオブジェクトが配置されるモジュールSAMPLEA.Oというモジュールをリンクして、実行形式ファイルSAMPLE.COMを作成するとします。(下記のコマンドライン)
LINK -Z -Ptext=100H -C100H -OSAMPLE.COM SAMPLEC.O SAMPLEA.O
ここで、SAMPLEC.Oのサイズが大きくなり、TEXTモジュールが3F00H以上のサイズになった場合、リンカが、SAMPLEC.Oの4000H以降の内容をSAMPLEA.Oで上書きするため、4000H以降のプログラムが実行された場合、暴走することになります。
この場合、症状としては、途中までは正常に実行されていたプログラムが突如暴走すると言った症状が現れます。
対策としては、PSECT命令でアドレスが管理されるモジュールを小さくするなり、ORG命令を使うモジュールの配置を換えるなどして、モジュールの上書きが起こらないよう、気を付ける必要があります。
HI-TECH Cでは、大きく分けて、5つの方法でメモリを管理しています。
1つめは、data psectによるもので、初期値のあるグローバル変数や、初期値のある関数内のstatic指定された変数がこのグループに属し、ブロック化されます。このグループの変数のメモリアドレスは、リンカの-Pオプションがある場合は、(-Pdata=1000H)-Pオプションで指定したブロックの先頭アドレスに、アドレス指定が無い場合、text psectの直後のアドレスに配置されます。
2つめは、bss psectによるもので、初期値の無いグローバル変数や、初期値の無い関数内のstatic指定された変数がこのグループに属し、ブロック化されます。このグループの変数のメモリアドレスは、リンカの-Pオプションがある場合は、(-Pbss=1000H)-Pオプションで指定したブロックの先頭アドレスに、アドレス指定が無い場合、data psectの直後のアドレスに配置されます。
3つめはスタックを利用したもので、auto変数がこのグループに属します。関数が呼ばれた際に、内部関数ncsv関数によって、スタックから変数のための領域が確保され、内部関数cret関数によって開放されます。
4つめは、アセンブラのorg命令でアドレス指定したメモリ領域です。メモリアドレスは、org命令の指定と、以降のプログラムコードの命令長、データ長で決定します。
最後になるのが、ヒープから取得されるメモリで、sbrk,malloc,calloc関数によって取得されるメモリブロックがこの方法によって管理されます。malloc,calloc関数を実行すると、ヒープの先頭からスタックに向かって、より高位アドレスのメモリを取得するようになります。ヒープの先頭は、システム定義シンボル__Hbssで示されるbss psectの終わりになります。
これを図式したものが次の図です。
下位アドレス 0000H ------------ |MSX-DOS | | | 0100H -> ------------ | TPA | | | -Ptext=xxxx -> ------------ |text psect| | | -Pdata=xxxx -> ------------ |data psect| | | -Pbss=xxxx -> ------------ |bss psect | | | __Hbss -> ------------ |ヒープ | | | | | |/ | | | | | | /| | | | | -Pbss=xxxx -> ------------ |スタック | |(auto変数)| -Pbss=xxxx -> ------------ |システム | |ワークなど| 0FFFFH ------------ 上位アドレス
C言語には、ユーザーがC言語やアセンブラから呼び出すライブラリのほかに、システムが呼び出すランタイムライブラリがあります。
ランタイムライブラリは、C言語で書かれたプログラムが内部で呼び出す関数群で、Z80には存在しない乗算や除算、浮動小数点演算などを実行してくれます。
内部で利用するとは言っても、一般のライブラリと同様の関数なので、global宣言をしてやれば、ユーザーが作るアセンブリ言語のプログラムから呼び出すこともできます。
ただ、掛け算や割り算などの基本的な演算は頻繁に呼び出されるので、いちいちパラメーターをスタックを使って渡していては、実行速度が低下するので、パラメーターをレジスタを使って渡したりするなど、コール法が一般の関数とは異なっています。
たとえば、16bitの掛け算、割り算/剰余は、次のような形で行うことができます。
これらの関数を使うと、アセンブリ言語で書かれたプログラムから簡単に掛け算や割り算などの複雑な演算ができます。(私自身もアセンブリ言語で掛け算や割り算をする方法は知りません^^)。
また、C言語で書かれたプログラムと演算ルーチンを共用できるので、省メモリ化にも役立ちます。
HI-TECH Cでは、インラインアセンブラを使うことで、1つのソースファイルにC言語で書いたプログラムとアセンブリ言語で書いたプログラムを混在させることができますが、この場合、コードオプティマイザOPTIM.COMは動作しないようです。
HI-TECH Cは、元々は、CP/M用ですが、CP/Mエミュレーターでの動作が確認されました。今回は、その方法について、MS-DOSマシンと、CP/MエミュレータZ_EMを題材にして説明いたしましょう。
(CPM.EXEではなぜか正常に動作しなかった。)
まずは、MS-DOSマシンと、CP/MエミュレータZ_EMを用意し、Z_EMの実行ファイルのあるディレクトリに、パスをかけて、いつでも、Z_EMを立ちあげられるようにしましょう。
次に、HI-TECH Cの専用ディレクトリを作り、そこに、HI-TECH C本体(HITECH-1.PMA)をアーカイブから展開しましょう。ちなみに、今回は、MSX-DOSじゃないので、パッチをかける必要はありません。
次に、C.COMをCP/Mエミュレータ環境で使えるように改良してしまいましょう。
C.C(C.COMのソース)の
static char * paths[] =
{
"LINK",
"OBJTOHEX",
"CGEN",
"OPTIM",
"CPP",
"ZAS",
"LIB",
"P1",
"CRT.O",
"$EXEC",
"CREF",
};
となっている所を、
static char * paths[] =
{
"z LINK",
"z OBJTOHEX",
"z CGEN",
"z OPTIM",
"z CPP",
"z ZAS",
"LIB",
"z P1",
"CRT.O",
"$EXEC",
"z CREF",
};
として、CP/Mエミュレータを実行して、その上で、各コンパイラの実行ファイルを実行するようにしてしまいましょう。(zとあるのは、Z_EMの実行ファイルです。)
さらに、cmdrun関数を単にプログラムの実行を終了する関数に書き換えてしまいましょう。cmdrun関数はMSXのキーバッファを使うので、CP/Mエミュレータ上で実行すると暴走する恐れがあるので、物騒な物は取ってしまいます。
書き換え例)
cmdrun(s)
char *s;
{
exit(0);
}
次は、いよいよ、C.Cのコンパイルです。
HI-TECH Cをインストールしたドライブ・ディレクトリで、
Z C -Q C[RET]
と入力します。
ここでできた、$exec.batを
REM ここから
z CPP -DCPM -DHI_TECH_C -Dz80 -I C.C $C1.T
z P1 $C1.T $C2.T $C3.T
Pause If error occur Hit CTRL-C
z CGEN $C2.T $C1.T
z ZAS -N -oC.O $C1.T
DEL $C1.T
DEL $C2.T
DEL $C3.T
z LINK -Z -Ptext=0,data,bss -C100H -OC.COM CRT.O C.O LIBC.LIB
DEL C.O
REM ここまで
と書き換えて、書き換えた$exec.batを実行します。
これで、全て、準備は完了しました。
後は、実際に実行する時には、z C (プログラムのファイル名、拡張子なし)[RET]
として、できた$exec.batを実行するとプログラムのコンパイルが行えます。
後は、HI-TECH CやCP/Mエミュレータのマニュアルを読みながら、好きにして下さい。
これで、HI-TECH Cの開発環境をMS-DOSマシン上に持って行く事ができました。
この方法の利点は、MS-DOSマシン上で開発環境を構築するので、比較的楽に、ハードディスクや、RAMディスク、ディスクキャッシュなどの恩恵を受けることができる、ソースファイルを書くテキストエディタなどにMSXより、高機能なソフトを使う事ができる、高速CPU(486とか、Pentium)と高速なエミュレータがある場合、下手にZ80でHI-TECH Cを動かすより、早く、コンパイルができるといった利点があります。
最近は、Windowsマシンも、速い物が昔より安く手に入るので、皆さんも試してみてはいかがでしょうか?
Ken Hasegawa氏によるWin32用CP/Mエミュレータz80+cpmにおいてもHI-TECH Cが動作したのでこちらについても説明を行ってみましょう。
まず、HI-TECH Cの専用ディレクトリを作り、そこに、HI-TECH C本体(HITECH-1.PMA,LINK.LZH)をアーカイブから展開しましょう。ちなみに、MSX-DOSじゃないのでパッチを当てる必要はありません。
次に、同じディレクトリにz80+cpmのアーカイブの内容を展開しましょう。
インストールはこれで終了です。
まず、MS-DOSプロンプト(Windows 95/98/Me)やコマンドプロンプト(Windows NT/2000/XP)を起動してください。
次に、次のコマンドを打ち込んで先ほどのインストールを行ったディレクトリに移動してください。(C:\HITECHCにインストールしたと仮定します。)
C:[Enter]
CD HITECHC[ENTER]
では、まずはz80+cpmサンプルのhellow.cをコンパイルしてみましょう。
コンパイルするには次のように打ち込みます。
hcc hellow.c[ENTER]
コンパイルが終わるとhellow.comというファイルができています。
これをMSX実機に持っていってMSX-DOSから実行するとメッセージが表示されます。
hcc.exeはz80+cpmに付属するc.comの代替コンパイラドライバです。
使い方についてはhccとだけ入力して表示されるヘルプを参照してください。
ちなみに、-lオプションの後ろにつけられるのは1文字だけなので、気をつけましょう。
大規模なプログラムをコンパイルする際はHI-TECH Cの実行ファイルやライブラリファイルとは別なディレクトリにソースファイルや生成されるファイルを置きたいと思うことでしょう。その場合の方法について説明します。
hccを使ってコンパイルする際に、-vオプションをつけてコンパイルしてみてください。どのようなコマンドを呼んでコンパイルするかが分かります。
下記は、hcc.exeが実行する先ほどのz80+cpmサンプルhellow.cのコンパイル時のコマンドです。
cpm -qh a:cpp -DCPM -DHI_TECH_C -Dz80 -Ia: hellow.c $CTMP1.$$$
cpm -qh a:p1 $CTMP1.$$$ $CTMP2.$$$ $CTMP3.$$$
rm $CTMP1.$$$
cpm -qh a:cgen $CTMP2.$$$ hellow.as
rm $CTMP2.$$$
cpm -qh a:zas -n -ohellow.obj hellow.as
rm hellow.as
cpm -qh a:link -z -Ptext=0,data,bss -C100H -ohellow.com a:crtcpm.obj hellow.obj a:libc.lib
rm hellow.obj
ここで、エミュレーションで実行するHI-TECH Cの実行ファイル名および、ライブラリファイル名にa:と書かれていることが分かります。
z80+cpmではエミュレートされるCP/Mプログラムが、Aドライブを指定してアクセスしてきたときはcpm.exeの存在するディレクトリをAドライブとして扱うようにしています。
この指定はcpm.drvというテキストファイルをcpm.exeと同じディレクトリに置いて(CP/Mのドライブ名:)=(Windowsマシンのドライブ:ディレクトリ名)という風に書くことで変更することができます。
先ほどのインストール例では次のようになります。
a:=C:\HITECHC
また、ドライブ指定がない場合はカレントディレクトリをカレントドライブとして扱います。
これを利用して、ユーザーのファイルをHI-TECH Cのファイルとは異なるカレントディレクトリに置いてコンパイルすることでソースファイルとHI-TECH Cの実行ファイル・ヘッダファイル・ライブラリファイルといったファイルを分離することができます。
あとは、いろいろと試行錯誤してコンパイルしやすい環境を構築してみましょう。
HI-TECH Cでは、コンパイルの際、ソースファイルをプリプロセッサCPP.COMにかけた後構文解釈、アセンブラコード生成...という過程を経て、最終的に実行可能なファイルを作ります。
プリプロセッサが独立しているということは、プリプロセッサを他のものに変えることができる可能性があると言うことを意味しています。もし、HI-TECH Cのプリプロセッサより高性能・高機能なプリプロセッサが利用できるとするなら、ぜひ使いたい所ですよね。
そこで、私のCP/Mエミュレータを利用したHI-TECH C開発環境でプリプロセッサをMS-DOS版LSI-C 3.3C試食版のものに変えてコンパイルを行ってみた所、何の問題もなくコンパイルを行うことができました。これにより、インクルードファイルのディレクトリを分離することができ、ファイルの整理がしやすくなりました。
ただ、問題としては、HI-TECH C特有のプリプロセッサ文の処理に問題が残るといったことが挙げられます。LSI-C 3.3C試食版のプリプロセッサでは、自分が試したところでは、#asm
文が正常に処理できませんでした。
また、プリプロセッサにより、コマンドラインの書式が異なるので、コンパイラドライバを利用するプリプロセッサの書式に対応するよう、再コンパイルする必要が生じる場合があるかもしれません。
とはいえ、HI-TECH Cのプリプロセッサは差し替えが利くので、もし実行に移せる環境があり、処理速度が向上する、ヘッダファイルを独立したディレクトリに置けるといったメリットがあるので、実行に移してみるのはいかがでしょうか?
HI-TECH Cが動作するCP/Mエミュレータにはどのようなものがあるのでしょうか?
私が調べた範囲で、動作したかどうかについてあげてみます。
エミュレータ | 動作したかどうか |
---|---|
CP/M program EXEcuter CPM.EXE V0.6 | 不可 |
Z_EM Ver.2.0 | 動作 |
MSX-DOS Emulator Ver.0.08 | 不可 |
z80+cpm | 動作 |
CP/Mシミュレータ Ver 3.0に関しては、動作したとの報告を受けているのですが、私のほうでは未確認です。