TAG: 调用惯例, cdecl, stdcall, fastcall, thiscall
DATE: 2013-08-06
什么是调用惯例
调用惯例(Calling Conventions)指计算机程序执行时调用函数或过程的一些约定,包括:
- 函数的参数是通过栈还是寄存器传递?
- 如果通过栈传递,顺序是怎样的,是从左至右入栈还是相反。
- 谁负责清理栈,是调用者还是被调用者?
从清理栈的角度来讲,调用惯例可分为三类:函数的调用者清理,函数清理,混合清理(有时由调用者清理,有时由函数自己清理)。
调用者清理
著名的cdecl就是由函数调用者清理栈的调用惯例。 cdecl是基于c语言的调用惯例,也是x86机器上大多数C编译器采用的调用惯例。
函数的返回结果多通过EAX寄存器返回。 对于32位机器,EAX能容纳4个字节。 整数或内存地址(指针),通过EAX寄存器返回是没有问题的。 超过4个字节的结构体呢?如何返回?
通过阅读 http://en.wikipedia.org/wiki/X86_calling_conventions,我找到了答案。 对于较小的结构体或对象,可以通过EAX:EDX寄存器对返回。 对于超大的对象或结构体,caller在调用函数之前会分配出内存空间,然后把这个空间地址作为第一个参数隐式地传给函数。被调用的函数callee把结果写进这片内存空间,再pop空间地址,之后才返回。 对于浮点数的结果,似乎是通过 ST0 x87 register(浮点寄存器)返回的。
因为调用者知道为参数分配了多少栈空间,所以由调用者清理栈就有一个好处: 为参数分配的栈空间大小可以动态决定。 因此cdecl支持可变参数的函数的调用,例如printf
。
如果强迫某个函数使用cdecl调用惯例,可以在函数声明中加_cdecl
关键字,如:
void _cdecl funct();
函数自己清理
pascal,stdcall,fastcall都是由函数来清理栈。 通过阅读程序的汇编代码,可以很容易识别这类调用惯例。因为函数返回前会清理栈。
pascal是基于PASCAL编程语言的函数调用惯例。 参数按照从左到右的顺序压栈(和cdecl的入栈顺序相反)。 OS/2 1.x,Microsoft Windows 3.x 和 Borland Delphi 1.x中的16位API都使用这种函数调用惯例。
stdcall是从pascal调用惯例演变出来的,和pascal不同的是,stdcall以从右到左的顺序对参数压栈。 返回值存储在EAX寄存器中。Win32 API就是采用的这种调用惯例。
fastcall是混合使用寄存器和栈来存储函数的参数,比如把前两个参数存储在寄存器中,其余的参数入栈。 有Microsoft fastcall和Borland fastcall等不同的实现。
由函数自己清理栈的好处在于:调用者不需要每次调用函数之后都清理栈,从而节省了不少代码, 从而生成的二进制文件比较小。坏处在于,由于清理栈的代码是事先生成在函数体内, 所以不能支持可变参数的函数。
混合清理
混合清理的代表是thiscall,对C++中非静态成员函数使用的就是这种调用惯例。
对于gcc编译器来说,thiscall几乎和cdecl相同:函数调用者负责清理栈,参数按从右到左的顺序入栈。 不同的是,thiscall最后会把this
指针压栈,就好象它是函数的第一个参数。(其实也是的吧)
对于Microsoft VC++编译器,thiscall类似于Windows API的stdcall,函数的参数从右到左压栈,由参数来清理栈。和stdcall不同的是,thiscall会通过ECX寄存器来传递this
指针。因为由函数自己清理栈不支持可变参数的函数调用,所以对于可变参数的函数,则由函数的调用者来清理栈。这是thiscall的灵活之处。
总结
调用惯例 | 出栈方 | 参数传递 | 名字修饰 |
---|---|---|---|
cdecl | 函数调用方 | 从右至左的顺序压参数入栈 | 下划线+函数名 |
pascal | 函数本身 | 从左至右的顺序入栈 | 较为复杂,参见pascal文档 |
stdcall | 函数本身 | 从右至左的顺序压参数入栈 | 下划线+函数名+@+参数的字节数, 如函数 int func(int a, double b)的修饰名是 _func@12 |
fastcall | 函数本身 | 头两个 DWORD(4字节)类型或者更少字节的参数 被放入寄存器,其他剩下的参数按从右至左的顺序入栈 | @+函数名+@+参数的字节数 |
thiscall | 不一定 | 从右至左的顺序压参数入栈(有时会通过寄存器传递this指针) | 不详 |