先週は、
コード生成で問題になるのは、手続き呼び出しである。これは局所変数 (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 までメールすること。