C&C++软件逆向技术

2、函数调用约定

1、课程目标

  1. 掌握常见调用约定的特点
  2. 学会识别反汇编中的调用约定
  3. 理解参数传递和栈清理机制
  4. 掌握函数返回值的处理方式

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、课后作业

  1. 基础练习:编写不同调用约定的函数,观察生成的汇编
  2. 反汇编分析:分析一个DLL导出函数,识别其调用约定
  3. x64分析:对比同一函数在x86和x64下的不同
  4. 原型还原:根据反汇编代码还原3个函数的完整原型