3.5 栈操作

栈是 x86-64 中管理函数调用、局部变量和临时数据的重要数据结构。栈在内存中向下增长(向低地址方向扩展)。

栈的特点

  • 后进先出(LIFO):最后压入的数据最先弹出
  • 向下增长pushq 减小 %rsppopq 增加 %rsp
  • %rsp(栈指针寄存器)始终指向当前栈的栈顶

栈的内存布局:

高地址
+-----------------+
|   调用者栈帧      |
+-----------------+ <--- %rbp (可选基址)
|   被调用者栈帧    |
+-----------------+ <--- %rsp (始终指向栈顶)
|       ...        |
低地址

pushq 指令

pushq   源操作数

等价操作(两步骤):

subq    $8, %rsp          # 1. 栈指针减 8(分配空间)
movq    源操作数, (%rsp)   # 2. 将值写入新栈顶

执行效果

假设初始 %rsp = 0x1000%rax = 0x1234

pushq   %rax
# 1. %rsp = 0x1000 - 8 = 0x0FF8
# 2. 内存[0x0FF8] = 0x1234
pushq 之前:
地址 0x1000 ┌──────────────┐
            │   ...        │
            └──────────────┘

pushq 之后:
地址 0x0FF8 ┌──────────────┐ ← %rsp
            │   0x1234     │
地址 0x1000 ├──────────────┤
            │   ...        │
            └──────────────┘

pushq 的操作数类型

pushq   $0xFF              # 压入立即数
pushq   %rax               # 压入寄存器
pushq   (%rdi)             # 压入内存中的值
pushq   8(%rbp)            # 压入栈上的值

popq 指令

popq    目的操作数

等价操作(两步骤):

movq    (%rsp), 目的操作数  # 1. 从栈顶读取值
addq    $8, %rsp           # 2. 栈指针加 8(回收空间)

执行效果

popq    %rbx
# 1. %rbx = 内存[0x0FF8] = 0x1234
# 2. %rsp = 0x0FF8 + 8 = 0x1000
popq 之前:
地址 0x0FF8 ┌──────────────┐ ← %rsp
            │   0x1234     │
            └──────────────┘

popq 之后:
地址 0x0FF8 ┌──────────────┐
            │   0x1234     │ (数据仍然存在,但已无效)
地址 0x1000 ├──────────────┤ ← %rsp
            │   ...        │
            └──────────────┘

注意popq 后栈上的数据物理上仍然存在,但逻辑上属于无效区域。下次 pushq 会覆盖该位置。

pushq %rsp 的特殊语义

不同处理器对 pushq %rsp 的行为定义不同,x86-64 采用 Intel 语义

pushq   %rsp
# Intel 语义(x86-64 采用):先减 rsp,再存原始 rsp 的值
# 等价于:
#   temp = %rsp       # 保存原始值
#   %rsp = %rsp - 8   # 分配空间
#   memory[%rsp] = temp  # 写入原始值

popq %rsp 的特殊语义

popq    %rsp
# 等价于:
#   %rsp = memory[%rsp]   # 读取栈顶值作为新栈指针
#   %rsp = %rsp + 8       # 然后加 8

这实际上是将栈顶的值弹出作为新的栈指针——一种栈切换操作。

栈操作的典型用途

1. 保存和恢复寄存器

func:
    pushq   %rbx            # 保存 %rbx 到栈
    pushq   %rbp            # 保存 %rbp 到栈
    # ... 函数体,可以使用 %rbx 和 %rbp ...
    popq    %rbp            # 恢复 %rbp
    popq    %rbx            # 恢复 %rbx
    ret

2. 临时数据存储

pushq   %rax               # 临时保存 %rax
# ... 使用 %rax 做其他事情 ...
popq    %rdx               # 将之前保存的值恢复到 %rdx

3. 函数调用与返回

call 指令隐式 pushq 返回地址,ret 指令隐式 popq 返回地址。详见 。

栈操作与函数调用

在函数调用中,栈的典型变化:

# 调用前:%rsp = 0x1000
call    func              # pushq 返回地址; %rsp = 0x0FF8
 
func:
    pushq   %rbp          # 保存旧 %rbp; %rsp = 0x0FF0
    movq    %rsp, %rbp    # 设置新 %rbp
    subq    $16, %rsp     # 分配局部变量空间; %rsp = 0x0FE0
 
    # 函数体...
 
    movq    %rbp, %rsp    # 回收局部变量空间; %rsp = 0x0FF0
    popq    %rbp          # 恢复 %rbp; %rsp = 0x0FF8
    ret                   # popq 返回地址; %rsp = 0x1000

相关笔记

    • %rsp 栈指针寄存器
    • 栈帧的完整结构
    • call/ret 与栈操作的关系
    • 使用栈保存寄存器

链接到