_stdcall与_cdel及_fastcall区别(以及什么是平衡堆栈)

最近忙着实习,好久没写博文了,赶紧来补一个。
我们在写c/c++程序时都会用到函数,那么函数在调用时怎么保存参数,怎么执行调用呢。
CISC(如x86)机器上由于寄存器个数有限,参数使用堆栈来保存。而保存参数的顺序,在哪里平衡堆栈(清理参数)的约定称作calling convention(调用方法)。
c/c++上我们可以在函数开头通过_cdel _stdcall _fastcall等标记来指定函数的调用方法。c/c++默认的调用方法_cdel。

linux开发时也许不会注意到,但做windows programming的童鞋肯定知道,windows有很多宏
WINAPI CALLBACK PASCALL APIENTRY(我举的这几个实际上的定义都是_stdcall)指定的就是函数的调用方法。
win api默认的调用约定是_stdcall所以掉win api时需要加上WINAPI宏。

现在我们依次来看看_cdel _stdcall _fastcall:
_cdel 实际上是c declaration的缩写,顾名思义是c语言的默认调用,他的法则是:参数从右向左依次入栈,调用函数者平衡堆栈(清理参数)
比如我们掉了一个函数

void foo(0x1234,0x5678); //foo接受两个int参数,返回void。

他的汇编代码是:(注:这里只能叫做汇编伪代码,我只是拿来说明下原理,具体的汇编代码根据实际情况会有稍许差别)

调用者代码:
push 5678h
push 1234h
call foo
add esp,8

foo内部代码

//void foo(int,int)
push ebp
mov ebp,esp
dosomething...
mov esp,ebp
pop ebp
ret

我们可以忽略foo的汇编代码,只看调用者代码。我们看到参数的入栈顺序是从右向左和参数列表刚好相反的,而调用者在call完foo函数时,讲堆栈指针往回移了8刚好是两个int的空间,相当于把之前压入的参数弹出了。(见下注)

注:这里还要说明一下,程序的堆栈一般是在程序内存空间的尾部,从高地址向低地址储存,所以push入栈,则esp值减少,pop出栈则esp值增加,和我们的习惯可能有点出入。所以add esp是说从堆栈里拿走东西。再有,pop/push的操作数是32位,则esp加减4,是16位则esp加减2。我们调用函数之前放进堆栈了两个32位参数。执行完之后add esp,8就相当于把那两个push进去的参数拿走,等同于将两个参数pop出来之后丢弃。将参数从堆栈中拿走这样调用前和调用后堆栈的大小及堆栈里的内容是一样的,如果将调用前和调用后的堆栈放在一个天平上,天平是平衡的,所以这个动作也形象的叫做平衡堆栈。

现在再来看看_stdcall它是windows api的默认调用方式,他和_cdel的区别在于它把平衡堆栈的操作交给被调用的函数了。
用_stdcall调用同样的函数

void _stdcall foo(0x1234,0x5678); //foo是一个空函数,只接受两个int参数,什么都不做。

他的汇编伪代码:

调用者代码:
push 5678h
push 1234h
call foo

注意,平衡堆栈的那段代码没了

//void foo(int,int)
push ebp
mov ebp,esp
dosomething...
mov esp,ebp
pop ebp
ret 8

ret后面多了一个立即操作数8,它的作用就是平衡堆栈。
所以_stdcall和_cdel的区别就在于平衡堆栈由调用者完成还是由被调用的函数完成。

_fastcall顾名思义fast,一般是将两个参数放入寄存器来执行函数调用,大于两个参数依然是放在堆栈中处理的,具体处理方法看编译。

历史上还有_pascal的调用方法,用来模拟pascall语言的函数调用,他的调用规则和_stdcall类似,只是参数的入栈顺序是从左到右的。

Comments

  1. 讲解的太少了。

    我觉得既然讲调用约定就要讲printf这种参数个数不定的函数。
    printf只能使用__cdecl约定

    ReplyDelete
  2. 我只是解释下几种调用约定的顺序,没想展开来讲,我相信看得懂这篇文章的人也一定懂得函数堆栈的结构,了解堆栈的结构就知道为什么printf只能用__cdecl约定约定了。

    ReplyDelete

Post a Comment

Popular posts from this blog

socket close shutdown函数区别

批量在文件头插入

hash表取模技巧