C++ 函数参数入栈方式与调用约定

代码开发运行环境: VS2017+Win32+Debug


1.调用约定简介

实现函数调用,除了需要根据函数名称获取函数的入口地址外,还要向函数传递合适的参数以及结束时清理堆栈。这些可以有不同的实现方式,为了能够让函数主调方顺利完成对被调方的调用,二者需要遵守相同的约定,这样的约定被称为调用约定(Calling Convention)或调用规范。C/C++中常见的调用约定有__cdecl、__stdcall、__fastcall和__thiscall。

1.1__cdecl 调用约定

称为 C 调用约定,是 C/C++ 默认的函数调用约定,语法是:

int function (int a ,int b)           // 不加修饰就是C调用约定
int __cdecl function(int a,int b)     // 明确指出C调用约定

约定的内容有:
(1)参数从右向左入栈;
(2)在被调用函数 (Callee) 返回后,由调用方 (Caller)调整堆栈;
(3)函数名修饰规则:下划线+函数名。

C调用约定允许函数的参数个数不固定,这也是C语言的一大特色,因为每个调用的地方都需要生成一段清理堆栈的代码,所以最后生成的目标文件较__stdcall、__fastcall调用方式要大。

1.2__stdcall 调用约定

称为标准调用约定,语法是:

int __stdcall function(int a,int b)

约定的内容有:
(1)参数从右向左入栈;
(2)函数自身清理堆栈;
(3)函数名修饰规则:下划线+函数名+@+参数的字节数。如函数int foo(int a,double b)的修饰名是_func@12。

1.3__fastcall 调用约定

称为快速调用方式,语法是:

int __fastcall function(int a,int b);

和 __stdcall 类似,它约定的内容有:
(1) 函数的第一个和第二个DWORD(4字节)参数(或者尺寸更小的)通过ecx和edx传递,其他参数从右向左入栈;
(2)被调用者清理堆栈;
(3)函数名修饰规则:@+函数名+@+参数的字节数。

注意,不同编译器编译的程序规定的寄存器不同。在 Intel 386 平台上,使用 ECX 和 EDX 寄存器。使用__fastcall 方式无法用作跨编译器的接口。

1.4__thiscall 调用约定

__thiscall 用于C++类成员函数的调用约定。因为 __thiscall 不是关键字,所以不能被显示指明。由于成员函数调用还有一个this指针,因此必须特殊处理。__thiscall 在不同平台有着不同的实现。在Visual C++中,this 指针存放于ecx寄存器,参数从右向左入栈。在 GNU C++ 中,__thiscall__cdecl 完全一样,只是将 this 看作函数的第一个参数。

2.cout<<++i<<- -i<< i++; 输出结果的讨论

在Visual C++的函数调用规范中,如果函数的任何一个参数表达式包含自增(自减)运算,所有这些运算会在第一个push操作之前全部完成,然后再完成其他的运算并将结果入栈。考察如下程序。

#include <iostream>
using namespace std;
int main(int argc,char* argv[])
{
	int i=10;
	cout<<++i<<--i<<i++;
	return 0;
}

按照正常思维,标准输出操作符 << 是从左向右结合的,所以应该依次计算表达式 ++i,–i 和 i++ 的值,那么最终应该依次输出11,10,和10。但是在 Visual C++ 中运行结果是 11,11 和 10。考察此程序的汇编代码,发现语句 cout<<++i<<--i<<i++; 所对应的汇编代码是:

00EF6ED5  mov         eax,dword ptr [i]  
00EF6ED8  mov         dword ptr [ebp-0D0h],eax  //保存i的值

00EF6EDE  mov         ecx,dword ptr [i]  
00EF6EE1  add         ecx,1  					//变量i自增1
00EF6EE4  mov         dword ptr [i],ecx  

00EF6EE7  mov         edx,dword ptr [i]  
00EF6EEA  sub         edx,1  					//变量i自减1
00EF6EED  mov         dword ptr [i],edx  		

00EF6EF0  mov         eax,dword ptr [i]  
00EF6EF3  add         eax,1  					//变量i自增1
00EF6EF6  mov         dword ptr [i],eax  

00EF6EF9  mov         esi,esp  
00EF6EFB  mov         ecx,dword ptr [ebp-0D0h]  
00EF6F01  push        ecx  						//将保存的数值10入栈

00EF6F02  mov         edi,esp  
00EF6F04  mov         edx,dword ptr [i]  
00EF6F07  push        edx  						//将变量i入栈

00EF6F08  mov         ebx,esp  
00EF6F0A  mov         eax,dword ptr [i]  
00EF6F0D  push        eax  						//将变量i入栈

//获取cout对象地址,this指针通过ecx传递
00EF6F0E  mov         ecx,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0F002E0h)]

//cout<<++i;
00EF6F14  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F002E8h)]
00EF6F1A  cmp         ebx,esp
00EF6F1C  call        __RTC_CheckEsp (0EF12DFh)

//cout<<--i;
00EF6F21  mov         ecx,eax  
00EF6F23  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F002E8h)]
00EF6F29  cmp         edi,esp  
00EF6F2B  call        __RTC_CheckEsp (0EF12DFh)  

//cout<<i++;
00EF6F30  mov         ecx,eax  
00EF6F32  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F002E8h)]
00EF6F38  cmp         esi,esp  
00EF6F3A  call        __RTC_CheckEsp (0EF12DFh)  

这段汇编代码比较复杂,先解释关键的地方。首先,虽然<<运算符是从左向右结合,但在<<运算符构成的链式操作中,各表达式的入栈顺序还是从右向左,只有这样才能实现<<运算从左向右进行。所以,先计算的是表达式 i++ 的值。因为 i 自增之后无法提供入栈的值,所以另外开辟了一个内存单元 dword ptr [ebp-0D0h] 来存放第一个入栈的表达式的值。

接着计算 --i 的值,自减运算完成之后,编译器认为i的值可以直接作为参数入栈,所以并没有开辟别的内存单元存放这一个入栈参数的值。

再接下来计算++i情形跟计算- -i类似。这些操作完成之后,分别将 dword ptr [ebp-0D0h] 处的值、最终的i和i入栈。再三次调用 cout.operator<< 函数将它们输出。所以程序的最终结果是11,11,10。

汇编代码中cmp ebx,espcall __RTC_CheckEsp (0EF12DFh)表示VC编译器提供了运行时刻对程序正确性/安全性的一种动态检查,可以在项目属性的C++选项中打开来启用Runtime Check。开启与打开步骤如下图:
这里写图片描述

在程序中 cout.operator<< 执行完后,会将对象 cout 的地址存放在寄存器 eax 中作为该函数的返回值。由于在 Visual C++中,调用对象的成员函数之前会先将对象的地址存放在寄存器 ecx 中,所以在下一次调用cout.operator<< 之前,会先将 eax 的值送入 ecx 中。

如果生成 Release 版本,发现输出结果变成10,10和10。这是编译器对代码所做的优化导致的结果。

从上面的程序中,我们可以看出,自增(自减)运算虽然可以使表达式更为紧凑,但很容易带来副作用。过分追求小的技巧正式很多程序缺陷的缘由,应该编写哪些可读性较好的代码,避免那些看似简单但蕴藏危机的表达式。

假设 i 的值是 10,执行语句 i=i++; 之后,i 的值是多少呢?其实,这样的代码在不同的编译器中有着不同的实现,输出结果是不一样的,所以,尽量避免编写类似存在二义性的代码。


参考文献

[1] 陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008. C3.2函数参数是如何传递的.P94-P97
[2] 百度百科.__stdcall
[3] 百度百科.__cdecl
[4] 程序员的自我修养[M].机械工业出版社.C10.2.2调用惯例.P293-P299

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读