栈的数据结构特性
栈是一种后进先出(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 阶段)
- 参数入栈:将传给 B 的参数逆序(从右到左)压入栈中。
- 保存返回地址:执行
CALL指令时,CPU 会自动将下一条指令的地址(即 B 执行完后 A 继续执行的地方)压入栈顶。
进入函数 B(Callee 阶段 - Prologue/序言)
- 保存旧栈底:
push ebp。将 A 函数的 EBP 压栈保存,以便 B 执行完后能恢复 A 的栈环境。 - 建立新栈底:
mov ebp, esp。将当前的栈顶作为 B 函数的栈底。 - 分配局部变量空间**:
sub esp, N。抬高栈顶(地址减小),为 B 的局部变量预留空间。
此时的栈结构图(由高地址向低地址):
[ ... 函数 A 的栈帧 ... ]
-------------------------
[ 参数 n ]
...
[ 参数 1 ]
[ 返回地址 (Return Address) ] <-- 攻击目标
[ 旧 EBP (Saved EBP) ]
[ 局部变量 1 ]
[ 局部变量 2 ] <-- 栈顶 (ESP)
函数返回(Epilogue/尾声)
leave指令(等价于mov esp, ebp; pop ebp):恢复 ESP 和 EBP 到函数 A 的状态。ret指令:从栈顶弹出数据(即之前保存的返回地址)赋值给EIP,程序跳转回 A 继续执行。