栈的数据结构特性

栈是一种后进先出(LIFO, Last In First Out)的数据结构。 主要操作为压栈(push)和出栈(pop),且均针对栈顶进行操作。

程序运行时的栈

在汇编程序运行时,栈用于保存函数调用信息和局部变量。 栈的增长方向:从进程地址空间的高地址向低地址增长(即压栈时栈顶指针地址减小)。

函数调用约定(32位 & 64位)

x86 (32位):函数参数通常保存在栈上,位于函数返回地址的上方(高地址处)。 x64 (64位):Linux 等系统采用 System V AMD64 ABI,前 6 个整型/指针参数依次保存在寄存器 RDI, RSI, RDX, RCX, R8, R9 中,超过 6 个的参数才保存在栈上。

关键寄存器 (Registers)

在 x86 架构中,有三个寄存器对栈的操作至关重要(x64基本同理):

ESP (Extended Stack Pointer):栈顶指针。永远指向栈的最顶部数据。随着 push 减小,随着 pop 增大。 EBP (Extended Base Pointer):栈底指针(或基址指针)。在一个函数执行期间,EBP 通常保持不变,用于通过偏移量定位函数的参数和局部变量(例如 [ebp+8] 是第一个参数,[ebp-4] 是第一个局部变量)。 EIP (Extended Instruction Pointer):指令指针。存储 CPU 下一条要执行的指令地址。控制 EIP 等于控制了程序的执行流。

函数调用过程(Stack Frame 的建立与销毁)

当函数 A 调用函数 B 时,栈的变化通常遵循以下步骤:

调用前(Caller 阶段)

  1. 参数入栈:将传给 B 的参数逆序(从右到左)压入栈中。
  2. 保存返回地址:执行 CALL 指令时,CPU 会自动将下一条指令的地址(即 B 执行完后 A 继续执行的地方)压入栈顶。

进入函数 B(Callee 阶段 - Prologue/序言)

  1. 保存旧栈底:push ebp。将 A 函数的 EBP 压栈保存,以便 B 执行完后能恢复 A 的栈环境。
  2. 建立新栈底:mov ebp, esp。将当前的栈顶作为 B 函数的栈底。
  3. 分配局部变量空间**:sub esp, N。抬高栈顶(地址减小),为 B 的局部变量预留空间。

此时的栈结构图(由高地址向低地址):

[ ... 函数 A 的栈帧 ... ]
-------------------------
[ 参数 n ]
...
[ 参数 1 ]
[ 返回地址 (Return Address) ]  <-- 攻击目标
[ 旧 EBP (Saved EBP) ]
[ 局部变量 1 ]
[ 局部变量 2 ]                 <-- 栈顶 (ESP)

函数返回(Epilogue/尾声)

  1. leave 指令(等价于 mov esp, ebp; pop ebp):恢复 ESP 和 EBP 到函数 A 的状态。
  2. ret 指令:从栈顶弹出数据(即之前保存的返回地址)赋值给EIP,程序跳转回 A 继续执行。