函数调用约定__stdcall与__cdecl

在Windows编程中,我们经常看到如int WINAPI _tWinMain(HINSTANCE hInstanceExe, PSTR pszCmdLine, int nCmdShow)这样的函数定义,它被WINAPI所修饰。WINAPI其实是一个宏,我们可以在WinDef.h中找到它的定义:

#define WINAPI      __stdcall

__stdcall是函数调用约定。所谓函数调用约定,其实是主调和被调函数间的协议,该协议事先约定好函数参数以什么顺序依次压栈,以及函数调用结束后由谁来完成对入栈参数的清理。__stdcall和__cdecl是其中两个最常用的调用约定,其中__stdcall是PASCAL默认的调用方式,而__cdecl则为C/C++所默认。在使用VC进行编程时,我们可以在工程属性--C/C++--Advanced--Calling Convention中对该编译选项进行改变和设置。

如果我们定义一个C函数f并调用它:

我们使用__stdcall调用约定,编译器编译后产生如下汇编代码,对于主调函数:

可以看到,函数f的参数是按照从右到左的顺序依次压栈的,__stdcall和__cdecl两种调用约定在参数压栈顺序上是一致的。call汇编指令使eip跳转,并且它包含了将函数返回地址压栈的操作。

相应地,对于被调函数:

被调函数f开始执行前首先将ebp压栈,这是为了在f调用结束后重建主调函数栈框架。mov ebp, esp使得ebp指向当前函数(即被调函数f)的栈顶。因为对于被调函数f来说,它的栈目前是空的,所以栈底指针和栈顶指针相同。函数f内部需要为局部变量分配空间时,esp则会向下移动使得总是指向栈顶,而ebp不会在f的代码真正执行时发生改变。此时函数调用的栈框架如下图所示:

第三行和第四行才是真正完成加法运算的汇编代码。可以看到,对操作数的寻址都是通过ebp进行的。事实上,ebp向下的栈空间保存的都是当前函数的局部变量,而ebp向上则保存了当前函数的返回地址和压栈参数。在32位机器上指针占用4个字节内存空间,ebp + 0x08依次跳过第一行代码压栈的ebp值(4字节)和函数返回地址值(4字节),因而取出的就是a的值,a作为int型参数本身占用4字节,故ebp + 0x0C处存放的是另一个操作数b的值。

加法操作完成后,esp的值被ebp覆盖,也就是将当前函数的栈完全清空,而函数栈清空就意味着函数f的局部变量全部被销毁了(简单起见本例函数f中没有声明局部变量)。pop操作将函数f开始执行前压栈保存的ebp弹出,于是ebp重新回到了主调函数的栈框架中,esp指向了函数f的返回地址。ret使eip跳转至该返回地址,除此之外,它还做了一件很关键的事情--将esp向上移动8个字节,这8个字节中存储的正是压栈的参数a和b。这表明,__stdcall是由被调用者来负责清理栈参数的。

对于相同的C函数定义函数f,我们再来看看采用__cdecl时产生的汇编代码。对于主调函数:

对于被调函数:

看到不同了吗?主调函数多出来一条add esp, 8,这正是清理压栈参数的操作,而这个操作现在放在主调函数中执行了。被调函数ret少了操作数8,它现在只负责使eip跳转,不再有移动esp的操作。另外一方面,使用__cdecl时函数参数压栈顺序也是从右至左,这一点上文提到过。__cdecl是C/C++的默认调用约定,正是因为如此,C语言才能很好地支持不定参数声明。由于VC新建工程时默认的是__cdecl,所以想采用__stdcall时一定要在函数前显式指明。另外,WinDef.h头文件中还提供了#define CALLBACK    __stdcall 的定义,这里的callback就是我们常常听到的回调函数。

现在知道了__stdcall与__cdecl的不同了吧~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值