(栈帧和函数调用二)_stdcall和_cdecl的区别

(栈帧和函数调用二)_stdcall和_cdecl的区别

     通过上文的介绍,我们大致知道了函数调用时实际发生了什么,以及为什么我们使用编译器调试代码时,可以通过栈回溯看到整个调用的流程。从这样的信息里能够发现一个现象,那就是函数的调用方和被调用方对函数如何调用有这统一的理解,例如,它们双方都一致认同函数参数是按照某个固定的方式压入栈内的,如果不这样,那么函数无法取得正确的参数,这样的约定称为函数调用惯例。

一,函数调用惯例

1, 函数参数的传递顺序和方式
     函数参数的传递方式有很多种,最常见的也就是栈传递。函数的调用方将参数按照一定顺序压入栈中,函数自身再按照该顺序从栈中取出参数,参数压栈的顺序分为从左至右和从右至左。有些调用惯例还允许使用寄存器传递函数参数以提升性能(本章不讨论该情况)

2, 栈的维护方式
     调用方将参数压入栈中,函数从栈中取出参数,并执行完函数体后,需要将压入栈中的参数进行弹出,以使得栈在函数调用前后保持一致,而这个操作既可以由函数调用方来完成,也可以由函数自身来完成。

3, 函数名字修饰方式
     使用不同调用修饰符修饰的函数,会对函数名字进行不同策略的修饰,比如_cdecl修饰的函数名字直接在名称前加1个下划线(int fun(int a,int b) =>_fun),_stdcall修饰的函数名字是由下划线+函数名+@+参数的字节数(int fun(int a,int b) =>_fun@8)

二,_cdecl

     在c/c++语言中,默认的函数调用规则是cdecl,任何一个没有显示指定调用规则的函数默认都是使用cedecl修饰的,它将参数以 从右至左的顺序压入栈中,出栈操作也是由函数调用方来执行的。
以以下函数为例:

int _cdecl fun(int a, int b)
{
	return 1;
}

     Fun(1,2),调用处的反汇编代码为:
在这里插入图片描述
     先将参数2入栈,再将参数1入栈,再执行函数体,函数体内反汇编为:
在这里插入图片描述
     我们只用看最后一条指令ret,是直接返回到函数调用处的,也就是push 1下面的那个栈地址,参数a和b的栈空间并没有被fun函数自己清理掉,而是被调用方清理了,再回过去看上上图的最后一行汇编代码:

add esp,8

     将栈顶指针正向偏移8个字节,刚好将a参数和b参数所占用的8个字节给回收了(32位环境)

三,_stdcall

     Stdcall是从pascall发展来的一种函数调用方式,它不是c/c++默认的调用方式,我们使用的时候需要显示加上_stdcall修饰符。它将参数以从右至左的顺序压入栈中,出栈操作也是由函数内部来执行的。
以以下函数为例:

int _stdcall fun(int a, int b)
{
	return 1;
}

     Fun(1,2),调用处的反汇编代码为:
在这里插入图片描述
     先将参数2入栈,再将参数1入栈,再执行函数体,函数体内反汇编为:
在这里插入图片描述
     直接看最后一个ret指令,可以看到参数为8,也就是说函数体内自动将传入的int a,int b两个参数的栈空间给回收了,跳转了函数返回地址的正向偏移8字节的位置。

四,总结

1, 微软使用__stdcall来定义Win32 API,因为理论上__stdcall比较节省空间,两者比较计算如下:
__stdcall消耗的空间是:
ret X(X是一个地址,在32位系统为4字节),这样就是1字节(ret)+4字节(地址)=5字节
__cdecl消耗的空间是
(被呼叫者)ret
(呼叫者)add esp,x 这样就是(ret)1字节+add esp(1字节)+X(4字节)=6字节

2, 可变参数的函数只能用_cdecl而不能用_stdcall,是因为_stdcall修饰的函数参数所占用的栈空间是由函数自身来清理的,对于可变参数的情况,函数是不确定有多少入参的,不能准确清理栈空间。

3, 使用_stdcall还是_cdecl是由函数调用方和函数方两者结合使用场景共同决定的,只要双方统一了调用规定,那么使用就是没问题的(不要认为dll中只能用_stdcall)

参考文献:
1,《程序员的自我修养:链接、装载与库》第十章第二节

发布了78 篇原创文章 · 获赞 79 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/bajianxiaofendui/article/details/102853865
今日推荐