先週は、
コード生成で問題になるのは、手続き呼び出しである。これは局所変数 (local variable)の配置とも密接に結び付いている。手続きは、引数を 持つのが普通であり、これが呼び出し側(caller)と呼び出された側(callee) を結び付ける。
Cは手続きは値渡しと呼ばれ、単に値をコピーして送る だけで良い。Pascal などは参照渡しと呼ばれ、アドレスを送る必要がある。 参照渡しならば、引数をcallee側で変更することができるので、複数の 値を返したい時になどには便利だ。しかし、Cでもアドレスを直接送れば 同じことができる。この方が実装的には美しい。が、C++ では参照渡しを 復活させてしまったようだ... 参照渡しには複雑な問題が付きまとい、 理論的にもかなりうっとうしい。しかし、プログラムの記述は簡潔に なる。見掛けを取るか、使いやすさを取るか、そういう問題かも知れない。
局所変数や引数は、手続きが終ればなくなる変数である。これらはスタック 上にとられる。スタックは、通常、CPUに特別なサポートがあり、特別な registerを使うことになっている。これでは不便なことも多いので、それを Frame Pointer というのにコピーして使うことが多い。Frame Pointerは、 単なるindex registerを使うのが普通だが、どのregisterかは固定されている のが普通である。
6809の場合は、S がsystem stackで、Uがuser stackであり、UがFrame Pointer として使われている。例えば、
*
* int func_int();
* int *func_ptr();
*
* main() {
* int i,j,k;
* int *p;
*
* i = func_int(i,j,k);
main
PSHS U
LEAU ,S
LEAS -8,S
* generate code on type:
* list(INT)
* expr:
* list(ASS,
* list(LVAR,-2),
* list(FUNCTION,
* list(FNAME,func_int),
*))
LDD -6,U
PSHS D
LDD -4,U
PSHS D
LDD -2,U
PSHS D
LBSR func_int
LEAS 6,S
STD -2,U
* p = func_ptr(&i,*p);
* generate code on type:
* list(POINTER,
* list(INT))
* expr:
* list(ASS,
* list(LVAR,-8),
* list(FUNCTION,
* list(FNAME,func_ptr),
*))
LDD [-8,U]
PSHS D
LEAX -2,U
PSHS X
LBSR func_ptr
LEAS 4,S
STD -8,U
* }
LEAS ,U
PULS U,PC
_1 RTS
_INITIALIZE EQU _1
_GLOBALS EQU 0
END
というような感じになる。Intel 386では、
.text
.align 2,0x90
.globl _main
_main:
pushl %ebp
movl %esp,%ebp
subl $4,%esp
pushl %ebx
pushl %edx
pushl %eax
movl -4(%ebp),%ecx
pushl %ecx
call _func_int
movl %eax,-4(%ebp)
movl (%ebx),%ebx
pushl %ebx
leal -4(%ebp),%eax
pushl %eax
call _func_ptr
movl -8(%ebp),%ebx
movl %ebp,%esp
popl %ebp
ret
Intelでの関数呼び出しいの時のスタックの動きを、以下の図にならって 図示せよ。
Micro-C では、手続き名の型はFNAMEであり、式を表す中間木の型はFUNCTION である。実際のコード生成はfunction()で行われる。(問題2 では、手続きの 構文解析はどこで行われているか?)
引数はスタックにつまれ、callee側ではUに対する負のオフセットを 使ってアクセスすることになる。この引数はcallerに戻る時には なくなってしまっているので、扱うことはできない。
SPARCでは、変わった呼び出し方をしている。SPARCには overraped register window というのがあり、それぞれ、%i,%l,%o というように区別されている。 それぞれ8本ずつある。call すると、caller の %o が、callee の%i となる。 これを引数渡しに使う。(したがって8個以上の引数はregister渡しにできない。 実際にはframe pointerやstack pointerがあるので、もっと少ない) また、浮動小数点には、regisger windowはない。 calleeからcallerに戻る時には、再び%iが%oとなる。これによって値を 返すことができる。この切替えは、save/restore という命令によって おこなわれる。
この方法の良いところは、stackにいちいち値をcopyしなくても済むところ である。しかし、register windowは無限にあるわけではないので、いつかは いっぱいになってしまう。その時にはsub routineを呼び出して、register windowの中身をスタックにcopyする。copyした後、戻る時には、また、スタック からregister windowにcopyする。実際のアプリケーションでは、この register window overflowが頻繁に起きるので、 残念ながら、この方法が成功しているとは いえなかったようである。
実際には以下のような呼び出しを用いる。
mov 4,%g3
call _print,0
mov %g3,%o0 callより、こちらが先に実行される
L1:
ret
restore register window をもとにもどす
.align 8
.global _print
.proc 04
_print:
!#PROLOGUE# 0
save %sp,-104,%sp register window の切替え
!#PROLOGUE# 1
st %i0,[%fp+68] %i0にcallerの%o0が入っている
sethi %hi(LC0),%o1
or %o1,%lo(LC0),%o0
ld [%fp+68],%o1
ld [%fp+68],%o2
call _printf,0 この呼び出しはregister windowを使わない
nop
L2:
ret
restore ret より先にこちらが実行される
上の例にならって、PowerPC (AIX)上での関数の呼び出し方を示せ。
来週までに、 実行結果と、変更した主要な部分をメールにして、 Compiler Construction Lecture 1/22 のSubjectを付けて kono@ie.u-ryukyu.ac.jp までメールすること。