TL/1 の呼出規約を予想する

cdecl か PASCAL

プログラミング言語 TL/1 について、呼出規約は cdecl か PASCAL を使っているものだろうと先日は予想した。 では cdecl と PASCAL のどちらなのだろうか。 それを検証するためにこのようなプログラムを実際に (TL/1 の実装のひとつである TL/1-FM で) コンパイルして動作させてもらった。

FUNC FOO

BEGIN
  WRITE(0: FOO(1, 2))
END

FOO(X)
BEGIN
  RETURN X
END

呼出規約が素朴な cdecl か PASCAL のどちらかであるという前提の元で、このプログラムが 1 を表示するようなら cdecl で 2 なら PASCAL である。 その理由についてはここでは説明しないが、呼出規約の詳細はウェブ上に情報があるのでそれを参照してもらいたい。

結果は 1 であったとのことで cdecl だろうと予想された。

反例

ところが追加の検証で以下のようなプログラムの実行例をもらった。

PROC ZERO,ONE,TWO

BEGIN
  ZERO(12,23)
  ONE (34,45)
  TWO (56,67)
END

ZERO
VAR X,Y
BEGIN
  WRITE(0:X," ",Y,CRLF)
END

ONE(X)
VAR Y
BEGIN
  WRITE(0:X," ",Y,CRLF)
END

TWO(X,Y)
BEGIN
  WRITE(0:X," ",Y,CRLF)
END

実行結果は以下のようになるのだという。

12 23
34 45
56 67

この情報をくれたはりせん氏はローカル変数と引数渡しの領域がかぶっているのだろうと述べているが、 cdecl ならそんなことはありえない。 TL/1 (の処理系のひとつである TL/1-FM) が cdecl を呼出規約として使っているという予想は間違いだったということだ。

また、このような結果を生みだすようなスタックの利用方法を思い付かないでいる。 実際のコンパイル結果を見れれば手っ取り早いのだが…。

C コンパイラはこうしている

では、 TL/1-FM コンパイラ以外はどうやっているのか。 調べてみたところ gccMC6809 (FM-7 などが採用しているプロセッサ) 版があり、それを用いて上述の TL/1 コードと同等のコード、すなわち以下のようなコードをコンパイルしてみた。

#include <stdio.h>

void zero();
void one();
void two();

int main(void) {
  zero(12, 23);
  one(34, 45);
  two(56, 67);
}

void zero(void) {
  int x, y;
  printf("%d %d\n", x, y);
}

void one(int x) {
  int y;
  printf("%d %d\n", x, y);
}

void two(int x, int y) {
  printf("%d %d\n", x, y);
}

コンパイルされたコードは以下のようなものであった。

;;; gcc for m6809 : Mar 28 2010 21:13:35 [no tag]
;;; 4.3.4 (gcc6809)
;;; ABI version 1
;;; -mint16
	.module	test.c
	.area .text
	.globl _main
_main:
	pshs	u
	leas	-2,s
	leau	,s
	ldx	#23
	pshs	x	;movhi_push: R:x
	ldx	#12
	jsr	_zero	;CALL: (VOIDmode) (2 bytes)
	leas	2,s
	ldx	#45
	pshs	x	;movhi_push: R:x
	ldx	#34
	jsr	_one	;CALL: (VOIDmode) (2 bytes)
	leas	2,s
	ldx	#67
	pshs	x	;movhi_push: R:x
	ldx	#56
	jsr	_two	;CALL: (VOIDmode) (2 bytes)
	leas	2,s
	leas	2,s
	puls	u,pc
LC0:
	.ascii "%d %d\n\0"
	.globl _zero
_zero:
	pshs	u
	leas	-4,s
	leau	,s
	ldx	2,u
	pshs	x	;movhi_push: R:x
	ldx	,u
	pshs	x	;movhi_push: R:x
	ldx	#LC0
	pshs	x	;movhi_push: R:x
	jsr	_printf
	leas	6,s
	leas	4,s
	puls	u,pc
	.globl _one
_one:
	pshs	u
	leas	-4,s
	leau	,s
	stx	,u
	ldx	2,u
	pshs	x	;movhi_push: R:x
	ldx	,u
	pshs	x	;movhi_push: R:x
	ldx	#LC0
	pshs	x	;movhi_push: R:x
	jsr	_printf
	leas	6,s
	leas	4,s
	puls	u,pc
	.globl _two
_two:
	pshs	u
	leas	-2,s
	leau	,s
	stx	,u
	ldx	6,u
	pshs	x	;movhi_push: R:x
	ldx	,u
	pshs	x	;movhi_push: R:x
	ldx	#LC0
	pshs	x	;movhi_push: R:x
	jsr	_printf
	leas	6,s
	leas	2,s
	puls	u,pc

基本的には cdecl だが、最初の引数だけは x レジスタ経由で渡していることがわかる。 いわゆる fastcall の一種といえるだろう。 しかしこれでは TL/1-FM の挙動と同じではなく、参考にならない。

今回、この出力結果を読み解くにあたって MC6809 の命令セットなどを調べたのだが、興味深いことに MC6809 はふたつのスタックポインタを持っている。 S レジスタが基本的なスタックポインタとして用いられるが、 U レジスタも S レジスタと同じ能力を持っている。 サブルーチンの呼出の際にリターンアドレスを積むときは S レジスタの方が使われるということだけしか差がないようだ。 gccMC6809 版では U レジスタX86 でいうところの BP レジスタと同じようにしか使っていないが、もっと変則的な使い方もできるのかもしれず、それを活用した効率的な呼出規約があるのかもしれない。

Document ID: fdfa7c554d56db3aa97fba03d48a38fe