基本栈介绍

栈是一种典型的后进先出 (Last in First Out) 的数据结构,其操作主要有压栈 (push) 与出栈 (pop) 两种操作,如下图所示(维基百科)。两种操作都操作栈顶,当然,它也有栈底。

image-20250701104727634

高级语言在运行时都会被转换为汇编程序,在汇编程序运行过程中,充分利用了这一数据结构。每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保存函数调用信息和局部变量。此外,常见的操作也是压栈与出栈。需要注意的是,程序的栈是从进程地址空间的高地址向低地址增长的

函数调用栈

核心组件

栈指针 (SP)

  • x86: ESP (32位), x64: RSP (64位)
  • 始终指向栈顶位置
  • push/pop 指令自动修改 SP

基址指针 (BP)

  • x86: EBP, x64: RBP
  • 作为当前栈帧的基准点
  • 用于定位参数和局部变量

指令指针 (IP)

  • x86: EIP, x64: RIP
  • 存储下一条执行指令地址
  • call/ret 指令修改 IP

函数调用过程(关键!)

调用者 (Caller) 准备

; 1. 参数压栈(从右向左)
push arg3
push arg2
push arg1
; 2. 调用函数
call function ; 自动压入返回地址(EIP/RIP)

被调函数 (Callee) 序言

function:
; 1. 保存调用者栈帧
push ebp ; 保存旧EBP
; 2. 建立新栈帧
mov ebp, esp ; EBP = 当前ESP
; 3. 分配局部变量空间
sub esp, 0x20 ; 分配32字节空间

栈帧内存布局(32位示例)

高地址
+-----------------+
| 参数3 | [ebp + 16]
+-----------------+
| 参数2 | [ebp + 12]
+-----------------+
| 参数1 | [ebp + 8]
+-----------------+
| 返回地址 | [ebp + 4] ← 漏洞利用关键点!
+-----------------+
| 保存的EBP | <-- EBP (当前栈帧基址)
+-----------------+
| 局部变量1 | [ebp - 4]
+-----------------+
| 局部变量2 | [ebp - 8]
+-----------------+
| ... |
+-----------------+
| 临时空间 | <-- ESP (栈顶)
低地址

函数返回过程

; 1. 返回值存入EAX(约定)
mov eax, return_value
; 2. 释放局部空间
mov esp, ebp ; ESP = EBP
; 3. 恢复调用者栈帧
pop ebp ; 恢复旧EBP
; 4. 返回到调用者
ret ; 弹出返回地址到EIP

Tip

寄存器的图

img

需要注意的是,32 位和 64 位程序有以下简单的区别:

  • x86

    • 函数参数函数返回地址的上方

    • 栈帧布局

      高地址
      | 参数N | ← EBP + 4*(N+1)
      | ... |
      | 参数2 | ← EBP + 12
      | 参数1 | ← EBP + 8
      | 返回地址 | ← EBP + 4
      | 保存的EBP | ← EBP
      | 局部变量 | ← EBP - 4
      低地址
  • x64

    • System V AMD64 ABI (Linux、FreeBSD、macOS 等采用) 中前六个整型或指针参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9 寄存器中,如果还有更多的参数的话才会保存在栈上。

    • 内存地址不能大于 0x00007FFFFFFFFFFF,6 个字节长度,否则会抛出异常。

    • 栈帧布局

      高地址
      | 额外参数N | ← RBP + 8*(N+1)
      | ... |
      | 额外参数1 | ← RBP + 16
      | 返回地址 | ← RBP + 8
      | 保存的RBP | ← RBP
      | 局部变量 | ← RBP - 8
      低地址