基础定义
栈由操作系统为进程预先分配,承担函数参数传递、保存返回地址、存放局部临时数据、寄存器现场等工作
堆栈平衡:调用函数前与函数完全执行完毕后,ESP 栈指针数值必须一模一样,栈整体状态完全还原
核心要求:函数内部临时增减 ESP 没关系,但 RET 返回前必须抵消掉参数压栈带来的 ESP 偏移;失衡会栈错乱、数据读取错误、程序崩溃
失衡危害
调用前 push 了若干参数,函数返回后 ESP 没有复原:
1.后续栈读写拿到错误内存数据
2.多层函数嵌套调用时,返回地址错位,EIP 跳飞
3.严重直接程序异常崩溃
标准准则:调用点执行 call 前后 ESP 数值相等
两种堆栈平衡方案(外平衡、内平衡)
外平衡(调用方平衡,对应 C 语言 cdecl 约定)
平衡代码写在call 指令之后、函数外部
原理:调用端 push 参数,函数 ret 单纯只弹出返回地址,外部手动 add esp 回收参数占用空间
示例
cpp
;调用端
push 10
push 20
call MyFunc
add esp,8 ;外平衡:外部修正ESP,回收两个参数8字节空间
;函数内部
MyFunc:
mov eax,[esp+4]
add eax,[esp+8]
ret ;只弹出返回地址,不处理参数
内平衡(被调函数内部平衡,stdcall 约定)
平衡逻辑放在函数内部,依靠ret n指令实现
ret n动作:弹出返回地址给 EIP 后,自动执行 ESP = ESP + n,一次性清空 n 字节参数空间
示例
cpp
;调用端只负责压参、调用,无后续平衡代码
push 10
push 20
call MyFunc
;函数内部内平衡
MyFunc:
mov eax,[esp+4]
add eax,[esp+8]
ret 8 ;内平衡:弹返回地址 + ESP+=8,清理两个参数
执行流程对比
外平衡流程: push 参数 → call 压返回地址 → 函数运算 → ret 回到 call 下一行 → 外部 add esp 修正栈
内平衡流程: push 参数 → call 压返回地址 → 函数运算 → ret n 一步完成返回 + 栈平衡,调用端无需额外代码
细节
1.函数内部开栈(sub esp 分配局部变量)、push 保存寄存器,只要在 ret 前原样 pop 恢复寄存器、恢复 ESP,不影响整体堆栈平衡
2.返回值统一存 EAX,平衡操作不改变 EAX 结果
3.平衡数值计算:参数个数×4 字节(32 位)
约定 平衡方式 返回指令 可变参数
cdecl 调用方外平衡 ret 支持
stdcall 函数内平衡 ret n 不支持