今回は、関数について書いていきたいと思います。
関数に触れる前に
関数が呼び出されると、制御は別のメモリアドレスに移動します。
CPUはそのメモリアドレスで処理を実行し、処理が終わると元のメモリアドレスに戻ってきます。関数のパラメータ、ローカル変数、フロー制御はすべてスタックと呼ばれる
メモリの重要な領域に保存されます。
まずスタックについて軽く説明していきます。
スタック
スタックとはスレッド作成時にOSによって作られるデータ構造です。
そして、LIFOを採用しています。(後入れ先出し、Last In First Outの略ね。
つまり、PUSH・POP命令でスタックのデータをやりとりするということです。
PUSH命令・POP命令
PUSH命令は、4バイト値をスタックにプッシュ(格納し、
POP命令は、4バイト値をスタックからポップ(取りだします。
push source ;sourceをスタックの最上部にプッシュします。
pop des ;スタックの最上部の値をdesにコピーします。
スタックは、上位アドレスから下位アドレス方向へデータを積み上げていきます。
つまり、スタックが作成されるとESPレジスタ(スタックポインタ)がスタックの上位アドレス(先頭)を指しています。
スタックにデータをプッシュするとESPレジスタの値が4バイトずつ減少し、
スタックからデータをポップするとESPレジスタの値が4バイトずつ増加します。
関数
アセンブリ言語のCALL命令を利用して、関数を呼び出すことができます。
call fanction ;関数の呼び出し
関数を大きなメモリアドレスだと考えるとわかりやすいです。
関数が終了されるとリターンアドレスがスタックから取り出され、メモリアドレス
から実行が継続されます。
関数の終了には、RET命令を使います。
この命令はスタックの最上部にあるリターンアドレスをポップします。
そして、制御はポップされたアドレスに移行します。
関数パラメータと戻り値
x86において、関数が受け取るパラメータはスタックにプッシュされ
戻り値がEAXレジスタに格納されます。
int test(int a,int b)
{
int x,y;
x=a;
y=b;
return 0;
}
int main(){
test(2,3)
return 0;
}
これをアセンブリで表すと、
test:
push ebp ;フレームポインタ(EBPをスタックに保存する
mov ebp, esp ;EBP=ESP
上の2行の処理を関数プロローグという。
sub esp, 8 ;ESPレジスタを減らしている
mov eax, [ebp+8] ;関数の実際の処理
mov [ebp-4], eax |
mov ecx, [ebp+0ch] |
mov [ebp-8], ecx ↓
xor eax, eax ;eaxの値を0にする。return 0;を示す
mov esp, ebp ;ESPはEBPに格納されているメモリアドレスを指す。
上の2行を関数エピローグという。
pop ebp ;スタックから古いEBPを復元する
ret ;スタックの先頭の戻りアドレスがポップ。制御が戻る
main:
push 3 ;3をスタックにプッシュ
push 2 ;2をスタックにプッシュ
call test ;test関数を呼び出す
add esp, 8 ;test関数が実行された後、制御がここに戻ってくる
xor eax, eax ;レジスタ値をクリアにする。return 0;を示す。
という感じです。スタックが登場して少しややこしくなりました。
関数プロローグとは、関数が利用できる環境のセットアップをしています。
そして、関数エピローグとはプロローグとは逆のことをして元の環境を復元します。
また、main関数はtest関数を呼び出しているので"呼び出し元関数"。
test関数は呼び出されているので"呼び出し先関数"といいます。
main関数の
add esp, 8
は、test関数で使用したスタックにプッシュされたパラメータを削除する役割があり、
関数を呼び出す前のスタックポインタに戻しています。
今回は、関数についてやってきましたが少し難しかったです。
ここは、自分でも少しわからないところがあるので、後で見返してみようと思いました。
みなさんも、おつかれさまでした。
次回は、配列と文字列をやっていきます。これでアセンブラ言語入門は一区切りつけたいと思います。