- ESP (Extended Stack Pointer) 是动态变化的栈顶指针,指向栈顶元素。
- EBP (Extended Base Pointer) 是帧指针,在函数生命周期内通常固定,作为访问局部变量、参数和返回地址的稳定基准。
建立新栈帧的典型序列。
push ebp ; 保存调用者的 EBP (旧 EBP) 到栈上
mov ebp, esp ; 将当前 ESP 设为新 EBP,确立当前函数的栈帧基址
sub esp, N ; 为局部变量分配 N 字节空间,ESP 向低地址移动
EBP 寻址布局:以 EBP 为基准,可以稳定地访问栈上内容,不受 ESP 变化影响。
- [ebp]:存放旧的 EBP 值,用于函数返回时恢复调用者的栈帧。
- [ebp+4]:存放返回地址 (Return Address),即 call 指令下一条指令的地址。
- [ebp+8] 及以上:存放传递给当前函数的参数 (从右到左入栈)。
- [ebp-4] 及以下:存放当前函数的局部变量。
指令对 ESP 的影响:
- push r32:等价于 sub esp, 4; mov [esp], r32。ESP 减 4。
- pop r32:等价于 mov r32, [esp]; add esp, 4。ESP 加 4。
- call address:等价于 push return_address; jmp address。
ESP 减 4,用于保存返回地址。
- ret:等价于 pop eip (实际上是 pop ip 并跳转)。ESP 加 4。
- retn N:等价于 pop eip; add esp, N。ESP 加 4 后再加 N,用于被调用者清理参数占用的栈空间。
函数收尾:销毁当前栈帧并返回。
// 标准形式:
mov esp, ebp ; 恢复 ESP 到 EBP 位置,丢弃所有局部变量
pop ebp ; 从栈上恢复调用者的 EBP
ret ; 弹出返回地址并跳转
// 紧凑形式:leave 指令等价于 mov esp, ebp; pop ebp。
leave ; 恢复 ESP 和 EBP
ret ; 返回
调用约定:决定了参数传递方式和栈清理责任。
- cdecl (C/C++ 默认):调用者 (Caller) 负责清理栈空间。通常在 call 指令后执行 add esp, N。这是支持可变参数函数 (如 printf) 的关键,因为只有调用者确切知道传递了多少参数。
- stdcall (WinAPI 默认):被调用者 (Callee) 负责清理栈空间,通过 retn N 指令实现。函数签名固定,不支持可变参数。
- 帧指针省略 (FPO):一种常见的编译器优化。当函数比较简单 (如叶子函数 Leaf Function) 或开启优化时,编译器可能不使用 EBP作为帧指针,而是将其当作一个通用寄存器使用。此时,局部变量和参数的寻址将直接基于 ESP 进行,这使得调试和逆向分析变得更复杂。
- x86-64 提示:在 64 位模式下,由于引入了 RIP 相对寻址和更多的通用寄存器,帧指针 (RBP) 常常被省略。参数传递优先通过寄存器 (如 RDI, RSI, RDX 等) 完成,而不是栈。
- 调试与逆向要点:
- EBP 链:通过 [ebp]、[[ebp]] ... 可以回溯整个函数调用链。
- 栈平衡:检查函数执行前后 ESP 的值是否恢复原状。ESP 不平衡是导致程序崩溃的常见原因。
- retn N:N 的值应与函数参数所占的总字节数匹配。
- 栈破坏:缓冲区溢出等漏洞可能覆盖栈上的返回地址或 EBP 值,从而劫持程序控制流。