Z80のC言語クロスコンパイル(SDCC)(2)

  • 投稿日:
  • 更新日:2015/03/08
  • by
  • カテゴリ:
前へ 第1回 第2回 第3回 第4回 第5回 次へ

sdccを使うことによりC言語でプログラムが可能になりますが、実際にはアセンブリ言語と一緒に使うことが多いと思います。理由は、

  • sdccの出力するコードは速度的にも容量的にも効率が悪い
  • 直接BIOSやI/Oポートにアクセスすることがある

などです。そのため、ここではsdccが出力する関数の構造についてみてみます。

関数の構造

簡単な関数の実装から、例を見てみましょう。

char func(char *arg) {
  char *str;
  unsigned char i;

  // 関数本体

  return i;
}  

という関数を例にとって見ましょう。sdccでは、この関数を次のような構造で出力します。

_func_start::
_func:
	; ixのバックアップ
	push	ix
	; ix = sp
	ld	ix, #0
	add	ix, sp
	; sp = sp - 3
	ld	hl, #-3
	add	hl, sp
	ld	sp, hl

	; 関数本体

	; sp = ix
	ld	sp, ix
	; ixを戻す
	pop	ix
	ret
_sdos_openFile_end::

まず、最初にixをバックアップします。このレジスタはスタックポインタをバックアップするために使います。そのため、ixにspを代入します。

次に、spを3減じています。これは、関数内で利用するローカル変数のためのヒープを確保するためです。この関数では、str(2バイト)、i(1バイト)をローカル変数として使っているので、その分の領域をスタックに確保しています。

この関数が呼び出されたときにSPが0xA200だとすると、この時点でのメモリは次のようになっています。

sdcc1.PNG

関数の最後では、spを保存されていたixに戻してretします。

関数呼び出し

次に、関数呼び出しです。先ほどのfunc内に、次のようなfunc2の呼び出しがあるとしましょう。

unsigned char func2(char *, char);

呼び出すためのCコードは次のようなものだとします。

i = func2(str, 2);

この部分の出力の様子を見てみます。

	; 引数2をスタックに入れる
	ld	a,#0x02
	push	af
	inc	sp
	; 引数strをスタックに入れる
	ld	l, -2(ix)
	ld	h, -1(ix)
	push	hl
	; 呼び出し
	call	_func2
	; 結果の退避(この部分は呼び出しによって異なる)
	ld	c, l
	; スタックを戻す
	pop	af
	inc	sp

まず、Aに2を代入してスタックにpushします。この引数は1バイトなので、inc spして不要なスタックを削除します。次に、HLにstrを代入してpushします。

callする直前のメモリマップは次のようになります。

sdcc2.PNG

これを見て判るように、関数呼び出しの際に与える引数は、後ろから積んでいきます。これにより、呼び出し先では最初の引数がより低いアドレスに格納されています。

呼 び出し結果ですが、この関数呼び出しは返り値が1バイトで返ってきます。その場合、結果はLレジスタに入っています。いったんそれをCレジスタに退避した あと、不要となった引数スタックを捨てています。pop afで引数str(2バイト)を捨て、次のinc spで引数2(1バイト)を捨てています。これでスタックの状態が元に戻ります。

アセンブリ言語による関数作成

アセンブリ言語でC言語から呼び出される関数を作成する場合、以下に注意します。

引数の格納位置

引数は前述のように最初の引数から順番に格納されています。実際にはcallされているので、スタックの先頭はリターンアドレスになっています。そのため、一般的なアクセス方法は次のようになります。

	ld	hl, #2
	add	hl, sp
	; (HL)に最初の引数が入っている
; 上記のfuncの呼び出しの例で言うとHL=0xa1fa

壊してはいけないレジスタ

上述のように、sdccの関数ではixレジスタをspの保存に使っています。そのため、呼び出し先でixを破壊してしまうと戻ってからの処理がおかしくなってしまいます。必要な場合は、作成する関数の最初でpush ixし、戻る直前でpop ixしましょう。

sdccは命令ごとにレジスタを初期化しなおすので、その他のレジスタは基本的に使っても問題ありません。これが、sdccのコンパイルコードの効率が悪い理由でもあります。

結果の格納

関数呼び出しの結果ですが、結果の受け渡しにはスタックポインタは使わず、レジスタを使います。結果の大きさに応じて次のようにレジスタが決まっています。

  • 1バイト(char)
    Lレジスタ
  • 2バイト(int, short, ポインタ(char*など))
    HLレジスタ
  • 4バイト(long)
    DEHLレジスタ、DEに上位16ビット、HLに下位16ビットを入れる

型に応じて上記レジスタに結果を格納してretします。

前へ 第1回 第2回 第3回 第4回 第5回 次へ

こちらもよく読まれています