1、课程目标
- 掌握常见调用约定的特点
- 学会识别反汇编中的调用约定
- 理解参数传递和栈清理机制
- 掌握函数返回值的处理方式
2、名词解释
| 术语 |
英文 |
说明 |
| cdecl |
C Declaration |
C语言默认调用约定,调用者清栈 |
| stdcall |
Standard Call |
Win32 API默认约定,被调用者清栈 |
| fastcall |
Fast Call |
使用寄存器传递前几个参数 |
| thiscall |
This Call |
C++成员函数调用约定 |
| x64 ABI |
x64 Application Binary Interface |
64位程序调用约定 |
3、使用工具
| 工具 |
用途 |
| IDA Pro |
识别和标注调用约定 |
| x64dbg |
观察参数传递过程 |
| Compiler Explorer |
在线查看编译生成的代码 |
4、技术原理
4.1、x86调用约定对比
| 约定 |
参数顺序 |
栈清理 |
特点 |
| cdecl |
右到左 |
调用者 |
支持可变参数 |
| stdcall |
右到左 |
被调用者 |
ret N指令 |
| fastcall |
ECX, EDX, 然后栈 |
被调用者 |
更快的参数传递 |
| thiscall |
ECX=this, 栈 |
被调用者 |
C++成员函数 |
4.2、x64调用约定 (Windows)
| 参数 |
整数/指针 |
浮点数 |
| 1 |
RCX |
XMM0 |
| 2 |
RDX |
XMM1 |
| 3 |
R8 |
XMM2 |
| 4 |
R9 |
XMM3 |
| 5+ |
栈 |
栈 |
5、代码实现
5.1、示例1:不同调用约定对比
#include <stdio.h>
// cdecl - 调用者清栈
int __cdecl CdeclFunc(int a, int b, int c) {
return a + b + c;
}
// stdcall - 被调用者清栈
int __stdcall StdcallFunc(int a, int b, int c) {
return a + b + c;
}
// fastcall - 寄存器传递
int __fastcall FastcallFunc(int a, int b, int c) {
return a + b + c;
}
int main() {
int r1 = CdeclFunc(1, 2, 3);
int r2 = StdcallFunc(1, 2, 3);
int r3 = FastcallFunc(1, 2, 3);
printf("Results: %d, %d, %d\n", r1, r2, r3);
return 0;
}
5.2、示例2:反汇编特征识别
; cdecl调用特征
push 3 ; 第三个参数
push 2 ; 第二个参数
push 1 ; 第一个参数
call CdeclFunc
add esp, 12 ; 调用者清理栈(12字节 = 3个参数)
; stdcall调用特征
push 3
push 2
push 1
call StdcallFunc ; 注意:没有add esp
; 因为函数内部使用 ret 12
; fastcall调用特征
push 3 ; 第三个参数还是通过栈
mov edx, 2 ; 第二个参数用EDX
mov ecx, 1 ; 第一个参数用ECX
call FastcallFunc
5.3、示例3:thiscall C++成员函数
class Calculator {
public:
int value;
int Add(int x) {
return value + x;
}
};
// 反汇编特征:
// mov ecx, [esp+4] ; ECX = this指针
// mov eax, [ecx] ; 获取value成员
// add eax, [esp+8] ; 加上参数x
// ret 4 ; 清理参数
5.4、示例4:x64调用约定
// x64函数
void Func64(int a, int b, int c, int d, int e, int f) {
// a = RCX, b = RDX, c = R8, d = R9
// e = [RSP+28h], f = [RSP+30h]
}
// x64反汇编特征:
// sub rsp, 38h ; 分配栈空间(包含影子空间)
// mov [rsp+30h], 6 ; 第6参数
// mov [rsp+28h], 5 ; 第5参数
// mov r9d, 4 ; 第4参数
// mov r8d, 3 ; 第3参数
// mov edx, 2 ; 第2参数
// mov ecx, 1 ; 第1参数
// call Func64
5.5、示例5:识别调用约定的工具
#include <stdio.h>
// 通过汇编验证调用约定
void AnalyzeCall() {
void* retAddr;
int stackDelta;
__asm {
; 在调用前后比较ESP可以判断谋谁清栈
mov eax, esp
mov retAddr, eax
}
printf("ESP at call site: 0x%p\n", retAddr);
}
// 识别ret N指令
void IdentifyStdcall() {
/*
在IDA中查看函数结尾:
- ret -> cdecl 或 fastcall
- ret 4 -> stdcall, 1个参数
- ret 8 -> stdcall, 2个参数
- ret 12 -> stdcall, 3个参数
或者查看调用点:
- call xxx; add esp, N -> cdecl
- call xxx -> stdcall
- mov ecx/edx + call -> fastcall 或 thiscall
*/
}
int main() {
AnalyzeCall();
return 0;
}
5.6、示例6:还原函数原型
/*
根据反汇编还原函数原型的步骤:
1. 确定调用约定
- 看调用点是否有add esp
- 看函数结尾是否有ret N
- 看是否使用ECX/EDX传参
2. 确定参数数量
- cdecl: add esp, N 中 N/4 = 参数数
- stdcall: ret N 中 N/4 = 参数数
- fastcall: ECX和EDX + 栈参数
3. 确定参数类型
- [ebp+8], [ebp+12]... 的使用方式
- movzx -> 无符号字节/字
- movsx -> 有符号字节/字
- mov eax, [x] -> DWORD或指针
4. 确定返回值
- EAX为返回值
- test eax, eax -> 可能是BOOL或指针
- 无返回值则为void
*/
// 例如,看到以下反汇编:
// push [ebp+10h]
// push [ebp+0Ch]
// push [ebp+8]
// call sub_401000
// add esp, 0Ch
// test eax, eax
// 可以还原为:
int __cdecl sub_401000(int param1, int param2, int param3);
6、课后作业
- 基础练习:编写不同调用约定的函数,观察生成的汇编
- 反汇编分析:分析一个DLL导出函数,识别其调用约定
- x64分析:对比同一函数在x86和x64下的不同
- 原型还原:根据反汇编代码还原3个函数的完整原型