sdccを使ってCソースをコンパイルする際は幾つか注意する点がある。
ROM用のバイナリを作るのか、MSXDOS用の実行形式、あるいはBASICのBLOAD形式のバイナリの
何れかの実行バイナリを作成するのかにより
コンパイルオプションが異なる。
ここではMSXDOS上で動作する実行バイナリを例に説明する。
MSXDOS実行形式を前提とする場合、コンパイラのコマンドは以下となる。
(codeとdataを別アドレスに明示的に分離する場合は--data-loc 0x300など指定)
> sdcc --no-std-crt0 --code-loc 0x100 --data-loc 0x100 -mz80 source.c
次に作成されたIntelHEX形式(*.ihx)のファイルをバイナリ形式に変更する。
この時メモリーアドレスを4kb以内とするので-sオプションを指定する。
(4kb以上のバイナリであれば4096を変える)
> makebin -s 4096 source.ihx > source.bin
このままでは作成されたバイナリは不要なメモリアドレス部分(0x0-0xff)が残っているので、
perl等のスクリプトで必要なアドレスのみを切り出して最終的な実行バイナリを作成する。
> perl binout.pl > source.com
このバイナリカッタはperlスクリプトで適当に作る。以下を参照。
open(IN,"source.bin");
binmode(IN);
$i=0;
while(eof(IN) == 0){
read(IN,$buf,1);
if($i<=0xff){
} else {
print $buf;
}
$i++;
}
close(IN);
このバイナリカッタにより作成されたバイナリがMSXDOS用の実行バイナリとなる。
多分このバイナリで動く。
GUIのバイナリ編集ツールを使うこともできるが
同様のスクリプトをPerlではなくWindows上VBScriptで使う場合は下の概念スクリプトをbinout.vbsで保存し
DOSプロンプト上からコマンド入力すればよい。
>cscript //nologo binout.vbs hoge.bin > hoge.com
'先頭256byteをカットするバイナリ編集VBSスクリプト
'引数を得る
set args=WScript.Arguments
if args.count=0 then
WScript.quit(1)
end if
set objfs=CreateObject("Scripting.FileSystemObject")
set fd=objfs.OpenTextFile(args(0))
i=0
do until(fd.AtEndOfStream)
buff=fd.Read(1)
if (i>&hff) then
WScript.StdOut.write buff
end if
i=i+1
loop
fd.close
しかしVBSはテキストファイルしか扱えないためバイナリデータが処理できない。
bin形式からcom形式(一般的なCPM実行ファイル)に変換するVBSスクリプトを下に示す。
'引数を得る
set args=WScript.Arguments
'
if args.count=0 then
WScript.echo "usgae:"
WScript.echo " cscirpt makecom.vbs filename(.bin)"
WScript.quit(1)
end if
dim fin
dim fout
dim lcount
set fin=CreateObject("ADODB.Stream")
set fout=CreateObject("ADODB.Stream")
fin.Type=1
fin.Open
fin.LoadFromFile(args(0)+".bin")
fout.Type=1
fout.Open
lcount=0
do until fin.EOS
buff=fin.Read(1)
if (lcount>&hff) then
fout.Write buff
end if
lcount=lcount+1
loop
fout.SaveToFile args(0)+".com",2
コマンド引数は以下で拡張子は指定なし(hoge.binというバイナリをcom形式にする)
>cscript //nologo makecom.vbs hoge
sdccの標準CライブラリはMSXでも動く。ただしprintfやputcなどを使い文字列を表示する
場合は、MSXDOSのシステムコールに対応するようにライブラリを一部書き換える必要がある。
コンパイラオプションで--no-std-crt0 を指定するとランタイムが無効となる。このsdccの
stdcrt0ランタイムとは、Z80組み込みボード用の初期化コードと割り込みハンドラのアセンブラコード。
MSXではシステムが既に割り込みベクトルや起動手順を処理しているので必要無い。
sdccにはライブラリとしてC標準関数や数学関数ライブラリ以外は存在しない。
VDP関数やBIOSコール関数などは自作する事。
C言語では幾つかの定数をstaticやコード内で初期化することがある。例えば以下のように
配列を予め初期化しておくなどの場合だ。
unsigned char str1[]="abc123";
このようにCソース内で初期化される文字列は、コンパイラが生成するコードの初期化ルーチンで
処理される。
今迄はsdccが出力する初期化コードを呼ばずにmain()関数を直接実行するようにコンパイル・リンクしていた。
これは簡単なプログラムを作成する実行形式では問題なく動作するが、
配列の初期化は正しく動作しない。なぜならsdccで作成したコマンドがロードされると
そのままmain()関数の実行を始めるので初期化ルーチンを呼び出していないからだ。
初期化コードを含めて正しく動作するように変更する場合はスタートアップ用の
アセンブラを用意し、ここから初期化ルーチンを呼び、次にmain()関数を呼ぶという処理が必要
となる。
始めに以下のアセンブラを作成してランタイム初期化ルーチンとする。
これらはCOMバイナリがDOS(CPM)にロードされて最初に実行を開始する処理を記述し、sdccが作成する
初期化コード(_GSINIT初期化)に続きmain()を呼ぶだけの為に存在する。
crtmx.asm
;
;main関数グローバル定義とモジュール定義
;
.module crtmx
.globl _main
;
;
;以下初期化コード(プログラムがロードされたらこの部分が実行される)
;GSINITで初期化コードを実行し、_MAINでCのMain関数を呼ぶ。最後にCOMはRETで終了
.area _INITCODE
init:
call gsinit
call _main
ret
;
;
;以下に初期化ルーチンの領域のアドレスだけを指定。
;(中身はCコンパイラのコードジェネレータが出力する)
;
.area _GSINIT
gsinit::
.area _GSFINAL
ret
アセンブルする場合は以下のコマンドを実行する。
>sdasz80 -o crtmx.rel crtmx.asm
次にソースコードを用意する。ここでは配列aを整数で初期化する。
test1.c
#include <stdio.h>
unsigned char a[]={1,2,3,4,5};
void main(void){
int i;
for(i=0; i<5; i++){
printf("%d %d \n",i,a[i]);
}
}
コンパイルは以下のコマンドとなる。
>sdcc -c -mz80 test1.c
最後にリンカでこれらを結合してバイナリを作成するのだが、リンカでは各種コードセクション
や初期化コードをメモリ上の適切なアドレス配置に注意しなければならない。
>sdld -b _INITCODE=0x100 -b _CODE=0x120 -b _GSFINAL=0x1000 -b _DATA=0x1200 -i test1.ihx crtmx.rel test1.rel stdio.rel
>makebin -s 8192 test1.ihx > test1.bin
>cscript makecom.vbs test1.bin
_INITCODEはアセンブラで書かれたスタートアップルーチンを配置する開始アドレスを指定する。
スタートアップコードなのでDOS(CPM)のCOMバイナリがロードされる0x100に配置する。
続いてmain()関数が書かれた個所は_CODE領域指定で示される0x120からのアドレスに配置される。
sdccが出力した配列初期化コードは_GSFINALが示すアドレス領域にマップされるので、ここでは
作成するバイナリの最後のほうのアドレスに初期化ルーチンを配置した。
サンプルのCソースの配列aの初期化はCコンパイラが生成し、RAMに確保されるデータ領域に
初期化コードによって内容が設定される。
このため、データ領域もまた正しくアドレスを指定する必要がある。
ここではメモリの0x1200付近から始まる領域をデータ領域とし、配列変数の領域として確保
している。そのためにリンカで _DATA=0x1200 と指定する。
これらのコード、データ領域は衝突しないように(重ならないように)配置しなければならない。
(通常は作成するアプリケーションや動作環境ごとにテンプレートを作って流用する)
今回はテストコードが短いのでprintf()関数処理のための領域(stdlib.rel)を加えると4KB程度
以内に収めることができるが、ライブラリが大きくなるなどコードサイズが巨大化する場合は
初期化やデータ領域のアドレスを適切に設定し、領域が衝突しないように調整する必要がある。
最終更新:2016年07月18日 01:13