函数调用约定通常规定如下几方面内容:
\1) 函数参数的传递顺序和方式
最常见的参数传递方式是通过堆栈传递。主调函数将参数压入栈中,被调函数以相对于帧基指针的正偏移量来访问栈中的参数。对于有多个参数的函数,调用约定需规定主调函数将参数压栈的顺序(从左至右还是从右至左)。某些调用约定允许使用寄存器传参以提高性能。
\2) 栈的维护方式
主调函数将参数压栈后调用 被调函数体,返回时需将 被压栈的参数 全部弹出,以便将栈恢复到调用前的状态。该清栈过程可由 主调函数 负责完成,也可由 被调函数 负责完成。
\3) 名字修饰(Name-mangling)策略
又称函数名修饰(Decorated Name)规则。编译器在链接时为区分不同函数,对函数名作不同修饰。
若函数之间的调用约定不匹配,可能会产生堆栈异常或链接错误等问题。因此,为了保证程序能正确执行,所有的函数调用均应遵守一致的调用约定。
Windows下可直接在函数声明前添加关键字__stdcall、__cdecl或__fastcall等标识确定函数的调用方式,如int __stdcall func()。Linux下可借用函数attribute 机制,如int attribute((stdcall)) func()。
c++函数的缺省值:
c++中,定义函数的时候可以让最右边的连续若干个参数有缺省值,在调用函数的时候,如果不写相应位置的参数,则调用的参数就为缺省值。
不同编译器产生栈帧的方式不尽相同,主调函数不一定能正常完成清栈工作;而被调函数必然能自己完成正常清栈,因此,在跨(开发)平台调用中,通常使用stdcall调用约定(不少WinApi均采用该约定)。
此外,主调函数和被调函数所在模块采用相同的调用约定,但分别使用C++和C语法编译时,会出现链接错误(报告被调函数未定义)。这是因为两种语言的函数名字修饰规则不同,解决方式是使用extern “C”告知主调函数所在模块:被调函数是C语言编译的。采用C语言编译的库应考虑到使用该库的程序可能是C++程序(使用C++编译器),通常应这样声明头文件:
1 | 1 |
这样C++编译器就会按照C语言修饰策略链接Func函数名,而不会出现找不到函数的链接错误。
x86函数参数传递方法:
x86处理器ABI规范中规定,所有传递给被调函数的参数都通过堆栈来完成,其压栈顺序是以函数参数从右到左的顺序。当向被调函数传递参数时,所有参数最后形成一个数组。由于采用从右到左的压栈顺序,数组中参数的顺序(下标0N-1)与函数参数声明顺序(Para1N)一致。因此,在函数中若知道第一个参数地址和各参数占用字节数,就可通过访问数组的方式去访问每个参数。
栈帧地址未压入参数时为4(%ebp)
x86函数返回值传递方法
函数返回值可通过寄存器传递。当被调用函数需要返回结果给调用函数时:
\1) 若返回值不超过4字节(如int、short、char、指针等类型),通常将其保存在EAX寄存器中,调用方通过读取EAX获取返回值。
\2) 若返回值大于4字节而小于8字节(如long long或_int64类型),则通过EAX+EDX寄存器联合返回,其中EDX保存返回值高4字节,EAX保存返回值低4字节。
\3) 若返回值为浮点类型(如float和double),则通过专用的协处理器浮点数寄存器栈的栈顶返回。
\4) 若返回值为结构体或联合体,则主调函数向被调函数传递一个额外参数,该参数指向将要保存返回值的地址。
eg:函数调用foo(p1, p2)被转化为foo(&p0, p1, p2),以引用型参数形式传回返回值。
具体步骤可能为:
a.主调函数将显式的实参逆序入栈;
1 | #"逆序入栈" 通常是指在将一系列元素入栈(push)到栈(stack)中时,这些元素按照它们原始顺序的逆序进入栈。也就是说,最后一个元素首先入栈,然后是倒数第二个元素,依此类推,直到第一个元素最后入栈。这样做通常是为了在后续的出栈(pop)操作中,元素能够以原始顺序出现。 |
b.将接收返回值的结构体变量地址作为隐藏参数入栈(若未定义该接收变量,则在栈上额外开辟空间作为接收返回值的临时变量);
c. 被调函数将待返回数据拷贝到隐藏参数所指向的内存地址,并将该地址存入%eax寄存器。
因此,在被调函数中完成返回值的赋值工作。****
注意:函数如何传递结构体或联合体返回值依赖于具体实现。不同编译器、平台、调用约定甚至编译参数下可能采用不同的实现方法(VC6编译器对于不超过8字节的小结构体,会通过EAX+EDX寄存器返回)。而对于超过8字节的大结构体,主调函数在栈上分配用于接收返回值的临时结构体,并将地址通过栈传递给被调函数;被调函数根据返回值地址设置返回值(拷贝操作);调用返回后主调函数根据需要,再将返回值赋值给需要的临时变量(二次拷贝)。实际使用中为提高效率,通常将结构体指针作为实参传递给被调函数以接收返回值(这种方法允许你修改结构体的内容,但并不能直接返回一个全新的结构体。如果你需要返回一个全新的结构体,你可能需要动态分配内存,但这会带来额外的复杂性和责任)。
\5) 不要返回指向栈内存的指针,如返回被调函数内局部变量地址(包括局部数组名)。因为函数返回后,其栈帧空间被“释放”,原栈帧内分配的局部变量空间的内容是不稳定和不被保证的。
函数返回值通过寄存器传递,无需空间分配等操作,故返回值的代价很低。基于此原因,C89规范中约定,不写明返回值类型的函数,返回值类型默认为int。但这会带来类型安全隐患,如函数定义时返回值为浮点数,而函数未声明或声明时未指明返回值类型,则调用时默认从寄存器EAX(而不是浮点数寄存器)中获取返回值,导致错误!因此在C++中,不写明返回值类型的函数返回值类型为void,表示不返回值。
GCC使用系统的标准约定来传递参数。在一些机器上,前几个参数通过寄存器传递;在另一些机器上,所有的参数都通过栈传递。原本可在所有机器上都使用寄存器来传递参数,而且此法还可能显著提高性能。但这样就与使用标准约定的代码完全不兼容。所以这种改变只在将GCC作为系统唯一的C编译器时才实用。当拥有一套完整的GNU 系统,能够用GCC来编译库时,可在特定机器上实现寄存器参数传递。
在一些机器上(特别是SPARC),一些类型的参数通过“隐匿引用”(invisible reference)来传递。这意味着值存储在内存中,将值的内存地址传给子程序。