ENTER LEAVE Instructions - GawinGowin/libasm GitHub Wiki

ENTER/LEAVE命令詳細解説

概要

ENTER命令とLEAVE命令は、x86/x86-64アーキテクチャにおいてスタックフレームの管理を自動化するために設計された命令ペアです。これらの命令は関数のプロローグ(開始処理)とエピローグ(終了処理)を効率的に実行し、従来の手動によるスタック操作を簡素化します。

命令の基本仕様

ENTER命令

enter imm16, imm8

パラメータ:

  • imm16: ローカル変数用に確保するスタック領域のサイズ(バイト数、0-65535)
  • imm8: ネスティングレベル(0-31、通常は0を使用)

機能:

  • 関数プロローグの自動実行
  • ベースポインタの保存と設定
  • ローカル変数用スタック領域の確保
  • ネスティング関数のサポート(Pascal/Ada言語用)

LEAVE命令

leave

パラメータ: なし

機能:

  • 関数エピローグの自動実行
  • スタックフレームの完全復元
  • ベースポインタの復元

ENTER命令の詳細動作

基本動作(ネスティングレベル0)

最も一般的な使用法であるenter imm16, 0の場合:

enter 32, 0    ; 32バイトのローカル領域確保

この命令は以下の手動操作と完全に等価です:

push rbp       ; 1. 呼び出し元のベースポインタをスタックに保存
mov rbp, rsp   ; 2. 現在のスタックポインタを新しいベースポインタに設定
sub rsp, 32    ; 3. 32バイトのローカル変数用領域を確保

実行時のメモリ状態変化

ENTER実行前

高位アドレス
┌─────────────┐
│ 呼び出し元  │
│ のデータ    │
├─────────────┤ ← 元のrbp (0x7fff12345698)
│ 戻りアドレス│ ← call命令で保存
├─────────────┤ ← 現在のrsp (0x7fff12345678)
│             │
└─────────────┘
低位アドレス

ENTER 32, 0 実行後

高位アドレス
┌─────────────┐
│ 呼び出し元  │
│ のデータ    │
├─────────────┤ ← 元のrbp (0x7fff12345698)
│ 戻りアドレス│
├─────────────┤ ← 保存されたrbp (0x7fff12345670)
│0x7fff12345698│   push rbpで保存された値
├─────────────┤ ← 新しいrbp (0x7fff12345670)
│             │
│  32バイト   │   ローカル変数用領域
│  ローカル   │   [rbp-8], [rbp-16], [rbp-24], [rbp-32]
│  領域       │   でアクセス可能
│             │
├─────────────┤ ← 新しいrsp (0x7fff12345650)
│ 未使用領域  │
└─────────────┘
低位アドレス

ローカル変数へのアクセス

ENTERで確保した領域は、ベースポインタからの負のオフセットでアクセスします:

enter 32, 0

; ローカル変数への値の保存
mov qword [rbp-8], rax    ; 1つ目のローカル変数
mov qword [rbp-16], rbx   ; 2つ目のローカル変数
mov qword [rbp-24], rcx   ; 3つ目のローカル変数
mov dword [rbp-28], edx   ; 4バイト変数

; ローカル変数からの値の読み込み
mov rax, qword [rbp-8]    ; 1つ目の変数を読み込み
add rax, qword [rbp-16]   ; 2つ目の変数と加算

LEAVE命令の詳細動作

内部実行処理

leave

この1つの命令は、以下の2つの命令と完全に等価です:

mov rsp, rbp   ; 1. スタックポインタをベースポインタの値に設定
pop rbp        ; 2. スタックから元のベースポインタを復元

実行時のメモリ状態変化

LEAVE実行前

高位アドレス
┌─────────────┐
│ 呼び出し元  │
├─────────────┤
│ 戻りアドレス│
├─────────────┤ ← 保存されたrbp値
│0x7fff12345698│
├─────────────┤ ← 現在のrbp (0x7fff12345670)
│ 使用済み    │
│ ローカル    │ ← この領域が解放される
│ 変数領域    │
├─────────────┤ ← 現在のrsp (0x7fff12345650)
│             │
└─────────────┘
低位アドレス

LEAVE実行後

高位アドレス
┌─────────────┐
│ 呼び出し元  │
├─────────────┤ ← 復元されたrbp (0x7fff12345698)
│ 戻りアドレス│ ← rsp (0x7fff12345678)
├─────────────┤   ret命令用の戻りアドレス
│             │   ローカル領域は自動的に解放
└─────────────┘
低位アドレス

手動プロローグ・エピローグとの比較

従来の手動方式

my_function:
    ; === 手動プロローグ ===
    push rbp        ; ベースポインタ保存
    mov rbp, rsp    ; 新しいフレーム設定
    sub rsp, 32     ; ローカル領域確保
    
    ; === 関数本体 ===
    mov qword [rbp-8], rdi
    mov qword [rbp-16], rsi
    ; 処理...
    
    ; === 手動エピローグ ===
    add rsp, 32     ; ローカル領域解放(または mov rsp, rbp)
    pop rbp         ; ベースポインタ復元
    ret             ; 関数終了

ENTER/LEAVE使用版

my_function:
    ; === ENTERプロローグ ===
    enter 32, 0     ; 32バイト確保、自動フレーム設定
    
    ; === 関数本体 ===
    mov qword [rbp-8], rdi
    mov qword [rbp-16], rsi
    ; 処理...
    
    ; === LEAVEエピローグ ===
    leave           ; 自動フレーム復元
    ret             ; 関数終了

比較分析

項目 手動方式 ENTER/LEAVE
命令数 3+2=5命令 1+1=2命令
コードサイズ 8-12バイト 5バイト
可読性 明示的 簡潔
エラー率 高い(順序ミス等) 低い
デバッグ 明確 抽象化
パフォーマンス 高速 同等〜やや遅い

実用的なコード例

基本的な関数フレーム

; 2つの整数を加算する関数
add_numbers:
    enter 16, 0          ; 16バイトのローカル領域確保
    
    ; 引数をローカル変数に保存
    mov qword [rbp-8], rdi   ; 第1引数
    mov qword [rbp-16], rsi  ; 第2引数
    
    ; 加算計算
    mov rax, qword [rbp-8]
    add rax, qword [rbp-16]
    
    ; 結果はraxに格納済み
    leave
    ret

より複雑な例(配列処理)

; 配列の要素を合計する関数
sum_array:
    enter 48, 0          ; 48バイトのローカル領域確保
    
    ; ローカル変数の配置計画:
    ; [rbp-8]:  配列ポインタ
    ; [rbp-16]: 配列サイズ
    ; [rbp-24]: ループカウンタ
    ; [rbp-32]: 現在の合計値
    ; [rbp-40]: 一時計算用
    
    ; 引数の保存
    mov qword [rbp-8], rdi    ; array pointer
    mov qword [rbp-16], rsi   ; array size
    
    ; 初期化
    mov qword [rbp-24], 0     ; counter = 0
    mov qword [rbp-32], 0     ; sum = 0
    
.loop:
    ; ループ条件チェック
    mov rax, qword [rbp-24]   ; counter
    cmp rax, qword [rbp-16]   ; compare with size
    jge .end_loop
    
    ; 配列要素の読み込み
    mov rbx, qword [rbp-8]    ; array pointer
    mov rcx, qword [rbp-24]   ; counter
    mov rax, qword [rbx + rcx*8]  ; array[counter]
    
    ; 合計に加算
    add qword [rbp-32], rax
    
    ; カウンタ増加
    inc qword [rbp-24]
    jmp .loop
    
.end_loop:
    ; 結果を戻り値に設定
    mov rax, qword [rbp-32]
    
    leave
    ret

構造体を扱う例

; Point構造体(x, y座標)の距離計算
calculate_distance:
    enter 64, 0          ; 64バイトのローカル領域
    
    ; 構造体データの配置:
    ; [rbp-16]: Point1 (x1: 8バイト, y1: 8バイト)
    ; [rbp-32]: Point2 (x2: 8バイト, y2: 8バイト)
    ; [rbp-40]: dx (x2-x1)
    ; [rbp-48]: dy (y2-y1)
    ; [rbp-56]: dx²
    ; [rbp-64]: dy²
    
    ; Point1のコピー
    mov rax, qword [rdi]      ; x1
    mov qword [rbp-16], rax
    mov rax, qword [rdi+8]    ; y1
    mov qword [rbp-8], rax
    
    ; Point2のコピー
    mov rax, qword [rsi]      ; x2
    mov qword [rbp-32], rax
    mov rax, qword [rsi+8]    ; y2
    mov qword [rbp-24], rax
    
    ; dx = x2 - x1
    mov rax, qword [rbp-32]   ; x2
    sub rax, qword [rbp-16]   ; x1
    mov qword [rbp-40], rax   ; dx
    
    ; dy = y2 - y1
    mov rax, qword [rbp-24]   ; y2
    sub rax, qword [rbp-8]    ; y1
    mov qword [rbp-48], rax   ; dy
    
    ; dx² (簡化: 平方根計算は省略)
    mov rax, qword [rbp-40]
    imul rax, rax
    mov qword [rbp-56], rax
    
    ; dy²
    mov rax, qword [rbp-48]
    imul rax, rax
    mov qword [rbp-64], rax
    
    ; distance² = dx² + dy²
    mov rax, qword [rbp-56]
    add rax, qword [rbp-64]
    
    leave
    ret

ネスティングレベル(高度な機能)

ネスティングレベルとは

ネスティングレベル(第2パラメータ)は、Pascal、Ada、Algolなどの言語で使用される「ネスティング関数」(関数内関数)をサポートするための機能です。

ネスティングレベル0(標準的な使用)

enter 32, 0    ; レベル0: 通常のC/C++スタイル

これは最も一般的な使用法で、C/C++のような言語に適しています。

ネスティングレベル1以上(Pascal/Ada スタイル)

enter 16, 1    ; レベル1: 1段階のネスティング
enter 24, 2    ; レベル2: 2段階のネスティング

ネスティングレベル1の動作例

enter 16, 1

この命令は以下と等価な複雑な処理を実行:

push rbp                    ; 現在のベースポインタ保存
mov rax, rbp                ; 現在のrbpをraxに
push rax                    ; 親関数のフレームポインタ保存
mov rbp, rsp                ; 新しいベースポインタ設定
sub rsp, 16                 ; ローカル領域確保

Pascal風のネスティング関数例

// Pascal例(概念的)
procedure OuterFunction;
var 
    outer_var: integer;
    
    procedure InnerFunction;  // ネスティング関数
    var 
        inner_var: integer;
    begin
        inner_var := outer_var;  // 外側の変数にアクセス可能
    end;
    
begin
    outer_var := 42;
    InnerFunction;
end;

対応するアセンブリ(概念的):

OuterFunction:
    enter 8, 0           ; レベル0: 通常の関数
    mov qword [rbp-8], 42    ; outer_var = 42
    call InnerFunction
    leave
    ret

InnerFunction:
    enter 8, 1           ; レベル1: ネスティング関数
    ; [rbp+8]: 親関数のフレームポインタ
    ; 親関数の変数にアクセス可能
    mov rax, qword [rbp+8]   ; 親フレームポインタ取得
    mov rbx, qword [rax-8]   ; outer_varにアクセス
    mov qword [rbp-8], rbx   ; inner_var = outer_var
    leave
    ret

現代での使用状況

  • C/C++: ネスティングレベル0のみ使用
  • Pascal/Ada: 高レベルネスティング使用
  • 現代のコンパイラ: 効率性の理由でENTER命令を避ける傾向

パフォーマンス分析

実行速度の比較

クロック数(プロセッサ依存)

処理 手動方式 ENTER LEAVE
古いプロセッサ 3-4クロック 11-15クロック 4-5クロック
現代のプロセッサ 3-4クロック 4-6クロック 2-3クロック
マイクロコード最適化 ハードウェア実行 マイクロコード実行 ハードウェア実行

ベンチマーク例

; 1,000,000回の関数呼び出しテスト

; 手動方式
test_manual:
    push rbp
    mov rbp, rsp
    sub rsp, 32
    ; 処理...
    add rsp, 32
    pop rbp
    ret

; ENTER/LEAVE方式
test_enter_leave:
    enter 32, 0
    ; 処理...
    leave
    ret

測定結果(概算):

  • 手動方式: 100ms
  • ENTER/LEAVE: 102-110ms(2-10%遅い)

コードサイズの比較

; 手動方式のバイト数
push rbp      ; 1バイト
mov rbp, rsp  ; 3バイト
sub rsp, 32   ; 4バイト (32ビット即値)
add rsp, 32   ; 4バイト
pop rbp       ; 1バイト
; 合計: 13バイト

; ENTER/LEAVE方式のバイト数
enter 32, 0   ; 4バイト
leave         ; 1バイト
; 合計: 5バイト

; サイズ削減: 8バイト (62%削減)

最適化との関係

GCCの選択ポリシー

# フレームポインタ有効
gcc -fno-omit-frame-pointer -O0 source.c
# → ENTERまたは手動プロローグ使用

# フレームポインタ省略(デフォルト最適化)
gcc -fomit-frame-pointer -O2 source.c
# → プロローグ・エピローグ最小化または完全省略

# デバッグ情報付き
gcc -g -fno-omit-frame-pointer source.c
# → 確実にフレームポインタ使用

最適化レベル別の動作

; -O0 (最適化なし): フレームポインタ使用
function:
    push rbp
    mov rbp, rsp
    sub rsp, 16
    ; ...
    leave
    ret

; -O2 (最適化あり): フレームポインタ省略
function:
    sub rsp, 16      ; 直接スタック操作
    ; ...
    add rsp, 16
    ret

デバッガサポートとの関係

スタックトレースの表示

適切なフレームポインタ使用時

(gdb) bt
#0  inner_function () at test.c:15
#1  0x0000000000401234 in middle_function () at test.c:10
#2  0x0000000000401250 in outer_function () at test.c:5
#3  0x0000000000401270 in main () at test.c:2

(gdb) frame 1
#1  0x0000000000401234 in middle_function () at test.c:10
10      result = inner_function();

(gdb) info frame
Stack frame at 0x7fff12345698:
 rip = 0x401234 in middle_function (test.c:10)
 saved rip = 0x401250
 called by frame at 0x7fff123456b8
 calls frame at 0x7fff12345678
 Arglist at 0x7fff12345698, args: 
 Locals at 0x7fff12345698

フレームポインタ省略時

(gdb) bt
#0  inner_function () at test.c:15
#1  0x0000000000401234 in ?? ()
#2  0x0000000000401250 in ?? ()
# スタックトレースが不正確

デバッグ情報とDWARF

# DWARF情報付きコンパイル
gcc -g -gdwarf-4 source.c

# objdumpでDWARF情報確認
objdump -W a.out | grep -A 10 "DW_TAG_subprogram"

DWARF情報により、フレームポインタなしでも正確なデバッグが可能になりますが、ENTER/LEAVEによる標準的なフレーム構造は依然として有効です。

現代のコンパイラでの扱い

GCCの戦略

関数の複雑さによる選択

// 単純な関数: フレームポインタ省略
int simple_add(int a, int b) {
    return a + b;
}
// 生成されるアセンブリ:
// add edi, esi
// mov eax, edi
// ret

// 複雑な関数: フレームポインタ使用
int complex_function(int *array, int size) {
    int local_array[100];
    int sum = 0;
    for (int i = 0; i < size; i++) {
        local_array[i] = array[i] * 2;
        sum += local_array[i];
    }
    return sum;
}
// 生成されるアセンブリ:
// push rbp
// mov rbp, rsp
// sub rsp, 408  (100*4 + アライメント)
// ...
// leave
// ret

LLVM/Clangの戦略

# Clangの最適化選択
clang -O0 -fno-omit-frame-pointer source.c  # フレームポインタ強制
clang -O2 source.c                          # 最適化優先
clang -Oz source.c                          # サイズ最適化

MSVCの戦略

# Visual Studio
cl /Od source.c     # デバッグビルド: フレームポインタ使用
cl /O2 source.c     # リリースビルド: 最適化優先

使用指針とベストプラクティス

ENTER/LEAVEを使うべき場合

  1. 教育目的

    ; アセンブリ学習時の理解促進
    student_function:
        enter 16, 0    ; 明確なスタックフレーム
        ; 学習内容...
        leave
        ret
    
  2. プロトタイピング

    ; 迅速な関数作成
    prototype_function:
        enter 64, 0    ; 十分な領域確保
        ; 実験的なコード...
        leave
        ret
    
  3. デバッグ重視

    ; デバッグしやすいコード
    debug_function:
        enter 32, 0    ; 標準的なフレーム
        ; デバッグ対象のコード...
        leave
        ret
    

手動を使うべき場合

  1. パフォーマンス重視

    ; 高速実行が必要
    performance_critical:
        push rbp
        mov rbp, rsp
        ; 最適化されたコード...
        pop rbp
        ret
    
  2. 特殊なスタック操作

    ; 可変長スタック確保
    variable_stack:
        push rbp
        mov rbp, rsp
        sub rsp, rdi    ; 引数に応じて動的確保
        ; 処理...
        mov rsp, rbp
        pop rbp
        ret
    
  3. 組み込みシステム

    ; メモリ制約が厳しい環境
    embedded_function:
        ; フレームポインタ省略
        sub rsp, 8
        ; 最小限の処理...
        add rsp, 8
        ret
    

コーディング規約の例

; === 推奨パターン ===

; 一般的な関数
general_function:
    enter 32, 0
    ; 処理...
    leave
    ret

; パフォーマンス重視関数
fast_function:
    push rbp
    mov rbp, rsp
    ; 処理...
    pop rbp
    ret

; 単純な関数(フレーム省略)
simple_function:
    ; 直接処理
    mov rax, rdi
    add rax, rsi
    ret

互換性とポータビリティ

アーキテクチャ間の違い

x86-32での動作

; 32ビットモード
enter 16, 0
; 等価:
; push ebp
; mov ebp, esp
; sub esp, 16

leave
; 等価:
; mov esp, ebp
; pop ebp

x86-64での動作

; 64ビットモード
enter 32, 0
; 等価:
; push rbp
; mov rbp, rsp
; sub rsp, 32

leave
; 等価:
; mov rsp, rbp
; pop rbp

他のアーキテクチャとの比較

アーキテクチャ スタックフレーム自動化 相当命令
ARM なし 手動プロローグ/エピローグ
RISC-V なし 手動プロローグ/エピローグ
MIPS なし 手動プロローグ/エピローグ
PowerPC なし 手動プロローグ/エピローグ

x86アーキテクチャのENTER/LEAVEは比較的特殊な機能です。

まとめ

ENTER/LEAVE命令の特徴

利点:

  • コードの簡潔性
  • エラー防止(手動操作のミスを回避)
  • 標準的なフレーム構造
  • デバッガサポート

欠点:

  • わずかなパフォーマンスオーバーヘッド
  • 柔軟性の制限
  • 現代の最適化との相性

推奨される使用場面

  1. 学習・教育: スタックフレームの理解促進
  2. プロトタイピング: 迅速な開発
  3. デバッグ重視: 問題の特定を優先
  4. 標準的な関数: 特殊な最適化が不要

現代のプログラミングでの位置づけ

ENTER/LEAVE命令は、アセンブリプログラミングの基礎を理解し、適切なスタックフレーム管理を学ぶための優れたツールです。パフォーマンスが最重要でない場合、これらの命令の使用により、安全で保守しやすいアセンブリコードを書くことができます。

高性能が要求される場面では手動による最適化が必要ですが、ENTER/LEAVEで得られる知識は、手動最適化の基礎としても非常に価値があります。