C&C++软件逆向技术

2、函数调用约定

1、课程目标

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

2、名词解释

术语英文说明
cdeclC DeclarationC语言默认调用约定,调用者清栈
stdcallStandard CallWin32 API默认约定,被调用者清栈
fastcallFast Call使用寄存器传递前几个参数
thiscallThis CallC++成员函数调用约定
x64 ABIx64 Application Binary Interface64位程序调用约定

3、使用工具

工具用途
IDA Pro识别和标注调用约定
x64dbg观察参数传递过程
Compiler Explorer在线查看编译生成的代码

4、技术原理

4.1、x86调用约定对比

约定参数顺序栈清理特点
cdecl右到左调用者支持可变参数
stdcall右到左被调用者ret N指令
fastcallECX, EDX, 然后栈被调用者更快的参数传递
thiscallECX=this, 栈被调用者C++成员函数

4.2、x64调用约定 (Windows)

参数整数/指针浮点数
1RCXXMM0
2RDXXMM1
3R8XMM2
4R9XMM3
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个函数的完整原型