代码开发运行环境: Win7+VS2012 +Win32
首先,要实现函数调用,除了要知道函数的入口地址外,还要向函数传递合适的参数。向被调函数传递参数,可以有不同的方式实现。这些方式被称为“调用规范”或“调用约定”。C/C++中常见的调用规范有__cdecl、__stdcall、__fastcall和__thiscall。
__cdecl调用约定又称为C调用约定,是C/C++默认的函数调用约定,它的定义语法是:
int function (int a ,int b) // 不加修饰就是C调用约定
int __cdecl function(int a,int b) // 明确指出C调用约定
约定的内容有:
(1)参数入栈顺序是从右向左;
(2)在被调用函数 (Callee) 返回后,由调用方 (Caller)调整堆栈。
由于这种约定,C调用约定允许函数的参数的个数是不固定的,这也是C语言的一大特色。因为每个调用的地方都需要生成一段清理堆栈的代码,所以最后生成的目标文件较__stdcall、__fastcall调用方式要大,因为每一个主调函数在每个调用的地方都需要生成一段清理堆栈的代码。
__stdcall又称为标准调用约定,申明语法是:
int __stdcall function(int a,int b)
约定的内容有:
(1)参数从右向左压入堆栈;
(2)函数自身清理堆栈;
(3)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。
__fastcall又称为快速调用方式。和__stdcall类似,它约定的内容有:
(1) 函数的第一个和第二个DWORD参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过从右向左的顺序压栈;
(2)被调用函数清理堆栈;
(3)函数名修改规则同stdcall。
其声明语法为:
int __fastcall function(int a,int b);
__thiscall调用规范是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理,thiscall意味着:
(1) 参数从右向左入栈;
(2) 如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈;
(3)对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈。
在Visual C++的函数调用规范中,如果函数的任何一个参数表达式包含自增(自减)运算,所有这些运算会在第一个push操作之前全部完成,然后再完成其他的运算并将结果入栈。考察如下程序。
#include <iostream>
using namespace std;
int main(int argc,char* argv[])
{
int i=10;
cout<<++i<<--i<<i++;
getchar();
return 0;
}
按照“正常”思维,标准输出操作符<<是从左向右结合的,所以应该依次计算表达式++i,–i和i++的值,那么最终应该依次输出11,10,和10。但是在Visual C++中运行结果是11,11和10。考察此程序的汇编代码,发现语句 cout<<++i<<–i<
00EF6ED5 mov eax,dword ptr [i]
00EF6ED8 mov dword ptr [ebp-0D0h],eax
00EF6EDE mov ecx,dword ptr [i]
00EF6EE1 add ecx,1
00EF6EE4 mov dword ptr [i],ecx
00EF6EE7 mov edx,dword ptr [i]
00EF6EEA sub edx,1
00EF6EED mov dword ptr [i],edx
00EF6EF0 mov eax,dword ptr [i]
00EF6EF3 add eax,1
00EF6EF6 mov dword ptr [i],eax
00EF6EF9 mov esi,esp
00EF6EFB mov ecx,dword ptr [ebp-0D0h]
00EF6F01 push ecx
00EF6F02 mov edi,esp
00EF6F04 mov edx,dword ptr [i]
00EF6F07 push edx
00EF6F08 mov ebx,esp
00EF6F0A mov eax,dword ptr [i]
00EF6F0D push eax
00EF6F0E mov ecx,dword ptr ds:[0F002E0h]
00EF6F14 call dword ptr ds:[0F002E8h]
00EF6F1A cmp ebx,esp
00EF6F1C call __RTC_CheckEsp (0EF12DFh)
00EF6F21 mov ecx,eax
00EF6F23 call dword ptr ds:[0F002E8h]
00EF6F29 cmp edi,esp
00EF6F2B call __RTC_CheckEsp (0EF12DFh)
00EF6F30 mov ecx,eax
00EF6F32 call dword ptr ds:[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,esp和call __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.
[2]百度百科.__stdcall
[3]百度百科.__cdecl