Visual C/C++ 的编译器提供了几种函数调用约定,了解这些函数调用约定的含义及它们之间的区别可以帮助我们更好地调试程序。在这篇文章里,我就和大家共同探讨一些关于函数调用约定的内容。
Visual C/C++ 的编译器支持如下的函数调用约定:
关键字 |
清理堆栈 |
参数入栈顺序 |
函数名称修饰(C) |
__cdecl |
调用函数 |
右 à 左 |
_函数名 |
__stdcall |
被调用函数 |
右 à 左 |
_函数名@数字 |
__fastcall |
被调用函数 |
右 à 左 |
@函数名@数字 |
thiscall(非关键字) |
被调用函数 |
右 à 左 |
/ |
上面这张表只简单地列出了每种函数调用约定的特点,既然这篇文章题目的前两个字是“剖析”,哪能这么容易就完事!?下面就对上面这四种函数调用约定逐个“剖析”:
一、__cdecl函数调用约定
这是C和C++ 程序默认的函数调用约定,参数按从右到左的顺序压入堆栈,由调用函数负责清理堆栈,把参数弹出栈。也正是因为用来传送参数的堆栈是由调用函数维护的,所以实现可变参数的函数只能使用这种函数调用约定。因为每一个调用它的函数都要包含清理堆栈的代码,所以编译后的可执行文件的大小要比调用__stdcall函数的大。使用这种函数调用约定时,修饰后的函数名只是在原函数名前加上了一个_(下划线),并且不改变函数的大小写。对于__cdecl,我们一般不特别指出,因为它是C和C++ 程序默认的函数调用约定,所以只有将编译选项设置成/Gz(stdcall)或/Gr(fastcall)时,我们才有必要在函数名前显式地指出采用这种函数调用约定。下面举一个例子:
int __cdecl Sumcdecl(int a, int b, int c) { int i = 1000; short j = 2000; int k = 3000; int rEBP = 0; int value = 0;
// ...
return (a + b + c);
}
调用:Sumcdecl(10, 20, 30);
|
函数体及调用语句如上所示,修饰后的函数名为_Sumcdecl,堆栈和寄存器状态如下(一行表示4个字节):
0 |
value |
0 |
rEBP |
3000 |
k |
2000 |
j |
1000 |
i |
|
<---------EBP |
|
|
10 |
a |
20 |
b |
30 |
c |
|
|
[未使用] |
ECX |
[未使用] |
EDX |
口说无凭,代码能说明一切,下面的程序乃Win32 console application(.exe)是也:
#include "iostream.h" #include "stdio.h"
extern "C" __declspec(dllexport) int __cdecl Sumcdecl(int a, int b, int c) { // 声明局部变量 int i = 1000; short j = 2000; int k = 3000; int rEBP = 0; int value = 0;
// 显示局部变量的地址 cout << "局部变量的地址:" << endl; cout << &value << " <-----------value" << endl; cout << &rEBP << " <-----------rEBP" << endl; cout << &k << " <-----------k" << endl; cout << &j << " <-----------j" << endl; cout << &i << " <-----------i" << endl;
// 显示寄存器的值 cout << "寄存器:" << endl; __asm mov rEBP, ebp; printf("0x%08X <-----------EBP/n", rEBP);
// 显示函数参数的地址 cout << "函数参数的地址:" << endl; cout << &a << " <-----------a" << endl; cout << &b << " <-----------b" << endl; cout << &c << " <-----------c" << endl;
// 通过 EBP 寄存器获得堆栈中的数据并显示 cout << "通过EBP获取堆栈中的数据:" << endl; __asm mov eax, [ebp - 4]; __asm mov value, eax; cout << "i: " << value << endl;
__asm mov eax, [ebp - 8]; __asm mov value, eax; cout << "j: " << (short)value << endl;
__asm mov eax, [ebp - 12]; __asm mov value, eax; cout << "k: " << value << endl;
__asm mov eax, [ebp + 8]; __asm mov value, eax; cout << "a: " << value << endl;
__asm mov eax, [ebp + 12]; __asm mov value, eax; cout << "b: " << value << endl;
__asm mov eax, [ebp + 16]; __asm mov value, eax; cout << "c: " << value << endl;
// 返回 return (a + b + c);
}
// 主函数 int main(int argc, char* argv[]) {
Sumcdecl(10, 20, 30);
return 0;
}
|
在我的机器上,运行结果如下:
局部变量的地址: 0x0012FF0C <-----------value 0x0012FF10 <-----------rEBP 0x0012FF14 <-----------k 0x0012FF18 <-----------j 0x0012FF1C <-----------i 寄存器: 0x0012FF20 <-----------EBP 函数参数的地址: 0x0012FF28 <-----------a 0x0012FF2C <-----------b 0x0012FF30 <-----------c 通过EBP获取堆栈中的数据: i: 1000 j: 2000 k: 3000 a: 10 b: 20 c: 30 |
函数声明部分的extern “C”表示连接规范(Linkage Specification)采用C,而不是C++,不加extern “C”的情况我会在后面统一讨论。__declspec(dllexport)表示将该函数导出,将生成.lib文件,以便我们验证函数名是怎样修饰的。关于修饰后的函数名,我们可以使用VC98/bin目录下的dumpbin工具来验证:
dumpbin /exports 文件名>
输出结果如下:
File Type: LIBRARY
Exports
ordinal name
_Sumcdecl
Summary
C9 .debug$S 14 .idata$2 14 .idata$3 4 .idata$4 4 .idata$5 E .idata$6 |
二、__stdcall函数调用约定
__stdcall函数调用约定通常用于Win32 API函数,参数按从右到左的顺序压入堆栈,由被调用函数负责清理堆栈,把参数弹出栈。在windows.h中包含了windef.h,而windef.h中定义了一个WINAPI宏:#define WINAPI __stdcall,呵呵,应该心知肚明了。使用这种函数调用约定时,修饰后的函数名在原函数名前加上了一个_(下划线),并且在原函数名后加上“@数字”,当然也不改变函数的大小写,@ 后面的数字表示参数所占的字节数,这里有一点要注意的,不足32位(4字节)的参数将在参数传递时被扩充到32位。下面举一个例子:
int __stdcall Sumstdcall(int a, int b, int c) { int i = 1000; short j = 2000; int k = 3000; int rEBP = 0; int value = 0;
// ...
return (a + b + c);
}
调用:Sumstdcall(10, 20, 30);
|
函数体及调用语句如上所示,修饰后的函数名为_Sumstdcall@12,int是32位的,占4个字节,3个32位的变量,共12个字节。堆栈和寄存器状态如下(一行表示4个字节):
0 |
value |
0 |
rEBP |
3000 |
k |
2000 |
j |
1000 |
i |
|
<---------EBP |
|
|
10 |
a |
20 |
b |
30 |
c |
|
|
[未使用] |
ECX |
[未使用] |
EDX |
仍然以代码说明:
#include "iostream.h" #include "stdio.h"
extern "C" __declspec(dllexport) int __stdcall Sumstdcall(int a, int b, int c) { // 声明局部变量 int i = 1000; short j = 2000; int k = 3000; int rEBP = 0; int value = 0;
// 显示局部变量的地址 cout << "局部变量的地址:" << endl; cout << &value << " <-----------value" << endl; cout << &rEBP << " <-----------rEBP" << endl; cout << &k << " <-----------k" << endl; cout << &j << " <-----------j" << endl; cout << &i << " <-----------i" << endl;
// 显示寄存器的值 cout << "寄存器:" << endl; __asm mov rEBP, ebp; printf("0x%08X <-----------EBP/n", rEBP);
// 显示函数参数的地址 cout << "函数参数的地址:" << endl; cout << &a << " <-----------a" << endl; cout << &b << " <-----------b" << endl; cout << &c << " <-----------c" << endl;
// 通过 EBP 寄存器获得堆栈中的数据并显示 cout << "通过EBP获取堆栈中的数据:" << endl; __asm mov eax, [ebp - 4]; __asm mov value, eax; cout << "i: " << value << endl;
__asm mov eax, [ebp - 8]; __asm mov value, eax; cout << "j: " << (short)value << endl;
__asm mov eax, [ebp - 12]; __asm mov value, eax; cout << "k: " << value << endl;
__asm mov eax, [ebp + 8]; __asm mov value, eax; cout << "a: " << value << endl;
__asm mov eax, [ebp + 12]; __asm mov value, eax; cout << "b: " << value << endl;
__asm mov eax, [ebp + 16]; __asm mov value, eax; cout << "c: " << value << endl;
// 返回 return (a + b + c);
}
// 主函数 int main(int argc, char* argv[]) {
Sumstdcall(10, 20, 30);
return 0;
}
|
在我的机器上,运行结果如下:
局部变量的地址: 0x0012FF0C <-----------value 0x0012FF10 <-----------rEBP 0x0012FF14 <-----------k 0x0012FF18 <-----------j 0x0012FF1C <-----------i 寄存器: 0x0012FF20 <-----------EBP 函数参数的地址: 0x0012FF28 <-----------a 0x0012FF2C <-----------b 0x0012FF30 <-----------c 通过EBP获取堆栈中的数据: i: 1000 j: 2000 k: 3000 a: 10 b: 20 c: 30 |
其实和__cdecl的差不多,只是把__cdecl改成了__stdcall,又换了个函数名。用dumpbin分析.lib文件的结果如下:
File Type: LIBRARY
Exports
ordinal name
_Sumstdcall@12
Summary
C9 .debug$S 14 .idata$2 14 .idata$3 4 .idata$4 4 .idata$5 E .idata$6 |
三、__fastcall函数调用约定
__fastcall,顾名思义,特点就是快,因为它是靠寄存器来传递参数的。传递参数时,最左边的两个小于等于32位(4字节)的参数将被分别存入ECX和EDX寄存器,其余参数仍然按从右到左的顺序压入堆栈,由被调用函数负责清理堆栈,把参数弹出栈。这里有一点想强调一下:存入寄存器的那两个参数实际也存入到了堆栈中,后面的例子和代码将证明这一点。使用这种函数调用约定时,修饰后的函数名在原函数名前加上了一个 @,并且在原函数名后加上“@数字”,同样不改变函数的大小写,@ 后面的数字表示参数所占的字节数,其实和__stdcall差不多,只是把最前面的_(下划线)换成了@。下面举一个例子,和前面两个稍有不同:
int __fastcall Sumfastcall(int a, double x, int b, int c) { int i = 1000; short j = 2000; int k = 3000; int rEBP = 0; int rECX = 0; int rEDX = 0; int value = 0;
// ...
return (a + b + c);
}
调用:Sumfastcall(10, 8.8, 20, 30);
|
函数体及调用语句如上所示,修饰后的函数名为@Sumfastcall@20,int是32位的,占4个字节,double是64位的,占8个字节,3个32位的变量加1个64位的变量,共20个字节。堆栈和寄存器状态如下(一行表示4个字节):
0 |
value |
|
0 |
rEDX |
|
0 |
rECX |
|
0 |
rEBP |
|
3000 |
k |
|
2000 |
j |
|
1000 |
i |
|
20 |
b |
|
10 |
a |
|
|
<---------EBP |
|
|
|
|
8.8 |
x(8个字节) |
|
30 |
c |
|
|
|
|
10 |
ECX |
|
20 |
EDX |
由于__fastcall和前面两个函数调用约定不太一样,局部变量、函数参数在堆栈中的存放情况和寄存器(主要是ECX和EDX)中的值都有了变化,这些我们都要验证,因此代码也不一样,但大体相同,下面就将它们请出来:
#include "iostream.h" #include "stdio.h"
extern "C" __declspec(dllexport) int __fastcall Sumfastcall(int a, double x, int b, int c) { // 声明局部变量 int i = 1000; short j = 2000; int k = 3000; int rEBP = 0; int rECX = 0; int rEDX = 0; int value = 0;
// 显示 ECX 和 EDX 寄存器的值 __asm mov rECX, ecx; __asm mov rEDX, edx; cout << "ECX 和 EDX 寄存器的值:" << endl; cout << "ECX: " << rECX << endl; cout << "EDX: " << rEDX << endl;
// 显示局部变量的地址 cout << "局部变量的地址:" << endl; cout << &value << " <-----------value" << endl; cout << &rEDX << " <-----------rEDX" << endl; cout << &rECX << " <-----------rECX" << endl; cout << &rEBP << " <-----------rEBP" << endl; cout << &k << " <-----------k" << endl; cout << &j << " <-----------j" << endl; cout << &i << " <-----------i" << endl;
// 显示存入寄存器的参数的地址, 变量虽然存入了寄存器, 但也在堆栈中 cout << "显示存入寄存器的参数的地址:" << endl; cout << &b << " <-----------b" << endl; cout << &a << " <-----------a" << endl;
// 显示寄存器的值 cout << "寄存器:" << endl; __asm mov rEBP, ebp; printf("0x%08X <-----------EBP/n", rEBP);
// 显示函数参数的地址 cout << "函数参数的地址:" << endl; cout << &x << " <-----------x" << endl; cout << &c << " <-----------c" << endl;
// 通过 EBP 寄存器获得堆栈中的数据并显示 cout << "通过EBP获取堆栈中的数据:" << endl; __asm mov eax, [ebp - 12]; __asm mov value, eax; cout << "i: " << value << endl;
__asm mov eax, [ebp - 16]; __asm mov value, eax; cout << "j: " << (short)value << endl;
__asm mov eax, [ebp - 20]; __asm mov value, eax; cout << "k: " << value << endl;
__asm mov eax, [ebp - 4]; __asm mov value, eax; cout << "a: " << value << endl;
__asm mov eax, [ebp - 8]; __asm mov value, eax; cout << "b: " << value << endl;
__asm mov eax, [ebp + 16]; __asm mov value, eax; cout << "c: " << value << endl;
// 返回 return (a + b + c);
}
// 主函数 int main(int argc, char* argv[]) {
Sumfastcall(10, 8.8, 20, 30);
return 0;
}
|
在我的机器上,运行结果如下:
ECX 和 EDX 寄存器的值: ECX: 10 EDX: 20 局部变量的地址: 0x0012FEFC <-----------value 0x0012FF00 <-----------rEDX 0x0012FF04 <-----------rECX 0x0012FF08 <-----------rEBP 0x0012FF0C <-----------k 0x0012FF10 <-----------j 0x0012FF14 <-----------i 显示存入寄存器的参数的地址: 0x0012FF18 <-----------b 0x0012FF1C <-----------a 寄存器: 0x0012FF20 <-----------EBP 函数参数的地址: 0x0012FF28 <-----------x 0x0012FF30 <-----------c 通过EBP获取堆栈中的数据: i: 1000 j: 2000 k: 3000 a: 10 b: 20 c: 30 |
同样使用dumpbin /exports后结果如下:
File Type: LIBRARY
Exports
ordinal name
@Sumfastcall@20
Summary
C9 .debug$S 14 .idata$2 14 .idata$3 4 .idata$4 4 .idata$5 E .idata$6 |
四、thiscall函数调用约定
咦?thiscall前面怎么没有下划线,呵呵。事实上,thiscall并不是C++ 的关键字,所以我们不能在程序中显式地指出采用这种函数调用约定。可能有人会问了,那这东西到底怎么用?其实,我们经常都在用,因为它是C++ 成员函数默认的函数调用约定,参数按从右到左的顺序压入堆栈,由被调用函数负责清理堆栈,把参数弹出栈。在秘密传递this指针时,成员函数不仅将this指针存入到了ECX寄存器中,而且也存入到了堆栈中,但却是最后压入到堆栈,位置和采用__fastcall调用约定的函数的最左边的两个小于等于32位(4字节)的参数的位置相同。下面举一个例子:
class Test { public: int Sumthiscall(int a, int b, int c) { int i = 1000; short j = 2000; int k = 3000; int rEBP = 0; int value = 0;
// ...
return (a + b + c);
}
};
调用: Test test; test.Sumthiscall(10, 20, 30);
|
成员函数的定义及调用语句如上所示,堆栈和寄存器状态如下(一行表示4个字节):
0 |
value |
0 |
rEBP |
3000 |
k |
2000 |
j |
1000 |
i |
对象地址 |
this |
|
<---------EBP |
|
|
10 |
a |
20 |
b |
30 |
c |
|
|
对象地址 |
ECX |
[未使用] |
EDX |
下面我们还是让代码说话,我最相信它了:
#include "iostream.h" #include "stdio.h"
class Test { public: int Sumthiscall(int a, int b, int c) { // 声明局部变量 int i = 1000; short j = 2000; int k = 3000; int rEBP = 0; int value = 0;
// 通过 ECX 寄存器获得 this 指针 __asm mov value, ecx; printf("通过ECX 寄存器获得 this 指针: 0x%08X/n", value);
// 直接输出 this 所指对象的地址 printf("直接输出 this 所指对象的地址: 0x%08X/n", this);
// 显示局部变量的地址 cout << "局部变量的地址:" << endl; cout << &value << " <-----------value" << endl; cout << &rEBP << " <-----------rEBP" << endl; cout << &k << " <-----------k" << endl; cout << &j << " <-----------j" << endl; cout << &i << " <-----------i" << endl;
// 显示寄存器的值 cout << "寄存器:" << endl; __asm mov rEBP, ebp; printf("0x%08X <-----------EBP/n", rEBP);
// 显示函数参数的地址 cout << "函数参数的地址:" << endl; cout << &a << " <-----------a" << endl; cout << &b << " <-----------b" << endl; cout << &c << " <-----------c" << endl;
// 通过 EBP 寄存器获得堆栈中的数据并显示 cout << "通过EBP获取堆栈中的数据:" << endl; __asm mov eax, [ebp - 4]; __asm mov value, eax; printf("this: 0x%08X/n", value);
__asm mov eax, [ebp - 8]; __asm mov value, eax; cout << "i: " << value << endl;
__asm mov eax, [ebp - 12]; __asm mov value, eax; cout << "j: " << (short)value << endl;
__asm mov eax, [ebp - 16]; __asm mov value, eax; cout << "k: " << value << endl;
__asm mov eax, [ebp + 8]; __asm mov value, eax; cout << "a: " << value << endl;
__asm mov eax, [ebp + 12]; __asm mov value, eax; cout << "b: " << value << endl;
__asm mov eax, [ebp + 16]; __asm mov value, eax; cout << "c: " << value << endl;
// 返回 return (a + b + c);
}
};
// 主函数 int main(int argc, char* argv[]) { Test test; test.Sumthiscall(10, 20, 30);
return 0;
} |
在我的机器上,运行结果如下:
通过ECX 寄存器获得 this 指针: 0x0012FF7C 直接输出 this 所指对象的地址: 0x0012FF7C 局部变量的地址: 0x0012FF04 <-----------value 0x0012FF08 <-----------rEBP 0x0012FF0C <-----------k 0x0012FF10 <-----------j 0x0012FF14 <-----------i 寄存器: 0x0012FF1C <-----------EBP 函数参数的地址: 0x0012FF24 <-----------a 0x0012FF28 <-----------b 0x0012FF2C <-----------c 通过EBP获取堆栈中的数据: this: 0x0012FF7C i: 1000 j: 2000 k: 3000 a: 10 b: 20 c: 30 |
代码中没有通过输出this指针变量所在的地址(存在&this吗?)来验证它是否被存入到堆栈中,但却通过EBP寄存器正确获得了this所指对象的地址,和ECX寄存器中的值一样。因为thiscall函数调用约定只应用于C++ 类的成员函数,所以不存在C语言函数名称修饰机制,因此我没有讨论它,也没有将成员函数导出。
Sumcdecl、Sumstdcall和Sumfastcall三个函数前都有个extern “C”,表示连接规范(Linkage Specification)采用C,而不是C++,如果不写的话默认采用C++,当然也可以写成extern “C++”。把这3个函数放到一个文件中,并去掉每个函数前面的extern “C”,编译后用dumpbin分析.lib文件,结果如下(我只取了关键部分):
?Sumcdecl@@YAHHHH@Z (int __cdecl Sumcdecl(int,int,int)) ?Sumfastcall@@YIHHNHH@Z (int __fastcall Sumfastcall(int,double,int,int)) ?Sumstdcall@@YGHHHH@Z (int __stdcall Sumstdcall(int,int,int)) |
哇!?这都是些什么东西啊,看上去好像有点乱,且听在下慢慢道来:
1、每个函数都是以“?”开头,接着是函数名,不改变大小写。
2、对于__cdecl,函数名后接@@YA;对于__stdcall,函数名后接@@YG;对于__fastcall,函数后接@@YI。
3、再后面接着的是函数返回值类型的代号和参数类型的代号,规则如下:
代 号 |
类 型 |
X |
void |
D |
char |
E |
unsigned char |
F |
short |
H |
int |
I |
unsigned int |
J |
long |
K |
unsigned long |
M |
float |
N |
double |
_N |
bool |
O |
long double |
PA |
指针前缀 |
AA |
引用前缀 |
V类名@@ |
类 |
如果某个参数是指针,则在类型代号前加上PA;如果是引用,则在类型代号前加上AA。如果相同类型的指针连续出现,则以“0”代替,每个“0”都代表一次重复;如果相同类型的引用连续出现,则以“1”代替,每个“1”都代表一次重复。
4、代号列表后接@Z或Z来标识整个函数名的结束:如果该函数有参数,则以@Z标识函数名结束;如果该函数没有参数,则以Z标识函数名结束。
5、函数名结束标志后接一个空格,空格后是带括号的函数原型。在生成的最终文件(.exe或.dll)中,函数名结束标志后面将不会有函数原型。
举个简单的例子,假设函数原型如下(Test为自定义类):
void abc(int a, long b, char* c, char* d, bool &e, Test f, short g)
那么,修饰后的函数名为:
?abc@@YAXHJPAD0AA_NVTest@@F@Z
其实,我们可以在VC++ 6.0的IDE环境中设置当前工程采用的默认函数调用约定。在主界面下按 Alt + F7 打开【Project Settings】对话框,选择【C/C++】选项卡,然后在【Category】下拉列表框中选择“Code Gernation”,就可以在【Call convetion】下拉列表框中选择函数调用约定了。至于命令行开关,/Gd表示__cdecl,/Gr表示__fastcall,/Gz表示__fastcall。
最后还有一个小插曲,和这篇文章的主题没太大关系,但细心的人一定会发现这个问题。上面提到的所有函数(包括Test类的成员函数),局部变量j都是short型的,应该只占2个字节,但从内存分配情况来看却占了4个字节,为什么呢?因为内存分配的最小单位是4个字节。不信可以char s[10],再使用和上面类似的方法分析一下,我们都认为占10个字节天经地义,但事实上,有12个字节是分给它的。
好了,本次旅途到此结束,希望您旅途愉快。最后再送上一个小礼物:我把上面的Test类和其余3个函数放到了一个文件中,将所有例子综合到了一起:
http://csdngoodname008.51.net/CallTest.zip
*-------------------------------------------*
* 转载请通知作者并注明出处,CSDN欢迎您! *
* 作者:卢培培(goodname008) *
* 邮箱:[email protected] *
* 专栏:http://blog.csdn.net/goodname008 *
*-------------------------------------------*