函数调用约定
文章目录
- 什么是函数调用约定?
- 历史发展时间线
- 1. 早期阶段(1970s-1980s)
- 2. cdecl的出现(1980s早期)
- 3. stdcall的出现(1980s后期)
- 4. fastcall的出现(1990s早期)
- 5. thiscall的出现(1990s中期)
- 为什么出现多种调用约定?
- 1. 技术演进驱动
- 2. 不同应用场景需求
- 3. 平台和编译器差异
- 现代发展趋势
- 1. 64位平台的统一
- 2. 新的优化约定
- 总结
- 各种函数调用约定的代码示例
- 1. cdecl 调用约定 - 调用者清理堆栈
- 2. stdcall 调用约定 - 被调用者清理堆栈
- 3. fastcall 调用约定 - 寄存器+堆栈混合
- 4. thiscall 调用约定 - C++成员函数
- 5. 完整对比演示
- 汇编代码总结表:
什么是函数调用约定?
函数调用约定是一套规则,定义了在函数调用过程中:
- 参数如何传递(通过堆栈还是寄存器)
- 参数压栈顺序(从左到右还是从右到左)
- 谁负责清理堆栈(调用者还是被调用者)
- 返回值如何传递
- 哪些寄存器需要保存
历史发展时间线
1. 早期阶段(1970s-1980s)
// 早期的C语言 - 没有标准化调用约定
int add(a, b, c) // K&R C风格,没有参数类型声明
int a, b, c;
{return a + b + c;
}// 调用时编译器各自实现参数传递
int result = add(1, 2, 3);
背景:
- 不同处理器架构(x86, Motorola, DEC)有不同的寄存器组
- 各编译器厂商(Microsoft, Borland, Watcom)有自己的实现
- 没有操作系统级别的标准化要求
2. cdecl的出现(1980s早期)
// cdecl - C语言的标准调用约定
int __cdecl printf(const char* format, ...); // 可变参数必须用cdeclvoid demo_cdecl_evolution() {// 早期的编译器实现// 压栈顺序不统一,有的从左到右,有的从右到左// 最终标准化为:参数从右到左压栈// 因为这样便于处理可变参数printf("Value: %d, Name: %s", 42, "test");// 汇编: push "test", push 42, push format string, call printf
}
出现原因:
- 可变参数支持:
printf
等函数需要cdecl的从右到左压栈 - 简单性:实现简单,适合早期编译器
- 灵活性:调用者知道参数数量,便于清理堆栈
3. stdcall的出现(1980s后期)
// Windows API 广泛使用stdcall
#define WINAPI __stdcall// Windows 16/32位API
int WINAPI MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);void demo_stdcall_evolution() {// 在Windows系统中,系统DLL需要稳定的接口MessageBoxA(NULL, "Hello", "Title", MB_OK);// 为什么Windows选择stdcall?// 1. 代码体积更小(每个调用少一条指令)// 2. 系统DLL被不同编译器调用,需要标准化
}
出现原因:
- 代码体积优化:减少
add esp, N
指令,节省代码空间 - 系统API标准化:Windows需要统一的接口规范
- 安全性:被调用者清理堆栈,减少调用错误
4. fastcall的出现(1990s早期)
// fastcall - 性能优化导向
void __fastcall fast_memcpy(void* dest, void* src, int size);void demo_fastcall_evolution() {char buffer1[100], buffer2[100];// 性能敏感的操作使用fastcallfast_memcpy(buffer1, buffer2, sizeof(buffer1));// 为什么需要fastcall?// 90年代CPU速度远快于内存速度// 寄存器访问比内存访问快得多
}
出现原因:
- 性能需求:90年代应用程序对性能要求提高
- CPU架构演进:更多通用寄存器可用
- 编译器优化:编译器技术成熟,可以更好利用寄存器
5. thiscall的出现(1990s中期)
// C++的兴起带来了新的需求
class MyClass {
private:int data;
public:// 成员函数需要传递this指针void __thiscall setData(int value) {this->data = value; // 需要访问对象实例}// 静态函数不需要thiscallstatic void staticMethod() {// 没有this指针}
};void demo_thiscall_evolution() {MyClass obj;obj.setData(100); // 编译器需要传递obj的地址// C++面向对象特性需要专门的调用约定// 1. this指针传递// 2. 与C++异常处理兼容// 3. 虚函数调用支持
}
出现原因:
- C++语言特性:需要传递this指针
- 面向对象编程:成员函数调用的特殊需求
- 编译器兼容性:不同C++编译器需要统一的对象模型
为什么出现多种调用约定?
1. 技术演进驱动
// 不同时期的技术需求
void technology_evolution() {// 1980s: 简单性优先// cdecl - 实现简单,适合各种场景// 1990s早期: 系统标准化 // stdcall - Windows系统API统一// 1990s中期: 性能优化// fastcall - 充分利用寄存器// 1990s后期: 面向对象// thiscall - C++语言支持// 2000s以后: 进一步优化// vectorcall, regcall等新约定
}
2. 不同应用场景需求
// 场景1: 系统API - 需要稳定性
typedef int (__stdcall* API_PROC)(int, int);
API_PROC pFunc = GetProcAddress("SomeSystemAPI");// 场景2: 回调函数 - 需要灵活性
typedef int (__cdecl* CALLBACK_PROC)(int, int, void*);
CALLBACK_PROC callback = my_callback;// 场景3: 性能关键代码 - 需要速度
void __fastcall performance_critical(int param1, int param2);// 场景4: C++代码 - 需要对象支持
class __declspec(dllexport) ExportedClass {void __thiscall method(); // 需要导出给其他编译器使用
};
3. 平台和编译器差异
// 不同平台的调用约定差异
void platform_differences() {
#ifdef _WIN32// Windows x86#define CDECL __cdecl#define STDCALL __stdcall#define FASTCALL __fastcall#elif defined(__linux__)// Linux x86 (gcc)#define CDECL __attribute__((cdecl))#define STDCALL __attribute__((stdcall))#define FASTCALL __attribute__((fastcall))#elif defined(__APPLE__)// macOS - 不同规则// 通常使用System V AMD64 ABI
#endif// 64位平台的统一化趋势
#ifdef _WIN64// Windows x64: 基本统一为一种调用约定// 前4个参数通过寄存器传递
#elif defined(__x86_64__)// System V ABI: 前6个参数通过寄存器传递
#endif
}
现代发展趋势
1. 64位平台的统一
// x64架构的调用约定统一
void x64_unification() {// Windows x64: 基本只有一种调用约定// 前4个整数/指针参数: RCX, RDX, R8, R9 // 前4个浮点参数: XMM0-XMM3// 剩余参数通过堆栈// Linux x64: System V AMD64 ABI// 前6个整数参数: RDI, RSI, RDX, RCX, R8, R9// 前8个浮点参数: XMM0-XMM7
}
2. 新的优化约定
// 现代调用约定进一步优化
#ifdef __AVX__
// vectorcall - 使用向量寄存器
void __vectorcall vector_optimized(float xmm0_param, // XMM0float xmm1_param, // XMM1 int ecx_param, // ECXint edx_param // EDX
);
#endif// regcall - 更多寄存器使用
int __regcall register_heavy(int a, int b, int c, int d, int e, int f
);
总结
函数调用约定的发展反映了计算机技术的演进:
- 早期多样性:各厂商自由实现,缺乏标准
- 标准化需求:操作系统和跨编译器调用需要统一
- 性能驱动:从堆栈到寄存器的优化
- 语言特性支持:C++等新语言特性需要专门支持
- 现代统一趋势:64位架构减少了调用约定的多样性
根本原因:在性能、代码大小、实现复杂度、跨平台兼容性之间的不同权衡,导致了多种调用约定的并存。每种约定都是为了解决特定时期、特定场景下的特定问题而出现的。
各种函数调用约定的代码示例
1. cdecl 调用约定 - 调用者清理堆栈
#include <iostream>// cdecl函数 - 调用者负责清理堆栈
int __cdecl cdecl_function(int a, int b, int c, int d) {return a + b + c + d;
}void demo_cdecl_assembly() {std::cout << "=== cdecl 汇编代码分析 ===" << std::endl;int result = cdecl_function(1, 2, 3, 4);std::cout << "结果: " << result << std::endl;
}// 对应的汇编代码:
/*
demo_cdecl_assembly PROC; 参数从右向左压栈push 4 ; 第四个参数push 3 ; 第三个参数 push 2 ; 第二个参数push 1 ; 第一个参数call cdecl_function ; 调用函数; !!! 调用者清理堆栈 !!!add esp, 16 ; 清理4个int参数(4*4=16字节); 使用返回值...mov DWORD PTR result[ebp], eaxret
demo_cdecl_assembly ENDPcdecl_function PROC; 函数体...mov eax, DWORD PTR a[ebp]add eax, DWORD PTR b[ebp]add eax, DWORD PTR c[ebp]add eax, DWORD PTR d[ebp]; !!! 被调用函数不清理堆栈 !!!ret ; 简单返回,不清理堆栈
cdecl_function ENDP
*/
2. stdcall 调用约定 - 被调用者清理堆栈
#include <iostream>// stdcall函数 - 被调用者负责清理堆栈
int __stdcall stdcall_function(int a, int b, int c, int d) {return a + b + c + d;
}void demo_stdcall_assembly() {std::cout << "=== stdcall 汇编代码分析 ===" << std::endl;int result = stdcall_function(1, 2, 3, 4);std::cout << "结果: " << result << std::endl;
}// 对应的汇编代码:
/*
demo_stdcall_assembly PROC; 参数从右向左压栈push 4 ; 第四个参数push 3 ; 第三个参数push 2 ; 第二个参数 push 1 ; 第一个参数call stdcall_function ; 调用函数; !!! 调用者不需要清理堆栈 !!!; 没有 add esp, xx 指令; 使用返回值...mov DWORD PTR result[ebp], eaxret
demo_stdcall_assembly ENDPstdcall_function PROC; 函数体...mov eax, DWORD PTR a[ebp]add eax, DWORD PTR b[ebp]add eax, DWORD PTR c[ebp]add eax, DWORD PTR d[ebp]; !!! 被调用者清理堆栈 !!!ret 16 ; 返回并清理16字节参数
stdcall_function ENDP
*/
3. fastcall 调用约定 - 寄存器+堆栈混合
#include <iostream>// fastcall函数 - 前两个参数通过寄存器传递
int __fastcall fastcall_function(int a, int b, int c, int d) {return a + b + c + d;
}void demo_fastcall_assembly() {std::cout << "=== fastcall 汇编代码分析 ===" << std::endl;int result = fastcall_function(1, 2, 3, 4);std::cout << "结果: " << result << std::endl;
}// 对应的汇编代码:
/*
demo_fastcall_assembly PROC; 前两个参数通过寄存器传递; 剩余参数从右向左压栈push 4 ; 第四个参数push 3 ; 第三个参数mov edx, 2 ; 第二个参数通过EDXmov ecx, 1 ; 第一个参数通过ECXcall fastcall_function ; 调用函数; !!! 调用者不需要清理堆栈 !!!; 但需要清理通过堆栈传递的参数; 使用返回值...mov DWORD PTR result[ebp], eaxret
demo_fastcall_assembly ENDPfastcall_function PROC; 保存寄存器参数到堆栈mov DWORD PTR [ebp-8], ecx ; 保存amov DWORD PTR [ebp-12], edx ; 保存b; 函数体...mov eax, DWORD PTR [ebp-8] ; aadd eax, DWORD PTR [ebp-12] ; +badd eax, DWORD PTR [ebp+8] ; +c add eax, DWORD PTR [ebp+12] ; +d; !!! 被调用者清理堆栈 !!!ret 8 ; 返回并清理8字节(2个堆栈参数)
fastcall_function ENDP
*/
4. thiscall 调用约定 - C++成员函数
#include <iostream>class MyClass {
private:int value;
public:MyClass(int val) : value(val) {}// thiscall - this指针通过ECX传递int __thiscall member_function(int a, int b, int c) {return value + a + b + c;}
};void demo_thiscall_assembly() {std::cout << "=== thiscall 汇编代码分析 ===" << std::endl;MyClass obj(100);int result = obj.member_function(10, 20, 30);std::cout << "结果: " << result << std::endl;
}// 对应的汇编代码:
/*
demo_thiscall_assembly PROC; 创建对象push 100lea ecx, DWORD PTR obj[ebp]call MyClass::MyClass; 调用成员函数; this指针通过ECX传递push 30 ; 第三个参数push 20 ; 第二个参数push 10 ; 第一个参数lea ecx, DWORD PTR obj[ebp] ; this指针call MyClass::member_function; !!! 调用者清理参数堆栈 !!!add esp, 12 ; 清理3个参数mov DWORD PTR result[ebp], eaxret
demo_thiscall_assembly ENDPMyClass::member_function PROC; this指针已经在ECX中mov DWORD PTR [ebp-4], ecx ; 保存this指针; 访问成员变量mov eax, DWORD PTR [ebp-4]mov ecx, DWORD PTR [eax] ; 获取value; 计算...add ecx, DWORD PTR a[ebp] ; +aadd ecx, DWORD PTR b[ebp] ; +badd ecx, DWORD PTR c[ebp] ; +cmov eax, ecx; !!! 被调用者不清理参数堆栈 !!!; 因为参数由调用者清理ret
MyClass::member_function ENDP
*/
5. 完整对比演示
#include <iostream>// 不同调用约定的相同函数
int __cdecl add_cdecl(int a, int b, int c, int d) {std::cout << "cdecl函数调用" << std::endl;return a + b + c + d;
}int __stdcall add_stdcall(int a, int b, int c, int d) {std::cout << "stdcall函数调用" << std::endl;return a + b + c + d;
}int __fastcall add_fastcall(int a, int b, int c, int d) {std::cout << "fastcall函数调用" << std::endl;return a + b + c + d;
}void compare_calling_conventions() {std::cout << "====== 调用约定汇编对比 ======" << std::endl;int x = 1, y = 2, z = 3, w = 4;std::cout << "\n1. cdecl调用:" << std::endl;int r1 = add_cdecl(x, y, z, w);// 汇编: push w, push z, push y, push x, call, add esp,16std::cout << "\n2. stdcall调用:" << std::endl;int r2 = add_stdcall(x, y, z, w);// 汇编: push w, push z, push y, push x, call// 函数内: ret 16std::cout << "\n3. fastcall调用:" << std::endl;int r3 = add_fastcall(x, y, z, w);// 汇编: push w, push z, mov edx,y, mov ecx,x, call// 函数内: ret 8std::cout << "\n结果: cdecl=" << r1 << ", stdcall=" << r2 << ", fastcall=" << r3 << std::endl;
}// 演示堆栈平衡
void stack_balance_demo() {std::cout << "\n=== 堆栈平衡演示 ===" << std::endl;// cdecl: 调用前后堆栈指针相同// stdcall/fastcall: 调用后堆栈指针自动恢复std::cout << "cdecl: 调用者负责堆栈平衡" << std::endl;std::cout << "stdcall: 被调用者负责堆栈平衡" << std::endl;std::cout << "fastcall: 被调用者负责堆栈平衡" << std::endl;
}int main() {demo_cdecl_assembly();demo_stdcall_assembly();demo_fastcall_assembly();demo_thiscall_assembly();compare_calling_conventions();stack_balance_demo();return 0;
}
汇编代码总结表:
调用约定 | 调用者汇编 | 被调用者返回 | 堆栈清理 |
---|---|---|---|
cdecl | call func add esp, N | ret | 调用者 |
stdcall | call func | ret N | 被调用者 |
fastcall | mov reg, param push remaining call func | ret M | 被调用者 |
thiscall | mov ecx, this push params call func add esp, N | ret | 调用者 |
关键观察点:
- cdecl: 调用后有
add esp, N
指令 - stdcall: 被调用函数使用
ret N
- fastcall: 使用寄存器传递参数,
ret M
清理堆栈参数 - thiscall: this指针通过ECX,参数清理类似cdecl
这些汇编代码清楚地展示了不同调用约定在堆栈管理和参数传递上的根本差异。