プログラミング言語 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 を呼出規約として使っているという予想は間違いだったということだ。
また、このような結果を生みだすようなスタックの利用方法を思い付かないでいる。 実際のコンパイル結果を見れれば手っ取り早いのだが…。
では、 TL/1-FM コンパイラ以外はどうやっているのか。 調べてみたところ gcc の MC6809 (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 レジスタの方が使われるということだけしか差がないようだ。 gcc の MC6809 版では U レジスタを X86 でいうところの BP レジスタと同じようにしか使っていないが、もっと変則的な使い方もできるのかもしれず、それを活用した効率的な呼出規約があるのかもしれない。
Document ID: fdfa7c554d56db3aa97fba03d48a38fe