006、函数的工作原理
CALL 与 RET
CALL 保存下一条指令地址并转移控制流;RET 从栈中取得返回地址。
1 | CALL ≈ push 返回地址 + jump 目标 |
直接相对调用常见 E8 rel32,间接调用常见 FF /2,后者经常对应函数指针、IAT 或虚函数。
经典 x86 栈帧
1 | push ebp |
典型布局:
| 位置 | 常见含义 |
|---|---|
[ebp] |
保存的上一层 EBP |
[ebp+4] |
返回地址 |
[ebp+8] |
第一个栈参数 |
[ebp-4] |
局部变量或临时空间 |
函数尾声常见:
1 | mov esp, ebp |
或 leave; ret。
参数和局部变量的偏移只是传统未优化栈帧的常见形式,不是所有函数都必须使用 EBP。
x86 调用方式
| 调用约定 | 参数 | 清栈 | 典型线索 |
|---|---|---|---|
__cdecl |
从右到左入栈 | 调用者 | call 后 add esp, n |
__stdcall |
从右到左入栈 | 被调用者 | ret n |
__fastcall |
部分参数用 ECX、EDX | 通常被调用者 | 调用前写 ECX、EDX |
__thiscall |
ECX 常保存 this | 依编译器 | 成员访问基于 ECX |
可变参数函数通常需要调用者清栈,因为只有调用者知道实际参数数量。
参数恢复
恢复函数原型时同时检查:
- 调用前准备了多少数据。
- 函数内部实际读取了哪些参数。
- 参数按值使用还是作为地址解引用。
- 所有调用点是否保持一致。
未被读取的压栈数据可能是对齐、寄存器保存或其他调用的准备,不应仅凭一处 push 计数。
返回值
常见规则:
- x86 整数和指针:
EAX。 - x86 宽整数:可能使用
EDX:EAX。 - Windows x64 整数和指针:
RAX。 - 浮点数:常见
XMM0,旧 x86 代码可能使用 x87。 - 大对象:可能通过隐藏返回缓冲区指针完成。
Windows x64 调用约定
前四个整数或指针参数使用:
1 | RCX, RDX, R8, R9 |
调用者还需要:
- 预留 32 Byte 影子空间。
- 在调用点满足 16 Byte 栈对齐要求。
- 把更多参数放到栈中。
非易失寄存器在函数返回前必须恢复,易失寄存器可被被调用函数破坏。
帧指针省略与内联
优化构建中可能不使用 EBP/RBP 作为帧指针,局部变量直接基于 ESP/RSP 寻址。小函数还可能完全内联,不再存在独立 CALL。
函数边界、参数数量和调用约定必须结合调用点、返回点、栈变化与展开信息共同判断。
函数分析模板
1 | 函数地址: |
学习检查清单
| 检查项 | 状态 |
|---|---|
| 能说明 CALL 与 RET 对栈的影响 | 待复盘 |
| 能画出经典 x86 栈帧 | 待复盘 |
| 能区分 cdecl、stdcall、fastcall 和 thiscall | 待复盘 |
| 能说明 Windows x64 前四个整数参数的位置 | 待复盘 |
| 能根据调用点验证候选函数原型 | 待复盘 |
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Ruiqy~!






















