您的位置:首页 > 编程语言 > C语言/C++

[C/C++]函数参数的入栈顺序与可变参数的实现

2013-06-26 16:22 836 查看
转自:http://blog.sina.com.cn/s/blog_54f82cc2010133mn.html

#include

void foo(int x, int y, int z)

{

printf("x = %d at [%X]\n", x, &x);

printf("y = %d at [%X]\n", y, &y);

printf("z = %d at [%X]\n", z, &z);

}

int main(int argc, char *argv[])

{

foo(100, 200, 300);

return 0;

}

运行结果:

x = 100 at [BFE28760]

y = 200 at [BFE28764]

z = 300 at [BFE28768]

C程序栈底为高地址,栈顶为低地址,因此上面的实例可以说明函数参数入栈顺序的确是从右至左的。可到底为什么呢?查了一直些文献得知,参数入栈顺序是和具体编译器实现相关的。比如,Pascal语言中参数就是从左到右入栈的,有些语言中还可以通过修饰符进行指定,如VisualC++。即然两种方式都可以,为什么C语言要选择从右至左呢?

进一步发现,Pascal语言不支持可变长参数,而C语言支持这种特色,正是这个原因使得C语言函数参数入栈顺序为从右至左。具体原因为:C方式参数入栈顺序(从右至左)的好处就是可以动态变化参数个数。通过栈堆分析可知,自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。

因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式。换句话说,如果不支持这个特色,C语言完全和Pascal一样,采用自左向右的参数入栈方式。可变参数主要通过第一个定参数来确定参数列表,所以从右至左入栈后,函数调用时pop出第一个参数就是参数列表的第一个确定参数,就OK了……

这儿其实还涉及到C语言中调用约定所采用的方式,下面简单的介绍一下:

__stdcall与C调用约定(__cdecl)的区别

C调用约定在返回前,要作一次堆栈平衡,也就是参数入栈了多少字节,就要弹出来多少字节.这样很安全.

有一点需要注意:stdcall调用约定如果采用了不定参数,即VARARG的话,则和C调用约定一样,要由调用者来作堆栈平衡.

(1)_stdcall是Pascal方式清理C方式压栈,通常用于Win32Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,在函数名后加上"@"和参数的字节数。int f(void *p) -->>_f@4(在外部汇编语言里可以用这个名字引用这个函数)

在WIN32API中,只有少数几个函数,如wspintf函数是采用C调用约定,其他都是stdcall

(2)C调用约定(即用 __cdecl关键字说明)(The Cdefault callingconvention)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数vararg的函数(如printf)只能使用该调用约定)。另外,在函数名修饰约定方面也有所不同。_cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。VC将函数编译后会在函数名前面加上下划线前缀。

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

(4)thiscall仅仅应用于"C++"成员函数。this指针存放于CX/ECX寄存器中,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。

(5)naked call。当采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。

  (这些代码称作 prolog and epilogcode,一般,ebp,esp的保存是必须的).

  但是naked call不产生这样的代码。nakedcall不是类型修饰符,故必须和_declspec共同使用。

  关键字__stdcall、__cdecl和__fastcall可以直接加在要输出的函数前。它们对应的命令行参数分别为/Gz、/Gd和/Gr。缺省状态为/Gd,即__cdecl。

  要完全模仿PASCAL调用约定首先必须使用__stdcall调用约定,至于函数名修饰约定,可以通过其它方法模仿。还有一个值得一提的是WINAPI宏,Windows.h支持该宏,它可以将出函数翻译成适当的调用约定,在WIN32中,它被定义为__stdcall。使用WINAPI宏可以创建自己的APIs。

综上,其实只有PASCAL调用约定的从左到右入栈的.而且PASCAL不能使用不定参数个数,其参数个数是一定的。

简单总结一下上面的几个调用方式:

调用约定

堆栈清除

参数传递

__cdecl

调用者

从右到左,通过堆栈传递

__stdcall

函数体

从右到左,通过堆栈传递

__fastcall

函数体

从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈

thiscall

函数体

this指针默认通过ECX传递,其他参数从右到左入栈

一.函数修饰符:

函数名字修饰(Decorated Name) 方式

函数的名字修饰(Decorated Name)就是编译器在编译期间创建的一个字符串,用来指明函数的定义或原型。LINK程序或其他工具有时需要指定函数的名字修饰来定位函数的正确位置。多数情况下程序员并不需要知道函数的名字修饰,LINK程序或其他工具会自动区分他们。当然,在某些情况下需要指定函数的名字修饰,例如在C++程序中,为了让LINK程序或其他工具能够匹配到正确的函数名字,就必须为重载函数和一些特殊的函数(如构造函数和析构函数)指定名字装饰。另一种需要指定函数的名字修饰的情况是在汇编程序中调用C或C++的函数。如果函数名字,调用约定,返回值类型或函数参数有任何改变,原来的名字修饰就不再有效,必须指定新的名字修饰。C和C++程序的函数在内部使用不同的名字修饰方式,下面将分别介绍这两种方式。

1._cdecl

按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于“C”函数或者变量,修饰名是在函数名前加下划线。对于“C++”函数,有所不同。

如 函数void test(void)的修饰名是_test;对于不属于一个类的“C++”全局函数,修饰名是?test@@ZAXXZ。

这 是MFC缺省调用约定。由于是调用者负责把参数弹出栈,所以可以给函数定义个数不定的参数,如printf函数。

2._stdcall

按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。对于“C”函数或者变量,修饰名以下划线为前缀,然后是函数名,然后是符号“@”及参数的字节数,如函数int func(int a, double b)的修饰名是_func@12。 对于“C++”函数,则有所不同。

所 有的Win32 API函数都遵循该约定。

3._fastcall

头两 个DWORD类型或者占更少字节的参数被放入ECX和EDX寄存器,其他剩下的参数按从右到左的顺序压入栈。由被调用者把参数弹出栈,对于“C”函数或者变量,修饰名以“@”为前缀,然后是函数名,接着是符号“@”及 参数的字节数,如函数int func(int a, double b)的 修饰名是@func@12。对于“C++”函数,有所不同。

未来的编译器可能使用不同的寄存器来存放参数。

4.thiscall

仅仅应 用于“C++”成员函数。this指 针存放于CX寄存器,参数从右到左压栈。thiscall不是关键词,因此不能被程序员指定。

5.naked call

采用1-4的 调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。

naked call不是类型修饰符,故必须和_declspec共同使用,如下:

__declspec( naked ) int func( formal_parameters )

{

// Function body

}

二.函数调用

千万要注意,C不支持默认参数

----------------------------------------------------------------------------------------------------------------

注释:默认参数:比如说下面的函数

intfun(int a,int b,int c=3)

{

}

c就是指定的默认实参,通常在函数原型中指定。这里给了3作为默认参数。用平常的时候调用这个函数fun(4,5,6);那么就是a=4,b=4,c=6。如果这样调用fun(1,2)那 么就是a=1,b=2,c=3,这里c没有指定,因为c是默认实参,已经有了默认值,这里c就 是采用默认值3。

为什么默认实参必须是函数参数表中最右边的参数。把上面的函数改下

intfun(int a=3,int b,int c)

{

}

这样调用fun(1,2), 这样就是a=1,b=2,而c根本就没有赋到值,就出错了。这些参数都是一一对应的。

--------------------------------------------------------------------------------------------------------------------

C/C++支持可变参数个数的函数定义,这一点与C/C++语言函数参数调用时入栈顺序有 关,

首先引用其他网友的一段文字,来描述函数调用,及参数入栈:

C支持可变参数的函数, 这里的意思是C支持函数带有可变数量的参数,最常见的例子就

是 我们十分熟悉的printf()系列函数。我们还知道在函数调用 时参数是自右向左压栈的。如果可变参数函数的一般形式是:

f(p1, p2,p3, …)

那么参数进栈(以及出栈)的顺序是:



pushp3

pushp2

pushp1

call f

pop p1

pop p2

pop p3



我可以得到这样一个结论:如果支持可变参数的函数,那么参数进栈的顺序几乎必然是

自右向左 的。并且,参数出栈也不能由函数自己完成,而应该由调用者完成。


这个结论的后半部分是不难理解的, 因为函数自身不知道调用者传入了多少参数,但是

调用者知道,所以调用者应该负责将所有参数 出栈。

在可变参数函数的一般形式中,左边是已经确定的参数,右边省略号代表未知参数部分。对于已经确定的参数,它在栈上的位置也必须是确定的。否则意味着已经确定的参数

是 不能定位和找到的,这样是无法保证函数正确执行的。衡量参数在栈上的位置,就是

离开确切的 函数调用点(call f)有多远。已经确定的参数,它在栈上的位置,不应该

依 赖参数的具体数量,因为参数的数量是未知的!

所以,选择只能是,已经确定的参 数,离开函数调用点有确定的距离(较近)。满足这

个条件,只有参数入栈遵从自右向左规则。 也就是说,左边确定的参数后入栈,离函数

调用点有确定的距离(最左边的参数最后入栈,离函 数调用点最近)。

这样,当函数开始执行后,它能找到 所有已经确定的参数。根据函数自己的逻辑,它负

责寻找和解释后面可变的参数(在离开调用点 较远的地方),通常这依赖于已经确定的

参数的值(典型的如prinf()函 数的格式解释,遗憾的是这样的方式具有脆弱性)。

据说在pascal中参数是自左向右压栈的,与C的相反。对于pascal这 种只支持固定参数函

数的语言,它没有可变参数带来的问题。因此,它选择哪种参数进栈方式都 是可以的。

甚至,其参数出栈是由函数自己完成的,而不是调用者,因为函数的参数的类型和数 量

是完全已知的。这种方式比采用C的 方式的效率更好,因为占用更少的代码量(在C中,

函 数每次调用的地方,都生成了参数出栈代码)。

C++为了兼容C,所以仍然支持函数带有可变的参数。但是在C++中 更好的选择常常是函数

重载。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: