文章摘要
C语言中常见的函数调用约定包括cdecl、stdcall和fastcall,它们规定了参数传递、栈维护和寄存器使用规则。cdecl是C语言默认约定,支持可变参数,调用者清理栈;stdcall用于Windows API,被调用者清理栈;fastcall通过寄存器传递部分参数以提升性能。此外,thiscall用于C++成员函数,vectorcall优化向量运算。不同调用约定不可混用,64位平台通常采用统一约定。开发者需注意跨平台兼容性和调用约定的正确声明。
C语言中常见的调用约定(Calling Convention),比如 cdecl
、stdcall
、fastcall
。这些约定主要规定了函数参数如何传递、返回值如何传递、栈的维护方式、寄存器的使用规则等。下面详细介绍:
1. cdecl(C Declaration)
- 全称:C declaration
- 参数传递:参数从右到左依次压栈(即最后一个参数先入栈)。
- 栈清理:调用者负责清理栈(Caller Clean-up)。
- 返回值:通常通过EAX寄存器返回。
- 可变参数:支持可变参数函数(如
printf
)。 - 使用场景:C语言默认调用约定(在GCC、MSVC下C语言默认就是cdecl)。
- 声明方式(MSVC):
__cdecl
- 示例:
int __cdecl add(int a, int b);
2. stdcall(Standard Call)
- 全称:Standard Call
- 参数传递:参数从右到左依次压栈。
- 栈清理:被调用者负责清理栈(Callee Clean-up)。
- 返回值:通过EAX返回。
- 可变参数:不支持可变参数函数。
- 使用场景:Windows API函数大多采用stdcall。
- 声明方式(MSVC):
__stdcall
- 示例:
int __stdcall add(int a, int b);
3. fastcall(Fast Call)
- 全称:Fast Call
- 参数传递:前几个参数通过寄存器传递(如x86下ECX、EDX),剩余参数从右到左压栈。
- 栈清理:被调用者负责清理栈。
- 返回值:通过EAX返回。
- 可变参数:不支持可变参数函数。
- 使用场景:对性能要求高的场合。
- 声明方式(MSVC):
__fastcall
- 示例:
int __fastcall add(int a, int b);
4. 其他常见调用约定
- thiscall:C++成员函数的默认调用约定,
this
指针通过ECX传递。 - vectorcall:用于向量运算,参数通过SIMD寄存器传递(MSVC支持)。
5. 调用约定的区别总结
调用约定 | 参数传递方式 | 栈清理者 | 可变参数 | 主要用途 |
---|---|---|---|---|
cdecl | 右到左,全部压栈 | 调用者 | 支持 | C语言默认 |
stdcall | 右到左,全部压栈 | 被调用者 | 不支持 | Windows API |
fastcall | 前2个寄存器,其余压栈 | 被调用者 | 不支持 | 性能优化 |
6. 代码示例(MSVC)
int __cdecl func1(int a, int b); // cdecl
int __stdcall func2(int a, int b); // stdcall
int __fastcall func3(int a, int b); // fastcall
7. 注意事项
- 不同调用约定不能混用,否则会导致栈混乱、程序崩溃。
- 跨平台时要注意,不同编译器、不同平台的默认调用约定可能不同。
- 64位下(如x86_64),Windows和Linux都有自己的统一调用约定,cdecl/stdcall/fastcall等在64位下意义不大。
好的,下面我将详细介绍cdecl、stdcall、fastcall三种调用约定在x86(32位)平台下的汇编实现细节,并简要说明它们在不同平台(如x86_64、ARM等)下的差异。
一、x86(32位)平台下的汇编实现
1. cdecl
特点:
- 参数从右到左依次压栈
- 调用者清理栈
示例函数声明:
int __cdecl add(int a, int b);
调用方汇编(伪代码):
push b ; 先压入b
push a ; 再压入a
call add ; 调用函数
add esp, 8 ; 调用者清理参数(2个int,共8字节)
被调用方汇编(伪代码):
add:
mov eax, [esp+4] ; 取a
mov edx, [esp+8] ; 取b
add eax, edx
ret
2. stdcall
特点:
- 参数从右到左依次压栈
- 被调用者清理栈
示例函数声明:
int __stdcall add(int a, int b);
调用方汇编:
push b
push a
call add
; 不需要add esp, 8
被调用方汇编:
add:
mov eax, [esp+4]
mov edx, [esp+8]
add eax, edx
ret 8 ; 被调用者在ret时弹出参数
3. fastcall
特点(MSVC x86实现):
- 前两个
int
参数分别通过ECX
和EDX
寄存器传递,其余参数从右到左压栈 - 被调用者清理栈
示例函数声明:
int __fastcall add(int a, int b);
调用方汇编:
mov ecx, a
mov edx, b
call add
; 没有参数压栈,也不需要清理栈
被调用方汇编:
add:
add ecx, edx
mov eax, ecx
ret
如果有第三个参数,第三个及以后的参数依然从右到左压栈,返回时ret
弹出相应字节。
二、不同平台的调用约定差异
1. x86_64(64位)
- Windows x64:前4个整数参数通过
RCX, RDX, R8, R9
传递,返回值用RAX
,其余参数压栈。栈由调用者清理。没有cdecl/stdcall/fastcall之分,统一为“Microsoft x64 calling convention”。 - Linux x86_64(System V ABI):前6个整数参数通过
RDI, RSI, RDX, RCX, R8, R9
传递,返回值用RAX
,其余参数压栈。栈由调用者清理。
注意:64位下,cdecl/stdcall/fastcall等传统约定已被统一,编译器一般不再区分。
2. ARM
- ARM EABI(嵌入式应用二进制接口):前4个参数通过
R0-R3
寄存器传递,返回值用R0
,其余参数压栈。栈由调用者清理。
3. 其他平台
- MIPS、PowerPC等:也有各自的ABI,参数传递和栈清理规则不同,但大体思路类似。
三、调用约定的实际影响
- 函数调用的二进制兼容性:不同调用约定的函数不能直接互调,否则会导致栈错乱、程序崩溃。
- 与汇编代码交互:如果C函数要被汇编调用,或汇编实现的函数要被C调用,必须保证调用约定一致。
- 跨平台移植:移植时要注意目标平台的ABI,不能简单照搬x86的调用约定。
四、总结表格
平台 | 参数传递方式 | 栈清理者 | 备注 |
---|---|---|---|
x86 cdecl | 右到左压栈 | 调用者 | 支持可变参数 |
x86 stdcall | 右到左压栈 | 被调用者 | Windows API常用 |
x86 fastcall | ECX/EDX寄存器+右到左压栈 | 被调用者 | MSVC实现,GCC略有不同 |
x86_64 Win | RCX, RDX, R8, R9+压栈 | 调用者 | 统一调用约定 |
x86_64 Lin | RDI, RSI, RDX, RCX, R8, R9+压栈 | 调用者 | System V ABI |
ARM | R0-R3寄存器+压栈 | 调用者 | EABI |