关于函数参数入栈的思考(函数调用约定,入栈顺序)

代码开发运行环境: Win7+VS2012 +Win32


1.调用规范简介

首先,要实现函数调用,除了要知道函数的入口地址外,还要向函数传递合适的参数。向被调函数传递参数,可以有不同的方式实现。这些方式被称为“调用规范”或“调用约定”。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)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸; (4)函数参数个数不可变。

__fastcall调用约定 又称为快速调用方式。和__stdcall类似,它约定的内容有: (1) 函数的第一个和第二个DWORD参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过从右向左的顺序压栈; (2)被调用者清理堆栈; (3)函数名修改规则同stdcall。

其声明语法为:

int __fastcall function(int a,int b);

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

__thiscall调用约定 是唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个this指针,因此必须特殊处理,thiscall意味着: (1) 参数从右向左入栈; (2) 如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈; (3)对参数个数不定的,调用者清理堆栈,否则函数自己清理堆栈。


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++;
    getchar();
    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  
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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Kevin-ZhangCG

[ Java面试题 ]基础篇之二

1685
来自专栏AzMark

Python 学习笔记之类与实例

类 (class) 封装一组相关数据,使之成为一个整体,并使用一种方法持续展示和维护。

511
来自专栏Python爱好者

Android面试之Java基础

1403
来自专栏用户2442861的专栏

Python yield 使用浅析

您可能听说过,带有 yield 的函数在 Python 中被称之为 generator(生成器),何谓 generator ?

911
来自专栏Celebi的专栏

C/C++ 学习笔记七(内存管理)

工作中经常使用到C/C++,为对C有个比较深刻的了解,重新拾起学习C的任务。在看书的同时,记录下思考的过程,也记录下重要的知识点。

2880
来自专栏小樱的经验随笔

记一次拿webshell踩过的坑(如何用PHP编写一个不包含数字和字母的后门)

这一串代码描述是这样子,我们要绕过A-Za-z0-9这些常规数字、字母字符串的传参,将非字母、数字的字符经过各种变换,最后能构造出 a-z 中任意一个字符,并且...

1792
来自专栏yang0range

Java的面试基础题(二)

1)特点:存储对象;长度可变;存储对象的类型可不同 2)Collection (1)List:有序的;元素可重复,有索引 (add(index, elem...

1822
来自专栏何俊林

NDK开发才是有精华和特色的部分

通常有特色的应用都会涉及NDK开发,而NDK开发基本是C/C++开发,核心和精华也是在这块。像滤镜、美颜、美肤、人脸识别,编解码,这些出了问项目问题外,首先要过...

3123
来自专栏编程

Python基础—让你规范Python语言的使用

群内不定时分享干货,包括最新的python企业案例学习资料和零基础入门教程,欢迎初学和进阶中的小伙伴入群学习交流 ? Lint 定义: pylint是一个在Py...

2218
来自专栏大闲人柴毛毛

图的邻接表示法Java版

边节点 ? 一个边节点有一条边 和 一个终止节点组成。 /** * 边节点(由一条边和一个终止节点构成) */ class ENode{ i...

3597

扫码关注云+社区

领取腾讯云代金券