組み込み教育・アセンブラ 関数呼び出し編


当社では新人向けにコンピュータの基礎知識や、組み込み制御などの教育を行っています。
前回はスタック上に変数がどの様に確保されるかについて説明しました。

今回は関数呼び出し時にスタックがどの様に使われるかについて説明していきます。

C言語プログラムとバイナリコード

以下のプログラム①をコンパイルし、アセンブラコードに変換すると②になります。

① C言語プログラム

int func(int c, int d )
{
 int a = 100
 c = a + d;
 return c;
}

int main()
 int a = 10;
 int b = 20;
 int rtn;
 rtn = func(a,b);
:

② アセンブラコード

main:
    push ebp
    mov  ebp, esp
    sub  esp, 12

    mov  DWORD PTR [ebp-4], 10
    mov  DWORD PTR [ebp-8], 20

    push DWORD PTR [ebp-8]
    push DWORD PTR [ebp-4]
    call func
    add  esp, 8

    mov  DWORD PTR [ebp-12], eax

    mov  esp, ebp
    pop  ebp
    ret

func:
    push ebp
    mov  ebp, esp
    sub  esp, 4

    mov  DWORD PTR [ebp-4], 100

    mov  eax, DWORD PTR [ebp-4]
    add  eax, DWORD PTR [ebp+12]
    mov  DWORD PTR [ebp+8], eax

    mov  eax, DWORD PTR [ebp+8]

    mov  esp, ebp
    pop  ebp
    ret

命令の説明

  • call : 関数呼び出しを行う。その際、現在のPC(プログラムカウンタ)をスタックへpushにて保存する。
  • ret : 関数から呼び出し元へ戻る。call時にスタックに保存されたPCをpopすることで呼び出し前の箇所へ戻ることができる。

※ 前回の説明にあった命令については省略。

アセンブラコードの動作説明

この②のアセンブラコードがどの様に動作するかを以下に記載する。

main:
    push ebp
    mov  ebp, esp
    sub  esp, 12

mainからプログラムが開始される。
まず現在のベースポインタをスタックへ保存し、新しいスタックフレームを構築する。
続いてスタック上に a, b, rtn の3個の変数領域(合計12バイト)を確保する。

mov  DWORD PTR [ebp-4], 10
mov  DWORD PTR [ebp-8], 20

確保したローカル変数領域に初期値を設定する。
変数a([ebp-4]) 10、変数b([ebp-8]) に 20 を格納する。

push DWORD PTR [ebp-8]
push DWORD PTR [ebp-4]

変数a 及び変数b の値をスタックへ push する。
これらは関数呼び出し時の引数となる。
スタックは後入れ先出し構造であるため、結果として関数側では正しい順序で引数を参照できる。

call func

関数 func を呼び出す。
call 命令は、次に実行すべき命令のアドレス(戻りアドレス)をスタックへ push し、その後 func へ制御を移す。
この仕組みにより、関数終了後に元の位置へ確実に復帰できる。

以下、関数内の処理である。

func:
    push ebp
    mov  ebp, esp
    sub  esp, 4
    mov  DWORD PTR [ebp-4], 100

新たに関数用のスタックフレームを構築する。
main のスタックフレームとは独立した領域を使用する。
変数 a 用に4バイト確保し、100 を代入する。
この変数aと同名の変数がmainにもあるが、同名であっても異なるスタック領域に存在する別の変数である。

mov  eax, DWORD PTR [ebp-4]
add  eax, DWORD PTR [ebp+12]
mov  DWORD PTR [ebp+8], eax

c = a + d; の処理を実行する。
まずローカル変数 a([ebp-4])を eax に読み込む。
次に引数 d([ebp+12])を加算する。
計算結果を引数 c([ebp+8])へ書き戻す。
演算は汎用レジスタ eax を用いて実行される。

mov  eax, DWORD PTR [ebp+8]

引数 c の値を eax に格納する。
x86 では戻り値は eax に格納する規約であるため、この値が関数の戻り値となる。

mov  esp, ebp
pop  ebp

スタックポインタとベースポインタを関数呼び出し直後の状態へ復元する。
これにより、関数内で確保したローカル変数領域は破棄される。

ret

スタックに保存されている戻りアドレスを取り出し、そのアドレスへ制御を戻す。

再び main 側の処理である。

add  esp, 8

push した引数2個分(8バイト)を破棄し、スタックポインタを元の位置へ戻す。

関数内で引数 c の値を書き換えているが、それは使われずに破棄される。
C言語で引数に値を返すことができないのはこの為である。

mov  DWORD PTR [ebp-12], eax

eax に格納されている戻り値を変数 rtn に保存する。

mov  esp, ebp
pop  ebp
ret

main のスタックフレームを解放し、プログラムを終了する。

ここでは、初期のx86系CPUである Intel 8086 を例に関数呼び出しの動作を説明した。
8086 は汎用レジスタ数が少ないため、引数はスタック経由で受け渡す方式を採用している。

その後の Intel 80386 以降のCPUではレジスタ数が拡張され、さらに現代の64ビット環境ではレジスタ渡しが主流となっている。

以上が本アセンブラコードの動作である。


関連記事

TOP
CONTACT ACCESS LINE CALL