🎨 颜色标记说明

  • 粉色:特别重要、主线必掌握、优先复盘的内容。
  • 蓝色:核心概念、术语、分类名。
  • 橙色:需要关注的边界、易错点、注意事项。
  • 绿色:解释类内容、帮助理解知识点。

线程栈是什么

每个线程都拥有自己的栈空间。函数参数、返回地址、局部变量以及部分需要临时保存的寄存器,都会在函数调用过程中进入线程栈。

栈是一段按照后进先出规则使用的内存,栈顶地址由 ESP/RSP 记录。

在 32 位程序中,常见的两个栈相关寄存器是:

寄存器 作用
ESP 指向当前栈顶
EBP 传统栈帧中作为当前函数的基准地址

在 64 位程序中,它们对应 RSPRBP

Windows 用户态程序的栈通常从较高地址向较低地址增长。因此,压栈会让栈指针减小,出栈会让栈指针增大。

1
2
3
4
5
6
7
8
9
10
11
高地址
┌─────────────────┐
│ 调用者的数据 │
├─────────────────┤
│ 返回地址 │
├─────────────────┤
│ 保存的寄存器 │
├─────────────────┤
│ 局部变量 │ ← ESP/RSP 附近
└─────────────────┘
低地址

PUSH 指令

以 32 位环境中的 push eax 为例,可以把它理解为两步:

1
2
sub esp, 4
mov dword ptr [esp], eax

实际由一条 PUSH 指令完成,但调试时要同时观察:

  • ESP 减少了 4。
  • 新栈顶保存了原来的 EAX 值。
  • 被压入的数据仍然按照小端序存储。

64 位环境下,常规栈操作以 8 字节为单位,使用 RSP

压栈保存的是当前值的副本。之后修改原寄存器,不会自动改变栈中已经保存的数据。

POP 指令

pop eax 可以近似拆成:

1
2
mov eax, dword ptr [esp]
add esp, 4

它先读取当前栈顶,再移动栈指针。连续执行:

1
2
3
4
push eax
push ebx
pop ecx
pop edx

最终 ECX 得到原 EBXEDX 得到原 EAX,这就是后进先出

栈平衡

一段逻辑执行结束后,如果没有故意改变调用结构,ESP/RSP 应回到预期位置。

1
2
3
4
5
push eax
push ebx
; 中间逻辑
pop ebx
pop 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

PUSHFDEFLAGS 保存到栈中,POPFD 再将其恢复。64 位环境对应 PUSHFQ/POPFQ

它们适合在一段会改变标志位的代码前后保存环境:

1
2
3
4
pushfd
cmp eax, ebx
; 使用比较结果
popfd

不过,用户态程序不能依靠 POPFD/POPFQ 随意修改所有特权相关标志位。

栈对齐

栈不仅要“数量平衡”,还要满足 ABI 的对齐要求。尤其在 x64 Windows 调用中,需要关注:

  • 调用前的栈对齐。
  • 调用者预留的影子空间。
  • 局部变量和保存寄存器占用的空间。

如果对齐被破坏,普通整数指令可能暂时不报错,但使用 SIMD 指令或调用系统函数时可能异常。

调试观察方法

可以在调试器中准备一个很小的实验:

1
2
3
4
5
6
mov eax, 11223344h
mov ebx, 55667788h
push eax
push ebx
pop ecx
pop edx

逐条执行并记录:

  1. 每条指令执行前后的 ESP/RSP
  2. 栈窗口中新增或消失的数据。
  3. 小端序显示与寄存器整数显示之间的关系。
  4. 最终四个寄存器的值。

逆向时不要只记“栈向下增长”,而要养成同时追踪栈指针、内存内容和数据用途的习惯。

学习检查清单

检查项 状态
能说明线程栈与 ESP/RSP 的关系 待复盘
能解释 PUSH 和 POP 对栈指针的影响 待复盘
能根据压栈顺序判断出栈结果 待复盘
能说明 PUSHAD/POPAD 的顺序和适用架构 待复盘
能使用 PUSHFD/POPFD 保存和恢复标志寄存器 待复盘
能在调试器中判断栈是否平衡 待复盘