3.13 过程与栈帧

过程(函数)调用是程序组织的基本单元。每次函数调用时,系统在栈上分配一个栈帧(stack frame)来存储该次调用的上下文信息。

栈帧概述

栈帧是函数调用时在栈上分配的一片连续内存区域,用于存储:

  • 返回地址:从被调函数返回后继续执行的位置
  • 保存的寄存器:被调用者需要恢复的寄存器旧值
  • 局部变量:函数内部定义的自动变量
  • 参数传递:第 7 个及以后的参数

运行时栈结构

高地址
       ┌──────────────────────────┐
       │   调用者栈帧              │
       │   ┌──────────────────┐   │
       │   │ 参数 7+ (if any)  │   │ ← 调用者分配
       │   │ 返回地址          │   │ ← call 指令压入
       ├───┴──────────────────┴───┤
       │   被调用者栈帧            │
       │   ┌──────────────────┐   │
       │   │ 保存的 %rbp       │   │ ← pushq %rbp
       │   │ 局部变量区域      │   │ ← subq $N, %rsp
       │   │ 保存的寄存器     │   │ ← pushq %rbx, ...
       │   │                   │   │
       └───┴──────────────────┴───┤ ← %rsp
低地址

栈帧的建立与销毁

典型栈帧建立序列

example_func:
    # 栈帧建立(prologue)
    pushq   %rbp                # 保存调用者的 %rbp
    movq    %rsp, %rbp          # 设置当前函数自己的 %rbp
    subq    $32, %rsp           # 分配 32 字节的局部变量空间
 
    # 函数体
    # ... 使用 -8(%rbp), -16(%rbp) 等访问局部变量 ...
 
    # 栈帧销毁(epilogue)
    movq    %rbp, %rsp          # 恢复 %rsp(回收局部变量空间)
    popq    %rbp                # 恢复调用者的 %rbp
    ret                         # 返回(弹出返回地址)

省略帧指针

现代编译器在优化级别 (-O2) 会省略 %rbp 作为帧指针,直接使用 %rsp 访问局部变量。这样可以节省一个寄存器:

# -O2 优化后的栈帧(无 %rbp)
example_func_opt:
    subq    $40, %rsp           # 分配 40 字节(包含可能的参数区)
 
    # 使用 %rsp + 偏移量访问局部变量
    movq    $100, 8(%rsp)       # 局部变量在 rsp+8
    movq    $200, 16(%rsp)      # 局部变量在 rsp+16
 
    addq    $40, %rsp           # 回收栈空间
    ret

栈帧内容详解

1. 返回地址

call 指令将返回地址压入栈,ret 指令弹出:

    call    func               # pushq 返回地址; %rsp -= 8
 
func:
    # %rsp 指向返回地址
    ...
    ret                        # popq 返回地址; %rsp += 8

详见 。

2. 保存的寄存器

被调用者如果使用 ,必须保存原始值:

func:
    pushq   %rbx               # 保存 %rbx(被调用者保存)
    pushq   %r12               # 保存 %r12
 
    # 函数体中使用 %rbx 和 %r12 ...
    movq    (%rdi), %rbx
    movq    %rbx, %r12
 
    popq    %r12                # 恢复 %r12
    popq    %rbx                # 恢复 %rbx
    ret

3. 局部变量

以下情况局部变量会被分配到栈上而非寄存器:

  • 变量数量超过寄存器数量
  • 变量需要取地址(& 操作符)
  • 变量是数组或结构体
void func() {
    int local_var = 42;        // 可能在栈上或寄存器中
    int arr[4] = {1, 2, 3, 4}; // 数组必须在栈上(支持指针运算)
    int *p = &local_var;       // 取地址后,local_var 必须在栈上
}
func:
    subq    $32, %rsp           # 分配 32 字节
    movl    $42, 28(%rsp)      # local_var = 42 (在 rsp+28)
    movl    $1, (%rsp)         # arr[0] = 1
    movl    $2, 4(%rsp)        # arr[1] = 2
    movl    $3, 8(%rsp)        # arr[2] = 3
    movl    $4, 12(%rsp)       # arr[3] = 4
    leaq    28(%rsp), %rax     # p = &local_var
    movq    %rax, 16(%rsp)     # 存储 p
    addq    $32, %rsp
    ret

4. 参数传递(栈参数)

当参数超过 6 个时,第 7 个及以后的参数通过栈传递。详见 。

long func(long a1, long a2, long a3, long a4, long a5, long a6,
          long a7, long a8) {
    return a1 + a7;
}
func:
    movq    %rdi, %rax          # a1
    addq    16(%rsp), %rax      # a7 (在返回地址上方 16 字节)
    ret
 
# 调用者视角:
#    %rdi = a1, %rsi = a2, ..., %r9 = a6
#    栈顶之上: a7, a8

栈帧大小

栈帧大小在编译时确定(变长数组 VLAs 除外):

void fixed_size() {
    int arr[10];   // 固定大小,编译时确定
}
 
void variable_size(int n) {
    int arr[n];    // 变长数组 VLA,运行时确定
}

VLA 需要动态调整栈指针:

variable_size:
    pushq   %rbp
    movq    %rsp, %rbp
    # 计算数组大小 N = n * 4
    leaq    (%rdi, %rdi, 4), %rax  # 不对,这只是示意
    salq    $2, %rax               # 实际大小计算
    subq    %rax, %rsp             # 动态分配栈空间
    # ... 初始化 arr 为 0 等 ...
    movq    %rbp, %rsp
    popq    %rbp
    ret

栈帧与递归

每次递归调用创建独立的新栈帧

long factorial(long n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
factorial:
    pushq   %rbx                # 保存 %rbx (被调用者保存)
    movq    %rdi, %rbx          # rbx = n
    cmpq    $1, %rdi            # if (n <= 1)
    jle     .Lbase              # 跳转到基本情况
 
    leaq    -1(%rdi), %rdi      # n - 1
    call    factorial           # 递归调用(创建新栈帧)
    imulq   %rbx, %rax          # n * factorial(n-1)
    popq    %rbx
    ret
 
.Lbase:
    movq    $1, %rax            # return 1
    popq    %rbx
    ret

每次递归调用向栈压入返回地址和保存的寄存器,深度递归可能导致栈溢出(stack overflow)。

相关笔记

    • call/ret 与栈帧的关系
    • 函数参数的传递方式
    • 寄存器的保存策略
    • 栈的基本操作
    • 栈在进程内存中的位置

链接到