分享

函数参数的压栈顺序

 semo_zhang 2013-12-09

 在CSDN上看到一篇关于函数参数的压栈顺序的帖子,挺有意思,贴出如下:
网址:http://topic.csdn.net/u/20100216/21/ec98464e-a47e-4263-bb1c-a001e130ba87.html
设int arr[]={6,7,8,9,10};
int *ptr=arr;
*(ptr++)+=123;
printf("%d,%d",*ptr,*(++ptr));
答案为什么是:8,8


int arr[]={6,7,8,9,10};
int *ptr=arr;//现在ptr指向6
*(ptr++)+=123;//现在ptr指向7,第一个元素变为129
printf("%d,%d",*ptr,*(++ptr)); //考虑从右往左计算,先是*(++ptr),现在ptr指向8,然后*ptr也是8,输出8,8

2
这个题考的关键估计就是printf的运算顺序
printf的参数,函数printf从左往右读取,那么将会将数据从右向左压入栈中,处理时候是从栈顶开始的,由函数的调用者将栈中的参数弹出栈。

 

3
printf("%d,%d",*ptr,*(++ptr));中每个参数的求值顺序是未规定的,或者说是编译器相关的,所以如果本题的编译器先对参数* (++ptr)求值,那么++的副作用(即使ptr+=1)就在求值后已经产生,之后再对参数*ptr求值,所用的ptr就是增加之后的ptr.如果这样,结果就是8,8
但是C/C++标准没有规定编译器对参数求值的顺序是从左到右还是从右到左,还是先中间后两边,所以这题有可能在某个编译器上实现为先对参数*ptr求值,再对参数,*(++ptr)求值,这样结果就是7,8
总之这题结果是编译器相关的
4
整理一下函数调用约定如下: 
          函数调用约定不仅决定了发生函数调用时函数参数的入栈顺序,还决定了是由调用者函数还是被调用函数负责清除栈中的参数,还原堆栈。函数调用约定有很多方式,除了常见的__cdecl,__fastcall和__stdcall之外,C++的编译器还支持thiscall方式,不少C/C++编译器还支持 naked call方式。这么多函数调用约定常常令许多程序员很迷惑,到底它们是怎么回事,都是在什么情况下使用呢?下面就分别介绍这几种函数调用约定。


1.__cdecl

          编译器的命令行参数是/Gd。__cdecl方式是C/C++编译器默认的函数调用约定,所有非C++成员函数和那些没有用__stdcall或 __fastcall声明的函数都默认是__cdecl方式,它使用C函数调用方式,函数参数按照从右向左的顺序入栈,函数调用者负责清除栈中的参数,由于每次函数调用都要由编译器产生清除(还原)堆栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是 __cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf和windows的API wsprintf就是__cdecl调用方式。对于C函数,__cdecl方式的名字修饰约定是在函数名称前添加一个下划线;对于C++函数,除非特别使用extern "C",C++函数使用不同的名字修饰方式。


2.__fastcall

          编译器的命令行参数是/Gr。__fastcall函数调用约定在可能的情况下使用寄存器传递参数,通常是前两个 DWORD类型的参数或较小的参数使用ECX和EDX寄存器传递,其余参数按照从右向左的顺序入栈,被调用函数在返回之前负责清除栈中的参数。编译器使用两个@修饰函数名字,后跟十进制数表示的函数参数列表大小,例如:@function_name@number。需要注意的是__fastcall函数调用约定在不同的编译器上可能有不同的实现,比如16位的编译器和32位的编译器,另外,在使用内嵌汇编代码时,还要注意不能和编译器使用的寄存器有冲突。


3.__stdcall

编译器的命令行参数是/Gz,__stdcall是Pascal程序的缺省调用方式,大多数Windows的API也是__stdcall调用约定。 __stdcall函数调用约定将函数参数从右向左入栈,除非使用指针或引用类型的参数,所有参数采用传值方式传递,由被调用函数负责清除栈中的参数。对于C函数,__stdcall的名称修饰方式是在函数名字前添加下划线,在函数名字后添加@和函数参数的大小,例如:_functionname@number

4.thiscall

          thiscall只用在C++成员函数的调用,函数参数按照从右向左的顺序入栈,类实例的this指针通过ECX寄存器传递。需要注意的是thiscall不是C++的关键字,不能使用thiscall声明函数,它只能由编译器使用。

5.naked call

            采用前面几种函数调用约定的函数,编译器会在必要的时候自动在函数开始添加保存ESI,EDI,EBX,EBP寄存器的代码,在退出函数时恢复这些寄存器的内容,使用naked call方式声明的函数不会添加这样的代码,这也就是为什么称其为naked的原因吧。naked    call不是类型修饰符,故必须和_declspec共同使用。

            VC的编译环境默认是使用__cdecl调用约定,也可以在编译环境的Project Setting...菜单-》C/C++ =》Code    Generation项选择设置函数调用约定。也可以直接在函数声明前添加关键字__stdcall、__cdecl或__fastcall等单独确定函数的调用方式。在Windows系统上开发软件常用到WINAPI宏,它可以根据编译设置翻译成适当的函数调用约定,在WIN32中,它被定义为 __stdcall。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多