int __attribute__((__cdecl__)) CalleeFunc(int i, int j, int k){
return i+j+k;
//int __attribute__((__stdcall__)) CalleeFunc(int i, int j, int k){
// return i+j+k;
//int __attribute__((__fastcall__)) CalleeFunc(int i, int j, int k){
// return i+j+k;
void CallerFunc(void){
CalleeFunc(0x11, 0x22, 0x33);
int main(int argc, char *argv[]){
CallerFunc();
return 0;
被调函数CalleeFunc分别声明为cdecl、stdcall和fastcall约定时,其汇编代码比较如下表所示:
3. 调用约定影响
当函数导出被其他程序员所使用(如库函数)时,该函数应遵循主要的调用约定,以便于程序员使用。若函数仅供内部使用,则其调用约定可只被使用该函数的程序所了解。
在多语言混合编程(包括A语言中使用B语言开发的第三方库)时,若函数的原型声明和函数体定义不一致或调用函数时声明了不同的函数约定,将可能导致严重问题(如堆栈被破坏)。
以Delphi调用C函数为例。Delphi函数缺省采用stdcall调用约定,而C函数缺省采用cdecl调用约定。一般将C函数声明为stdcall约定,如:
int __stdcall add(int a, int b);
在Delphi中调用该函数时也应声明为stdcall约定:
function add(a: Integer; b: Integer): Integer; stdcall; //参数类型应与DLL中的函数或过程参数类型一致,且引用时使用stdcall参数
external 'a.dll'; //指定被调DLL文件的路径和名称
不同编译器产生栈帧的方式不尽相同,主调函数不一定能正常完成清栈工作;而被调函数必然能自己完成正常清栈,因此,在跨(开发)平台调用中,通常使用stdcall调用约定(不少WinApi均采用该约定)。
此外,主调函数和被调函数所在模块采用相同的调用约定,但分别使用C++和C语法编译时,会出现链接错误(报告被调函数未定义)。这是因为两种语言的函数名字修饰规则不同,解决方式是使用extern "C"
告知主调函数所在模块:被调函数是C语言编译的。采用C语言编译的库应考虑到使用该库的程序可能是C++程序(使用C++编译器),通常应这样声明头文件:
#ifdef _cplusplus
extern "C" {
#endif
type Func(type para);
#ifdef _cplusplus
#endif
这样C++编译器就会按照C语言修饰策略链接Func函数名,而不会出现找不到函数的链接错误.
4. x86函数参数传递方法
x86处理器ABI规范中规定,所有传递给被调函数的参数都通过堆栈来完成,其压栈顺序是以函数参数从右到左的顺序。当向被调函数传递参数时,所有参数最后形成一个数组。由于采用从右到左的压栈顺序,数组中参数的顺序(下标0~N-1)与函数参数声明顺序(Para1~N)一致。因此,在函数中若知道第一个参数地址和各参数占用字节数,就可通过访问数组的方式去访问每个参数。
4.1 整型和指针参数的传递
整型参数与指针参数的传递方式相同,因为在32位x86处理器
上整型与指针大小相同(均为四字节)。下表给出这两种类型的参数在栈帧中的位置关系。注意,该表基于tail函数的栈帧。
4.2 浮点参数的传递
浮点参数的传递与整型类似,区别在于参数大小。x86处理器中浮点类型占8个字节,因此在栈中也需要占用8个字节。下表给出浮点参数在栈帧中的位置关系。图中,调用tail()函数
的第一个和第三个参数均为浮点类型,因此需各占用8个字节,三个参数共占用20个字节。表中word类型
的大小是4字节。
4.3 结构体和联合体参数的传递
结构体和联合体参数的传递与整型、浮点参数类似,只是其占用字节大小视数据结构的定义不同而异。x86处理器上栈宽是4字节,故结构体在栈上所占用的字节数为4的倍数。编译器会对结构体进行适当的填充以使得结构体大小满足4字节对齐的要求。
对于一些RISC处理器
(如PowerPC),其参数传递并不是全部通过栈来实现。PowerPC处理器寄存器中,R3~R10共8个寄存器用于传递整型或指针参数,F1~F8共8个寄存器用于传递浮点参数。当所需传递的参数少于8个时,不需要用到栈。结构体和long double参数的传递通过指针来完成,这与x86处理器完全不同。PowerPC的ABI规范中规定,结构体的传递采用指针方式,而不是像x86处理器那样将结构从一个函数栈帧中拷贝到另一个函数栈帧中,显然x86处理器的方式更低效。可见,PowerPC程序中,函数参数采用指向结构体的指针(而非结构体)并不能提高效率,不过通常这是良好的编程习惯。
5. x86函数返回值传递方法
函数返回值可通过寄存器传递。当被调用函数需要返回结果给调用函数时:
1) 若返回值不超过4字节(如int、short、char、指针等类型),通常将其保存在EAX寄存器中,调用方通过读取EAX获取返回值。
2) 若返回值大于4字节而小于8字节(如long long或_int64类型),则通过EAX+EDX寄存器联合返回,其中EDX保存返回值高4字节,EAX保存返回值低4字节。
3) 若返回值为浮点类型(如float和double),则通过专用的协处理器浮点数寄存器栈的栈顶返回。
4) 若返回值为结构体或联合体,则主调函数向被调函数传递一个额外参数,该参数指向将要保存返回值的地址。即函数调用foo(p1, p2)被转化为foo(&p0, p1, p2),以引用型参数形式传回返回值。具体步骤可能为:a.主调函数将显式的实参逆序入栈;b.将接收返回值的结构体变量地址作为隐藏参数入栈(若未定义该接收变量,则在栈上额外开辟空间作为接收返回值的临时变量);c. 被调函数将待返回数据拷贝到隐藏参数所指向的内存地址,并将该地址存入%eax寄存器。因此,在被调函数中完成返回值的赋值工作。
注意,函数如何传递结构体或联合体返回值依赖于具体实现。不同编译器、平台、调用约定甚至编译参数下可能采用不同的实现方法。如VC6编译器对于不超过8字节的小结构体,会通过EAX+EDX
寄存器返回。而对于超过8字节的大结构体,主调函数在栈上分配用于接收返回值的临时结构体,并将地址通过栈传递给被调函数;被调函数根据返回值地址设置返回值(拷贝操作);调用返回后主调函数根据需要,再将返回值赋值给需要的临时变量(二次拷贝)。实际使用中为提高效率,通常将结构体指针作为实参传递给被调函数以接收返回值。
5) 不要返回指向栈内存的指针,如返回被调函数内局部变量地址(包括局部数组名)。因为函数返回后,其栈帧空间被“释放”,原栈帧内分配的局部变量空间的内容是不稳定和不被保证的。
函数返回值通过寄存器传递,无需空间分配等操作,故返回值的代价很低。基于此原因,C89规范
中约定,不写明返回值类型的函数,返回值类型默认为int。但这会带来类型安全隐患,如函数定义时返回值为浮点数,而函数未声明或声明时未指明返回值类型,则调用时默认从寄存器EAX
(而不是浮点数寄存器)中获取返回值,导致错误!因此在C++中,不写明返回值类型的函数返回值类型为void,表示不返回值。
【扩展阅读】GCC返回结构体和联合体
通常GCC被配置为使用与目标系统一致的函数调用约定。这通过机器描述宏来实现。但是,在一些目标机上采用不同方式返回结构体和联合体的值。因此,使用PCC编译的返回这些类型的函数不能被使用GCC编译的代码调用,反之亦然。但这并未造成麻烦,因为很少有Unix库函数返回结构体或联合体。
GCC代码使用存放int或double类型返回值的寄存器来返回1、2、4或8个字节的结构体和联合体(GCC通常还将此类变量分配在寄存器中)。其它大小的结构体和联合体在返回时,将其存放在一个由调用者传递的地址中(通常在寄存器中)。
相比之下,PCC在大多目标机上返回任何大小的结构体和联合体时,都将数据复制到一个静态存储区域,再将该地址当作指针值返回。调用者必须将数据从那个内存区域复制到需要的地方。这比GCC使用的方法要慢,而且不可重入。
在一些目标机上(如RISC机器和80386),标准的系统约定是将返回值的地址传给子程序。在这些机器上,当使用这种约定方法时,GCC被配置为与标准编译器兼容。这可能会对于1,2,4或8字节的结构体不兼容。
GCC使用系统的标准约定来传递参数。在一些机器上,前几个参数通过寄存器传递;在另一些机器上,所有的参数都通过栈传递。原本可在所有机器上都使用寄存器来传递参数,而且此法还可能显著提高性能。但这样就与使用标准约定的代码完全不兼容。所以这种改变只在将GCC作为系统唯一的C编译器时才实用。当拥有一套完整的GNU 系统,能够用GCC来编译库时,可在特定机器上实现寄存器参数传递。
在一些机器上(特别是SPARC),一些类型的参数通过“隐匿引用”(invisible reference)来传递。这意味着值存储在内存中,将值的内存地址传给子程序。