関数

命令
> 汎用命令 (3)

関数 (サブルーチン) を呼び出す場合は 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 命令による領域も含みます。
ローカル変数
グローバル変数として使うデータ領域は、データセクションに定義して使用しますが、関数内で使うローカル変数の場合は、スタックに確保します。

ローカル変数の領域は、関数の先頭で、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 N, 0
push rBP
mov rBP, rSP
sub rSP, N   ; N = ローカル変数領域のサイズ

ただし、ENTER 命令は、ネストレベルの指定によって、内部では少々複雑な処理になるので、上記のような単純な操作の場合、基本的に ENTER 命令は使用されません。

毎回上記の3行を書くのが面倒な場合は、複数行マクロを定義しておくと良いでしょう。
LEAVE
LEAVE 命令は、以下と同じ処理になります。

; leave
mov rSP, rBP
pop rBP

単純にこの2つの命令が実行されるだけなので、ENTER 命令とは違って、LEAVE 命令は関数の最後でよく使用されます。
引数
関数に引数を渡したい場合は、特定のレジスタかスタックを使います。

自身が書いたアセンブラ内で、自身が書いた関数を呼び出す場合は、引数をどのようにして渡しても問題はありません。

ただし、アセンブラで書いた関数を C 言語から呼び出したい場合や、C ライブラリ内の関数をアセンブラから呼び出したい場合は、特定の規則がないと、引数や戻り値をどのようにして扱うかがわかりません。

そのため、Unix 系では System-V ABI、Windows の場合は Microsoft ABI という規則があります。
各アーキテクチャによっても規則は異なるので、特定のアーキテクチャと各 OS に対応した規則に従って、関数のコードを書く必要があります。

詳細については後述しますが、ここでは、そのような規則には従わずに、スタックを使って引数を渡す場合の使用例を紹介します。
引数の処理
すべての引数を、スタックで渡したい場合は、以下のようにする必要があります。

  • 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 にしてもよい

上記のコードでは、ローカル変数を使うことに意味はありませんが、ここでは使用例として使っています。