原文链接:http://blog.csdn.net/goodname008/archive/2004/07/24/50662.aspx
原文作者:卢培培(goodname008)
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;
|