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 コンパイラ以外はどうやっているのか。 調べてみたところ 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