017、堆栈—线程栈与 PUSH/POP
返回:网络安全知识库 · 上一节:位运算与操作数 · 下一节:CALL/RET 与函数调用
线程栈是什么
每个线程都拥有自己的栈空间。函数参数、返回地址、局部变量以及部分需要临时保存的寄存器,都会在函数调用过程中进入线程栈。
栈是一段按照后进先出规则使用的内存,栈顶地址由 ESP/RSP 记录。
在 32 位程序中,常见的两个栈相关寄存器是:
| 寄存器 | 作用 |
|---|---|
ESP |
指向当前栈顶 |
EBP |
传统栈帧中作为当前函数的基准地址 |
在 64 位程序中,它们对应 RSP 和 RBP。
Windows 用户态程序的栈通常从较高地址向较低地址增长。因此,压栈会让栈指针减小,出栈会让栈指针增大。
1 | 高地址 |
PUSH 指令
以 32 位环境中的 push eax 为例,可以把它理解为两步:
1 | sub esp, 4 |
实际由一条 PUSH 指令完成,但调试时要同时观察:
ESP减少了 4。- 新栈顶保存了原来的
EAX值。 - 被压入的数据仍然按照小端序存储。
64 位环境下,常规栈操作以 8 字节为单位,使用 RSP。
压栈保存的是当前值的副本。之后修改原寄存器,不会自动改变栈中已经保存的数据。
POP 指令
pop eax 可以近似拆成:
1 | mov eax, dword ptr [esp] |
它先读取当前栈顶,再移动栈指针。连续执行:
1 | push eax |
最终 ECX 得到原 EBX,EDX 得到原 EAX,这就是后进先出。
栈平衡
一段逻辑执行结束后,如果没有故意改变调用结构,ESP/RSP 应回到预期位置。
1 | push eax |
压入和弹出的数量、宽度、顺序不匹配,会造成栈失衡。常见现象包括:
- 返回地址被当成普通数据弹出。
- 函数返回后执行到错误地址。
- 参数位置整体错位。
- 异常只在后续
RET或访问局部变量时才出现。
PUSHAD 与 POPAD
在 32 位 x86 中,PUSHAD 用于批量保存通用寄存器。压栈顺序为:
1 | EAX → ECX → EDX → EBX → 原 ESP → EBP → ESI → EDI |
POPAD 按相反方向恢复寄存器。保存的“原 ESP”位置只用于保持布局,恢复时该槽位会被跳过。
PUSHAD/POPAD 是 32 位指令,x64 长模式不支持。64 位代码通常按需保存寄存器,并遵守调用约定规定的非易失寄存器集合。
PUSHFD 与 POPFD
PUSHFD 把 EFLAGS 保存到栈中,POPFD 再将其恢复。64 位环境对应 PUSHFQ/POPFQ。
它们适合在一段会改变标志位的代码前后保存环境:
1 | pushfd |
不过,用户态程序不能依靠 POPFD/POPFQ 随意修改所有特权相关标志位。
栈对齐
栈不仅要“数量平衡”,还要满足 ABI 的对齐要求。尤其在 x64 Windows 调用中,需要关注:
- 调用前的栈对齐。
- 调用者预留的影子空间。
- 局部变量和保存寄存器占用的空间。
如果对齐被破坏,普通整数指令可能暂时不报错,但使用 SIMD 指令或调用系统函数时可能异常。
调试观察方法
可以在调试器中准备一个很小的实验:
1 | mov eax, 11223344h |
逐条执行并记录:
- 每条指令执行前后的
ESP/RSP。 - 栈窗口中新增或消失的数据。
- 小端序显示与寄存器整数显示之间的关系。
- 最终四个寄存器的值。
逆向时不要只记“栈向下增长”,而要养成同时追踪栈指针、内存内容和数据用途的习惯。
学习检查清单
| 检查项 | 状态 |
|---|---|
| 能说明线程栈与 ESP/RSP 的关系 | 待复盘 |
| 能解释 PUSH 和 POP 对栈指针的影响 | 待复盘 |
| 能根据压栈顺序判断出栈结果 | 待复盘 |
| 能说明 PUSHAD/POPAD 的顺序和适用架构 | 待复盘 |
| 能使用 PUSHFD/POPFD 保存和恢复标志寄存器 | 待复盘 |
| 能在调试器中判断栈是否平衡 | 待复盘 |











