c/c++的函数参数压栈顺序.
为了这句话丢了很多次人.无所谓了,反正咱脸皮厚.
总结一下: 编译出来的c/c++程序的参数压栈顺序只和编译器相关!
下面列举了一些常见的编译器的调用约定
VC6:
调用约定 堆栈清除 参数传递
__cdecl 调用者 从右到左,通过堆栈传递
__stdcall 函数体 从右到左,通过堆栈传递
__fastcall 函数体 从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈
thiscall 函数体 this指针默认通过ECX传递,其它参数从右到左入栈
__cdecl是CC++的默认调用约定; VC的调用约定中并没有thiscall这个关键字,它是类成员函数默认调用约定;
CC++中的main(或wmain)函数的调用约定必须是__cdecl,不允许更改;
默认调用约定一般能够通过编译器设置进行更改,如果你的代码依赖于调用约定,请明确指出需要使用的调用约定;
C++Builder6:
调用约定 堆栈清除 参数传递
__fastcall 函数体 从左到右,优先使用寄存器(EAX,EDX,ECX),然后使用堆栈 (兼容Delphi的register)
(register与__fastcall等同)
__pascal 函数体 从左到右,通过堆栈传递
__cdecl 调用者 从右到左,通过堆栈传递(与CC++默认调用约定兼容)
__stdcall 函数体 从右到左,通过堆栈传递(与VC中的__stdcall兼容)
__msfastcall 函数体 从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈(兼容VC的__fastcall)
上述资料来源于HouSisong的一篇文章:http://www.allaboutprogram.com/index.php?option=content&task=view&id=29&Itemid=31.
由于能力和资源有限,只能找到这些东西,主要的差异体现在fastcall上面,vc是前两个参数放入寄存器,后面的压栈,bcb是前3哥参数使用寄存器,
更有变态的,一个朋友朋友说有的参数超过7个以后前5个从左到右传递,后面的从右到走,上面说的不可不信,不可全信.
如何确定你的编译采用的那种顺序那?
#include
int f(int i,int j,int k);
int main()
{
static int i=0;
f(i++,i++,i++);
return 0;
}
int f(int i,int j,int k)
{
int l;
int g;
printf("k=%d:[%x] ",k,&k);
printf("j=%d:[%x] ",j,&j);
printf("i=%d:[%x] ",i,&i);
printf("___________ ");
printf("l:%x ",&l);
printf("g:%x ",&g);
}
看看k->i的地址的增长顺序和l->g的顺序是否相同,如果相同则是从右到左,否则从左到右.
PS:
本来通过打印参数的值来判断那个先入栈,结果被一个朋友批评了,
他说:压栈顺序和参数计算顺序不是一回事,所以还是看地址更有保证.
//看过的朋友当作笑谈吧。
_cdecl
按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于“C”函数或者变量,修饰名是在函数名前加下划线。对于“C++”函数,有所不同。
如函数void test(void)的修饰名是_test;对于不属于一个类的“C++”全局函数,修饰名是?test@@ZAXXZ。
这是MFC缺省调用约定。由于是调用者负责把参数弹出栈,所以可以给函数定义个数不定的参数,如printf函数。
_stdcall
按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。对于“C”函数或者变量,修饰名以下划线为前缀,然后是函数名,然后是符号“@”及参数的字节数,如函数int func(int a, double b)的修饰名是_func@12。对于“C++”函数,则有所不同。
所有的Win32 API函数都遵循该约定。
_fastcall
头两个DWORD类型或者占更少字节的参数被放入ECX和EDX寄存器,其他剩下的参数按从右到左的顺序压入栈。由被调用者把参数弹出栈,对于“C”函数或者变量,修饰名以“@”为前缀,然后是函数名,接着是符号“@”及参数的字节数,如函数int func(int a, double b)的修饰名是@func@12。对于“C++”函数,有所不同。
未来的编译器可能使用不同的寄存器来存放参数。
thiscall
仅仅应用于“C++”成员函数。this指针存放于CX寄存器,参数从右到左压栈。thiscall不是关键词,因此不能被程序员指定。
naked call
采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。
naked call不是类型修饰符,故必须和_declspec共同使用,如下:
__declspec( naked ) int func( formal_parameters )
{
// Function body
}
[*]C 或者 C++ 标准只是规定了函数的参数是自动变量,并没有规定函数的参数是在栈内存中还是在寄存器中、如果在栈中的话其存放的先后顺序如何等。这些都属于具体的实现问题。
[*]函数参数的具体实现通常遵从于所采用的调用约定(或称调用协定)。调用约定独立于语言之外(比如多种语言有可能共用同一种调用约定)。
[*]调用约定有多种。编译器可以支持一种或者多种调用约定,在允许的范围内通常允许用户为函数指定某一调用约定。
[*]变长参数函数(简称变参函数)是参数个数可变的函数。变参函数的调用者知道调用时参数的个数和类型,而变参函数本身无从得知(虽然在实现中也可以给函数暗中增加一个额外的参数来传递信息,但这是非标准的做法)。由于变参函数的这个特点,通常依靠调用者和变参函数之间达成某种约定来解决实际参数和参数的类型未知的问题。
[*]为了便于在变参函数中遍历参数列表,变参函数的参数实现一般是按照顺序放在栈内存上。因此,对于只能通过寄存器传递参数的调用约定,通常在实现上还需要有一步向栈内存转换的过程。
[*]有入栈操作就要有相应的出栈操作。而实参个数未知意味着那些需要由函数自己来清理参数栈空间的调用约定无法适用在变参函数上——对于变参函数只能由函数的调用者负责释放参数占用的栈空间。实际上 cdecl 只是用来实现变参函数的一种常见的调用约定(这种调用约定按照从右到左的顺序对函数参数压栈),并不是唯一可行的调用约定。
[*]标准之前的 C 语言支持无固定参数(固定参数是参数列表中出现在 ... 之前的参数)的变参函数。一般在变参函数内访问参数总是从第一个参数开始的。为了便于定位第一个参数,对于变参函数通常采用从右向左的压栈方式,因为这样可以把第一个参数始终放在栈顶的位置。标准 C 或 C++ 要求变参函数至少要存在一个固定的参数,因而总可以通过最后一个固定参数以及与调用者的约定定位非固定参数,所以上面的从右向左的压栈限制就不存在了。
[*]C标准库在