这一部分围绕“函数怎么被调用、参数怎么传、返回地址怎么保存”。理解堆栈之后,调试器里的调用栈、返回地址、局部变量、参数、异常回溯就都能串起来。

1、内容范围

小节 内容 复盘点
06.9 线程栈结构 栈顶、栈底、增长方向和线程私有栈
07.0 - 07.3 PUSH/POP/PUSHAD/POPAD/PUSHFD/POPFD 保存数据、寄存器和标志位
07.4 CALL 与 RET 返回地址、函数入口、返回流程
07.5 MOV 数据移动和参数/局部变量访问
07.6 REP 与 STOS 字符串/批量内存操作
07.7 - 07.9 函数调用堆栈图 栈帧、嵌套调用、回溯
08.5 - 08.9 参数、返回值和调用约定 参数位置、返回寄存器、还原代码

2、线程栈结构

每个线程都有自己的栈。栈常用于保存函数参数、返回地址、局部变量和保存现场。

1
2
3
4
5
6
7
高地址
...
参数
返回地址
保存的 EBP/RBP
局部变量
低地址

x86/x64 下栈通常向低地址增长。

3、PUSH 与 POP

push 把数据压入栈,pop 从栈顶取出数据。

指令 作用
push reg 保存寄存器值
push imm 压入立即数
pop reg 从栈顶恢复到寄存器
pushfd/popfd 保存/恢复标志寄存器

调试时观察 ESP/RSP 的变化,可以理解栈操作。

4、CALL 与 RET

call 做两件事:把返回地址压栈,然后跳到目标函数。ret 从栈顶取回返回地址,跳回调用点。

1
2
3
4
5
6
call target
push next_instruction
jmp target

ret
pop eip/rip

函数返回异常时,优先检查返回地址、栈平衡和调用约定。

5、函数栈帧

典型函数开头可能出现:

1
2
3
push ebp
mov ebp, esp
sub esp, 20h

含义:

指令 作用
push ebp 保存调用者栈帧
mov ebp, esp 建立当前函数栈帧
sub esp, xx 为局部变量留空间

现代编译器可能优化掉固定栈帧,所以不要死背形式,要理解目的。

6、REP/STOS 与批量操作

rep stos 常用于批量填充内存,例如初始化局部变量、清空缓冲区。

指令 作用
stosb/stosd 把 AL/EAX 写入 EDI 指向位置
rep 按 ECX/RCX 次数重复
movsb/movsd 从源地址拷贝到目的地址

遇到大量内存初始化时,反汇编里可能看到这些指令。

7、调用约定与参数

约定 参数 栈清理
cdecl 多数经栈 调用者
stdcall 多数经栈 被调用者
fastcall 部分寄存器 约定决定
x64 前几个参数经寄存器 调用者维护

逆向函数时,参数位置和清栈方式非常关键。

8、返回值

常见返回值位置:

类型 常见位置
整数/指针 EAX/RAX
较大结构体 可能通过隐藏指针返回
浮点数 FPU 或 XMM 寄存器

9、函数回溯

函数回溯要把三件事对上:

  1. 当前 EIP/RIP 在哪个函数。
  2. 当前栈帧的返回地址在哪里。
  3. 调用者传入了哪些参数。

10、学习检查清单

检查项 状态
能画出一次 call/ret 的栈变化 待复盘
能解释返回地址为什么保存在栈里 待复盘
能根据 [ebp+8][ebp-4] 区分参数和局部变量 待复盘
能说明调用约定不匹配为什么会崩溃 待复盘
能在调试器里做一次函数回溯 待复盘