関数呼び出しと %esp と %ebp

例えば、こんなコード

/* f1.c */
int add(int a, int b)
{
  return a+b;
}

int main(int argc, char *argv[])
{
  int c = add(1, 2);
  return 0;
}

を書いて、

$ gcc -S f1.c -o f1.s

とすると、f1.s は

	.text
.globl _add
_add:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$8, %esp
	movl	12(%ebp), %eax
	addl	8(%ebp), %eax
	leave
	ret
.globl _main
_main:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$40, %esp
	movl	$2, 4(%esp)
	movl	$1, (%esp)
	call	_add
	movl	%eax, -12(%ebp)
	movl	$0, %eax
	leave
	ret
	.subsections_via_symbols

となる。
どちらの関数も

	pushl	%ebp
	movl	%esp, %ebp
	なんとかかんとか
	leave
	ret

って形になってる。どうやらこれはある種のイディオムのようなもので、-fomit-frame-poiter とかしなければ必ずこうなる(と思う)。
やってることは

ベースポインタをスタックに積んで保存 -> 現時点でのスタックポインタの位置をベースポインタに設定 -> なんとかかんとか
  -> ベースポインタとスタックポインタを元に戻す -> 呼び出し元に帰る

ってなかんじみたい。
何故わざわざベースポインタを用いるのかというと、ローカル変数や引数といったスタックにある値にアクセスする際にスタックポインタだと不便だからだ。
push/pop するたびにスタックポインタは動いていって、同じローカル変数にアクセスするのに、ずらす量が異なってしまい、非常にややこしい。
そこで、push/pop しても同じようにスタックにある値にアクセスできるように「ベース」を設定するわけだ。
ちなみに、-fomit-frame-pointer をつけて gcc にかけると、ebp が使われず esp のみのコードを吐く。
こっちのほうが、単純に命令数が少ないから高速なのかな?


で、関数呼び出しのときにスタックに起こることを詳しく見ていく。
まず関数呼び出しの前に引数をスタックに積んでいく。

	movl	$2, 4(%eps)
	movl	$1, (%eps)

の部分がそれ。後ろの引数から順にスタックに積んでいく。

	pushl	$2
	pushl	$1

と書き直した方が人間にはわかりやすい。
でも、きっと gcc が吐いた

一気にスタックを確保 (subl $40, %esp) -> movl で目的の場所へ入れる

のほうが高速なんだろうな。でも、なんで40も確保するんだろ?
引数2つとローカル変数1つで、12確保すれば十分な気がするのに。
このへんは、また調べてみよう。


引数を積み終わったら、今度は関数がある場所へ移動する。

	call	_add

がそれ。call では、

関数呼び出しが終わったときに帰るべきアドレスをスタックに積む -> 目的の関数へジャンプ

ということが行われている。だから、関数が呼び出された時にスタックの一番上*1にあるのは、帰るべきアドレスです。


ようやく関数を呼び出せました。
まずやることは、さっき書いたように現在のベースポインタの保存と設定。これが終わった後に

	subl	$8, %esp

として、スタックを成長させてますが、何故これが必要なのか相変わらず不明。
こうやってスタックが成長しても、ベースポインタの %ebp の指す場所は変わらない。
だから、8(%ebp) で第一引数、12(%ebp) で第二引数にアクセスできるわけだ。
4(%ebp) にはもともとの %ebp の値、(%ebp) には帰るべきアドレスが入っている。

	movl	12(%ebp), %eax
	addl	8(%ebp), %eax

の部分で、実際に a+b を計算し、結果を %eax に入れている。
%eax は破壊してもよい*2レジスタで、関数の戻り値を置くレジスタでもある。
だから、これで戻り値を a+b の結果にちゃんとできたわけだ。

	leave

これは、ベースポインタとスタックポインタを元に戻している。したがって、

	movl	%ebp, %esp
	popl	%ebp

と同じことをしている。


これで帰る準備ができたため、実際に帰る処理に入る。それが

	ret

だ。帰るべき場所はスタックの一番上に置いてあるので、

	popl	%edx
	jmp	*%edx

と同じことをしている。%edx は自由に使っていいレジスタ


そうして、無事に呼び出し元の関数 (main) に帰ってこれた。
戻り値は %eax に入っているため、

	movl	%eax, -12(%ebp)

で、ローカル変数 c に戻り値をコピーしている。
ところで、-8(%ebp) や -4(%ebp) には何が入ってるんだろ?よくわからない。


こうやって見ると、C ですら本当に処理を簡潔に記述できるなぁと思ってしまう。
でも、アセンブリコードレベルで考えるってのもなかなか面白い。

*1:スタックは負の方向に成長するから一番下?

*2:つまり、書き換えてもよい