chibicc compiler を6800向けに改造する (2) 関数呼び出しの仕組み
去年の末から、 rui314 (Rui Ueyama) さんの rui314/chibicc: A small C compiler を MC6800向けに改造しています。
今回は関数呼び出しの仕組みのメモです。
関数呼び出し
chibicc は x64アーキテクチャー用に作成されていて、関数呼び出しの仕組みは複雑です。
x64で使えるレジスタは汎用レジスタが16本、XMMレジスタが16本、FPUレジスタが8本あります。
これらのレジスタはレジスタ引数として使うものもありますし、関数が呼ばれた際にはABIに従って退避したりしなかったりします。
MC6800だとレジスタ引数として使えそうなレジスタはA,B,IXの3つですが、IXを受け側で保存することを考えると頭が痛くなるので、レジスタ引数は1つ(AccA,B)だけとします。
関数呼び出しの際は、引数を右からpushしていって、一番左の引数をレジスタで渡します。
chibiccではここのところを簡素化していて、全部pushしたあとに、関数呼び出し直前にpopしています。
積んだ引数のバイト数を覚えておいて、関数から戻った後に呼んだ側が削除します。高速ですが、メモリ使用量は増えます。
Fuzix C compiler や CC6303 は、呼ばれた側が削除するのでメモリ効率は良いのですが、面倒なスタック操作が発生するので速度は落ちます。トレードオフですね。
呼ばれた側の関数の処理
Cではローカル変数が使えて再起呼び出しが可能なので、スタックフレームを作ってローカル変数やその他の情報を管理します。
再起呼び出しをしない言語であれば、ローカル変数は、その関数からのみ見えるグローバル変数にしてしまえば簡単です。
昔はそのような言語もありました。Pascalは再起呼び出しできるぜ!すげー!って時代。
スタックフレームは(現在は)以下の構造です。ローカル変数の先頭を指すポインタ(@bp)をゼロページに置いて、そこからのオフセットでローカル変数をアクセスします。
// @bp points old @fp,@bp,argument // // SP -> stack top // alloca/VLA area // @bp -> local var top // : // local var end // old @bp // argment passed by register (if any) // return address // argment passed by stack
オリジナルのchibiccでは、return addressの上に古いrbpレジスタがpushされていて、rbpはそこを指しています。レジスタ引数はローカル変数と一体の領域に保存され、区別はありません。
オリジナルの実装はMC6800には向かないので、順序を変更しています。
- SPを増減させるためにもAccA,Bは使われるので、最初にpushする必要がある
- IX修飾アドレッシングでは、正のオフセットしか使えないので、ローカル変数の先頭を指す必要がある
- 古い@bpを指す変数があると関数からの復帰が速くなるが、更新・復帰が面倒くさいので止めた。あとで実装するかもしれない
Fuzix CCやCC6303、acwjなどはBPを持たず、現在のSP位置からのオフセットを使って ローカル変数や引数をアクセスします。この方が速いし、メモリも喰わないです。
しかしSPの追跡が面倒くさい(しかも、失敗するとスタックを壊す)のと、alloca/VLAを実装してみたかったので、chibiccに合わせて SPと@bpを分離しています。
引数は何バイト積むか問題
chibiccでは、32bitよりサイズの小さな引数は、32bit単位でスタックに積まれます。型昇格・アラインメントを考えると、こうなります。
MC6800ではアラインメントは考えなくて良いので、変更しました。すると、呼ばれた側の関数がchar引数は1バイトだと考えたコードを出すようになりました。頭いいですね。
(x64やリトルエンディアンのCPUだと、1バイトでも何バイトでも、アドレスの示す先から実体があれば良いので、計算は一緒なのです)
しかし、ビッグエンディアンのCPUだと、実体の位置がずれるのでバグが発覚します。
どうやって治すか? 呼ぶ側を直すか、呼ばれた側を直すか。呼ばれた側を直すと、アドレス計算の前提が狂うので、呼ぶ側を修正するしかありません。
char引数だと1バイトpushするようにしました。バグりました。
chibiccでは、関数引数は全部pushしてから、関数を呼ぶ直前にレジスタ引数だけをpopして、関数を呼び出します。
しかし、popするときにはpushした情報を忘れているので、intでpopしてスタックが壊れます。
積んだ引数の情報を覚えておいて、popするときにその単位で戻す必要があります。
今回のMC6800の実装では使えるレジスタ引数は1個にしたので、最後にpushした引数の型を覚えておいて、それを戻して解決しています。
(グローバル変数使ったけど大丈夫かいな…、あ、ダメだわ。後で直そう → 直した)
MC6800でもAccA,Bを別々に考えれば、charのレジスタ引数2個か intを1個使えますが、どう考えても面倒くさいだけなので1個にしました。
以上、現時点でのメモです。
ディスカッション
コメント一覧
まだ、コメントがありません