一、函数修饰关键字的作用
- 确定函数名,便于链接时链接到正确函数地址。
- 确定函数调用栈的汇编代码。
二、详细说明
1. 先声明未定义和实现函数
//声明未实现的C++函数
void CppDefault(const char* pMsg);
void __cdecl CppCdecl(const char* pMsg);
void __stdcall CppStdcall(const char* pMsg);
void __fastcall CppFastcall(const char* pMsg);
void __declspec(dllexport) CppDllexport(const char* pMsg);
void __declspec(dllimport) CppDllimport(const char* pMsg);
//声明带 extern "C" 的未实现函数
extern "C" {
void CDefault(const char* pMsg);
void __cdecl CCdecl(const char* pMsg);
void __stdcall CStdcall(const char* pMsg);
void __fastcall CFastcall(const char* pMsg);
void __declspec(dllexport) CDllexport(const char* pMsg);
void __declspec(dllimport) CDllimport(const char* pMsg);
}
2. 直接运行查看VS报错,从报错中确认生成的函数名
3. 函数名:确认不同关键字对其影响
- 所有不带 extern “C” 关键字均以C++的规范生成函数名,以 ? 开头,且函数名通过@带有参数和其他信息,这是C++支持重载的本质原因。
- 带 extern “C” 关键字时,__cdecl修饰的函数以下划线开头,__stdcall修饰的函数以下划线开头 且 结尾用@带上参数的大小,__fastcall修饰的函数以@开头 且 结尾用@带上参数的大小。
- C++中默认调用约定为:__cdecl,即:缺省调用约定为__cdecl。
- __declspec(dllexport) 只影响DLL函数的导出名,不影响DLL内部调用的名字。从 __declspec(dllimport) 可以看出,DLL导出函数时会在函数名前加上 __imp 且 __imp 后面的下划线数量受程序位数影响,如:Win32为两个,X64为一个。但不带extern "C"关键字时,固定为一个,因为可通过C++函数命名规则中的@直接带上程序的位数信息。
3. 函数栈:确认不同关键字对其影响
void __cdecl TestCdecl(int a, int b, int c, int d)
{
std::cout << a << " " << b << " " << c << " " << d << std::endl;
}
void __stdcall TestStdcall(int a, int b, int c, int d)
{
std::cout << a << " " << b << " " << c << " " << d << std::endl;
}
void __fastcall TestFastcall(int a, int b, int c, int d)
{
std::cout << a << " " << b << " " << c << " " << d << std::endl;
}
上面定义了三种不同调用约定的函数,进行函数调用,查看其被调用处的汇编代码,如下:
57: TestCdecl(1, 2, 3, 4);
00EC638E 6A 04 push 4
00EC6390 6A 03 push 3
00EC6392 6A 02 push 2
00EC6394 6A 01 push 1
00EC6396 E8 D0 AF FF FF call TestCdecl (0EC136Bh)
00EC639B 83 C4 10 add esp,10h
58: TestStdcall(1, 2, 3, 4);
00EC639E 6A 04 push 4
00EC63A0 6A 03 push 3
00EC63A2 6A 02 push 2
00EC63A4 6A 01 push 1
00EC63A6 E8 7D AC FF FF call TestStdcall (0EC1028h)
59: TestFastcall(1, 2, 3, 4);
00EC63AB 6A 04 push 4
00EC63AD 6A 03 push 3
00EC63AF BA 02 00 00 00 mov edx,2
00EC63B4 B9 01 00 00 00 mov ecx,1
00EC63B9 E8 64 AD FF FF call TestFastcall (0EC1122h)
从上面的汇编代码,对__cdecl、__stdcall和__fastcall进行对比,可以看出:
- 三种调用方式的参数压栈顺序都是从右往左。
- __cdecl对比另外两种,其在调用完函数后,会修改栈顶寄存器esp,本质是释放参数压栈所占用的栈空间,另外两个在调用完成后没有释放,是因为其函数内部在返回前,已经通过执行ret n指令(n为形参的占用空间)完成了释放,无需重复释放。
- __cdecl由 调用者 释放形参的栈空间,其好处是可以支持函数的变长参数,因为不同调用地方的释放代码可不一样,进而可完成不同形参空间的释放,但其缺点是每个调用的地方都会多一行释放栈空间的代码,会增加程序大小。另外两个由于释放形参栈空间的代码在其函数内部,无法完成不同数量形参的不同大小的释放,故不支持变长参数的函数,但好处是程序的大小会相对较小。
- __fastcall之所以快速,是因为其利用ecx和edx两个寄存器来存储前两个形参,省去了前两个参数的压栈操作,故被称为快速调用。注意:利用寄存器传形参的思想已经被大量应用于64位程序。