完全总结__cdecl __fastcall, __stdcall,__thiscall

来源:互联网 发布:淘宝bape复刻哪家好 编辑:程序博客网 时间:2024/06/05 20:01

__cdecl __fastcall, __stdcall,__thiscall它们都是调用约定(Calling convention),它决定以下内容:

1)函数参数的压栈顺序

2)由调用者还是被调用者把参数弹出栈
3)产生函数修饰名的方法


首先,为什么要有这么多种调用约定?函数调用时需要用到栈。当函数调用完成后,栈需要清除,这里就是问题的关键,如何清除?如果我们的函数使用了_cdecl,那么栈的清除工作是由调用者,这样带来了一个棘手的问题,不同的编译器产生栈的方式不尽相同,那么调用者能否正常的完成清除工作呢?答案是不能。如果使用__stdcall,上面的问题就解决了,函数自己解决清除工作。所以,在跨(开发) 平台的调用中,我们都使用__stdcall(虽然有时是以 WINAPI的样子出现)。那么为什么还需要_cdecl呢?当我们遇到这样的函数如fprintf()它的参数是可变的,不定长的,被调用者事先无法知道参数的长度,事后的清除工作也无法正常的进行,因此,这种情况我们只能使用_cdecl。
有了上面的基本认识,我们再仔细看看这几种调用方式:


_cdecl 是CDeclaration的缩写,表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。被调用函数不需要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。带有可变参数的函数必须且只能使用_cdecl方式。


  _stdcall 是StandardCall的缩写,是C++的标准调用方式(Windows的API函数也是这种调用方式):所有参数从右到左依次入栈,这些堆栈中的参数由被调用的函数在返回后清除,使用的指令是 retnX,X表示参数占用的字节数,CPU在ret之后自动弹出X个字节的堆栈空间。称为自动清栈。函数在编译的时候就必须确定参数个数,并且调用者必须严格的控制参数的生成,不能多,不能少,否则返回后会出错。


__fastcall调用约定是"人"如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字 (DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈,在函数名修饰约定方面,它和前两者均不同。


__thiscall仅仅应用于C++成员函数。this指针存放于CX寄存器,参数从右到左压。_thiscall不是关键词,因此不能被程序员指定。_thiscall是为了解决类成员调用this指针传递而规定的。_thiscall要求把this指针放在特定寄存器中,该寄存器由编译器决定。VC使用ecx,Borland的C++编译器使用eax。返回方式和_stdcall相当。


必须强调一点,C语言或者C++并没有规定参数压栈的顺序之类的,这都是编译器干的事情。从右向左压栈的最好的一个例子就是:

int main(void){int a = 0;printf("%d,%d",a,a++);return 0;}
两个打印的结果为1,0。
说了这么多似乎没见到有从左向右参数压栈的调用方式啊?听说Delphi默认调用方式从左到右参数压栈的。


对于函数修饰名,C语言编译器和C++编译器还是有区别的,我使用的是VS2010:
C++的函数名修饰规则有些复杂,但是信息更充分,通过分析修饰名不仅能够知道函数的调用方式,返回值类型,参数个数甚至参数类型。不管__cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和按照参数类型代号拼出的参数表。对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”,对于__fastcall方式则是“@@YI”。参数表的拼写代号如下所示:
X--void    
D--char    
E--unsigned char    
F--short    
H--int    
I--unsigned int    
J--long    
K--unsigned long(DWORD) 
M--float    
N--double    
_N--bool
U--struct
....
指针的方式有些特别,用PA表示指针,用PB表示const类型的指针。后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复。U表示结构类型,通常后跟结构体的类型名,用“@@”表示结构类型名的结束。函数的返回值不作特殊处理,它的描述方式和函数参数一样,紧跟着参数表的开始标志,也就是说,函数参数表的第一项实际上是表示函数的返回值类型。参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。下面举两个例子,假如有以下函数声明:
int Function1 (char *var1,unsigned long);
其函数修饰名为“?Function1@@YGHPADK@Z”,而对于函数声明:
void Function2();
 其函数修饰名则为“?Function2@@YGXXZ” 。


对于C++的类成员函数(其调用方式是thiscall),函数的名字修饰与非成员的C++函数稍有不同,首先就是在函数名字和参数表之间插入以“@”字符引导的类名;其次是参数表的开始标识不同,公有(public)成员函数的标识是“@@QAE”,保护(protected)成员函数的标识是“@@IAE”,私有(private)成员函数的标识是“@@AAE”,如果函数声明使用了const关键字,则相应的标识应分别为“@@QBE”,“@@IBE”和“@@ABE”。如果参数类型是类实例的引用,则使用“AAV1”,对于const类型的引用,则使用“ABV1”。下面就以类Test为例说明C++成员函数的名字修饰规则:

class Test{private:int func1();protected:void func2(char*)const;public:void func3(const Test& t);void useFunc1(){func1();}void useFunc2(){func2("aaa");}};
func3被修饰为:?func3@Test@@QAEXABV1@@Z
func1被修饰为:?func1@Test@@AAEHXZ
func2被修饰为:?func2@Test@@IBEXPAD@Z


我们如何查看函数的被修饰的名字,其实有一种很无赖的方法:你声明一个函数,而不去定义它。然后在程序中调用这个函数,链接器就会报错无法解析的外部符号……这里面就可以看到。