命令
> 汎用命令 (3)
関数 (サブルーチン) を呼び出す場合は CALL 命令を使い、関数から元の呼び出し位置に戻る場合は、RET 命令を使います。
CALL 命令は、JMP 命令と同じように、相対位置または絶対位置を指定して、その位置に移動します。
JMP 命令と異なる点は、戻ってくる位置を記録するために、rIP レジスタの値を PUSH することです。
RET 命令は、現在のスタック位置から rIP の値を POP して、CALL 命令の次の位置に戻ります。
call func2 の場合、オペランドは符号付き 32bit のオフセット値となるので、次の命令位置から func2 までの相対オフセット値、1 がエンコードされています。
関数 (サブルーチン) を呼び出す場合は CALL 命令を使い、関数から元の呼び出し位置に戻る場合は、RET 命令を使います。
CALL 命令は、JMP 命令と同じように、相対位置または絶対位置を指定して、その位置に移動します。
JMP 命令と異なる点は、戻ってくる位置を記録するために、rIP レジスタの値を PUSH することです。
RET 命令は、現在のスタック位置から rIP の値を POP して、CALL 命令の次の位置に戻ります。
global testfunc section .text testfunc: call func2 ; E8 01 00 00 00 ret ; C3 func2: ret
call func2 の場合、オペランドは符号付き 32bit のオフセット値となるので、次の命令位置から func2 までの相対オフセット値、1 がエンコードされています。
スタックフレーム
「スタックフレーム」とは、関数に関連して使用される、スタックの領域です。
関数の呼び出し前にセットされる引数の領域、関数から戻るための rIP 値、関数の開始後に確保するローカル変数領域、そのほか、関数内で使用される PUSH/POP 命令による領域も含みます。
ローカル変数の領域はまとめて確保するので、関数内でその値を参照したりする場合は、[RBP - <N>] という形で、アドレス指定します。
領域内に、ローカル変数をどのように配置するかは自由ですが、型のアライメント (バイト境界) も考えて配置しておくと良いでしょう。
また、PUSH 命令では、基本的に 8 byte 単位で、スタックに値が格納されるため、RSP の位置が常に 8 byte 境界になるように、ローカル変数領域のサイズも 8 byte の倍数にしておいた方が良いです。
関数の呼び出し前にセットされる引数の領域、関数から戻るための rIP 値、関数の開始後に確保するローカル変数領域、そのほか、関数内で使用される PUSH/POP 命令による領域も含みます。
ローカル変数
グローバル変数として使うデータ領域は、データセクションに定義して使用しますが、関数内で使うローカル変数の場合は、スタックに確保します。
ローカル変数の領域は、関数の先頭で、rSP レジスタの値を減算することで、全体のサイズをまとめてスタック上に確保します。
また、ローカル変数や引数を参照するために、rBP レジスタをベース位置として使います。
rSP レジスタは、関数内で PUSH 命令を使ったりすると値が変化するので、この位置を基準にして、スタック内の位置を参照することはできません。
そのため、一つの関数内で rBP の位置を固定して、関数内のどこからでも、ローカル変数や引数を参照できるようにします。
ローカル変数の領域は、関数の先頭で、rSP レジスタの値を減算することで、全体のサイズをまとめてスタック上に確保します。
また、ローカル変数や引数を参照するために、rBP レジスタをベース位置として使います。
rSP レジスタは、関数内で PUSH 命令を使ったりすると値が変化するので、この位置を基準にして、スタック内の位置を参照することはできません。
そのため、一つの関数内で rBP の位置を固定して、関数内のどこからでも、ローカル変数や引数を参照できるようにします。
関数内の一連の動作
関数内でローカル変数を使う場合、動作は以下のようになります。
- PUSH RBP
PUSH 命令で、現在の rBP 値をスタックに格納する。
(呼び出し元でも rBP をベース位置として使用している場合は、関数から戻る時に、値を復元する必要があります) - MOV RBP, RSP
rBP に、rSP の値を代入する。
現在のスタックポインタの値を、rBP レジスタにセットして、ベース位置とします。
(位置的には、rBP は、ローカル変数領域の終端となります) - SUB RSP, <N>
スタック上にローカル変数領域を確保するため、rSP の値から、ローカル変数として使用する全サイズ分を、まとめて減算する。 - 関数内でローカル変数を参照する場合、rBP の位置をベースにして、負の相対位置でオフセットを指定する。
rBP はローカル変数領域の終端のため、そこからマイナス方向の位置を指定します。 - rSP はその後も自由に変更できるので、関数内で PUSH/POP 命令を使っても良い。
- MOV RSP, RBP
関数の終了時、RET 命令で戻る前に、rSP に rBP の値を代入する。
スタックポインタは、ローカル変数を確保する前の位置に戻るため、ローカル変数領域を削除する形になります。 - POP RBP
POP 命令で、呼びし元の rBP 値を復元させる。 - RET 命令を使い、CALL 命令によって PUSH された rIP 値を取り出して、CALL 命令の直後の位置に戻る。
コード例
call function ... function: push rbp ; 現在の RBP をスタックに保存 mov rbp, rsp ; RBP = RSP sub rsp, 8 ; ローカル変数領域確保 ... mov rsp, rbp ; RSP = RBP。関数開始後の位置に戻す pop rbp ; 呼び出し元の RBP を戻す ret
<スタック> ↑アドレス低 ↓アドレス高 --------- 関数内で PUSH/POP を使って使用される領域 --------- 現在位置 (RSP) (ローカル変数領域) int32 a : [RBP - 8] int32 b : [RBP - 4] --------- (RBP) RBP (呼び出し元の RBP) --------- 関数先頭 RIP (CALL から戻る位置) --------- CALL 前
ローカル変数の領域はまとめて確保するので、関数内でその値を参照したりする場合は、[RBP - <N>] という形で、アドレス指定します。
領域内に、ローカル変数をどのように配置するかは自由ですが、型のアライメント (バイト境界) も考えて配置しておくと良いでしょう。
また、PUSH 命令では、基本的に 8 byte 単位で、スタックに値が格納されるため、RSP の位置が常に 8 byte 境界になるように、ローカル変数領域のサイズも 8 byte の倍数にしておいた方が良いです。
ENTER と LEAVE
ローカル変数を使う際の、関数開始直後の処理と、関数から戻る時の処理を、まとめて行える命令があります。
ENTER
ENTER 命令は、ネストレベルが 0 の場合、以下と同じ処理になります。
ただし、ENTER 命令は、ネストレベルの指定によって、内部では少々複雑な処理になるので、上記のような単純な操作の場合、基本的に ENTER 命令は使用されません。
毎回上記の3行を書くのが面倒な場合は、複数行マクロを定義しておくと良いでしょう。
; enter N, 0 push rBP mov rBP, rSP sub rSP, N ; N = ローカル変数領域のサイズ
ただし、ENTER 命令は、ネストレベルの指定によって、内部では少々複雑な処理になるので、上記のような単純な操作の場合、基本的に ENTER 命令は使用されません。
毎回上記の3行を書くのが面倒な場合は、複数行マクロを定義しておくと良いでしょう。
LEAVE
LEAVE 命令は、以下と同じ処理になります。
単純にこの2つの命令が実行されるだけなので、ENTER 命令とは違って、LEAVE 命令は関数の最後でよく使用されます。
; leave mov rSP, rBP pop rBP
単純にこの2つの命令が実行されるだけなので、ENTER 命令とは違って、LEAVE 命令は関数の最後でよく使用されます。
引数
関数に引数を渡したい場合は、特定のレジスタかスタックを使います。
自身が書いたアセンブラ内で、自身が書いた関数を呼び出す場合は、引数をどのようにして渡しても問題はありません。
ただし、アセンブラで書いた関数を C 言語から呼び出したい場合や、C ライブラリ内の関数をアセンブラから呼び出したい場合は、特定の規則がないと、引数や戻り値をどのようにして扱うかがわかりません。
そのため、Unix 系では System-V ABI、Windows の場合は Microsoft ABI という規則があります。
各アーキテクチャによっても規則は異なるので、特定のアーキテクチャと各 OS に対応した規則に従って、関数のコードを書く必要があります。
詳細については後述しますが、ここでは、そのような規則には従わずに、スタックを使って引数を渡す場合の使用例を紹介します。
上記のコードでは、ローカル変数を使うことに意味はありませんが、ここでは使用例として使っています。
自身が書いたアセンブラ内で、自身が書いた関数を呼び出す場合は、引数をどのようにして渡しても問題はありません。
ただし、アセンブラで書いた関数を C 言語から呼び出したい場合や、C ライブラリ内の関数をアセンブラから呼び出したい場合は、特定の規則がないと、引数や戻り値をどのようにして扱うかがわかりません。
そのため、Unix 系では System-V ABI、Windows の場合は Microsoft ABI という規則があります。
各アーキテクチャによっても規則は異なるので、特定のアーキテクチャと各 OS に対応した規則に従って、関数のコードを書く必要があります。
詳細については後述しますが、ここでは、そのような規則には従わずに、スタックを使って引数を渡す場合の使用例を紹介します。
引数の処理
すべての引数を、スタックで渡したい場合は、以下のようにする必要があります。
RBP の位置を基準にして、プラス方向にオフセットを指定すると、スタック内の引数を参照できます。
ただし、RBP 位置の直下にある、RBP と RIP の値のサイズを加算する必要があります。
引数の値として、PUSH で即値を格納した場合、基本的にすべて 64bit サイズで格納されるので、注意してください。
引数のオフセット位置は、常に 8 byte 単位で指定することになりますが、MOV 命令で引数の値を読み込む場合は、好きなサイズで扱うことができます。
RET 命令で関数から戻る時に、オペランドで 16bit 即値の値を指定すると、rIP をポップした後、指定された値を rSP に加算することができます。
これにより、関数側で、不要な引数のスタック領域を削除することができます。
- CALL 命令を呼ぶ前に、スタックに各引数の値を PUSH する。
最後の引数から順にプッシュしていくと、関数内で参照する時に扱いやすくなります。 - 呼び出す関数内では、通常通りスタックフレームを形成する。
- 引数の値は、「rBP + (PUSH rBP を行った場合、そのサイズ) + (CALL 時にプッシュされた rIP のサイズ)」の位置を引数の先頭として、[RBP + N] でアドレス指定して、参照する。
- 関数から戻った後 (CALL 命令の直後)、不要になった引数のスタック領域を削除するため、rSP に、引数の全体サイズ分の値を足す。
もしくは、RET 命令のオペランドとして、引数サイズ分の値を、即値で指定する。
<スタック> --------- RSP ローカル変数領域 --------- RBP RBP (呼び出し元の rBP 値) --------- CALL 直後 RIP (戻る位置) --------- CALL 前 引数1 [RBP + 16] 引数2... --------- 引数をセットする前
RBP の位置を基準にして、プラス方向にオフセットを指定すると、スタック内の引数を参照できます。
ただし、RBP 位置の直下にある、RBP と RIP の値のサイズを加算する必要があります。
引数の値として、PUSH で即値を格納した場合、基本的にすべて 64bit サイズで格納されるので、注意してください。
引数のオフセット位置は、常に 8 byte 単位で指定することになりますが、MOV 命令で引数の値を読み込む場合は、好きなサイズで扱うことができます。
RET 命令で関数から戻る時に、オペランドで 16bit 即値の値を指定すると、rIP をポップした後、指定された値を rSP に加算することができます。
これにより、関数側で、不要な引数のスタック領域を削除することができます。
サンプルコード
global testfunc section .text testfunc: push 200 ; 64bit 符号拡張されてスタックに格納される push 100 ; 上と同じ call func_add add rsp, 16 ; 引数の領域を削除 (8byte x 2) ret func_add: push rbp mov rbp, rsp sub rsp, 8 ; 8byte 単位で扱った方がいいので、8byte サイズにする mov eax, [rbp+16] ; 第1引数 (32bit サイズで読み込み) add eax, [rbp+24] ; 第2引数 mov [rbp-8], eax ; ローカル変数に 32bit 値を格納 leave ret ; add rsp, 16 を消して、ret 16 にしてもよい
上記のコードでは、ローカル変数を使うことに意味はありませんが、ここでは使用例として使っています。