作为开发相信对函数的调用都会很感兴趣吧
下面通过汇编的角度来分析下
一、函数的入参和返回值
1 | assume cs:code ss:stack ds:data |
上面展示了一个函数入参的方式有哪些,一般的cpu都是少量参数通过寄存器传参,当参数大于一定个数的时候就会采用栈传递参数,各个cpu不一样。可以通过函数调用协议来制定参数传递的方式的,分别有__stdcall
、__cdecl
、__fastcall
,下面通过win32
来看看各个关键字有什么不同
__cdecl
1 | int __cdecl sum2(int a, int b) { |
运行查看汇编如下
1 | main函数汇编截取 |
可以看出__cdecl
参数的传递方式是通过栈传递,参数入栈的方式是从右到左一次传递,并且是外平栈,也是默认的方式
__stdcall
修改代码
1 |
|
汇编代码如下
1 | main函数 |
可以得出__stdcall
从右到左入栈,是内平栈
__fastcall
再修改代码
1 | int __fastcall sum2(int a, int b) { |
此时汇编
1 | main |
可以看出__fastcall
参数是放在寄存器中的
再修改下代码增加2个参数
1 | int __fastcall sum2(int a, int b,int c,int e) { |
汇编如下
1 | main |
可以看出__fastcall
当参数个数大于2时,参数会从右到左入栈,并且是内平栈
综上在win32上可以得出以下结论
概要 | __cdecl | __stdcall | __fastcall |
---|---|---|---|
参数传递方式 | 从右到左 | 从右到左 | 左边开始的两个不大于4字节(DWORD)的参数分别放在ECX和EDX寄存器,其余的参数自右向左压栈传送 |
栈清理方式 | 外平栈 | 内平栈 | 大于2个参数内平栈 |
场合 | C/C++ | WinApi | 性能要求高的场合 |
二、函数栈帧
所谓函数栈帧就是从进入函数,到出函数,内存的变化,也叫函数的执行环境,调用函数要保持堆栈平衡,所谓堆栈平衡就是当你调用函数开始,到调用函数结束,你的内存应该是没有变化的,从哪开始还是要回到那里,其实上面有些程序就已经贴出了完整的堆栈平衡的代码了,下面例子说明
- 申请一个大小为20个字节的栈空间
1 |
|
内存中的表现如下由于此时是空栈所以此时的sp指向0x07114
也就是下面的这个图
sum函数的作用是计算a
和b
的和,我们采用栈的方式传参数
增加下面代码
当程序执行到21行的时候内存如下
此时的sp如下
当执行到21行的时候也就是ret,相当于pop ip 等价于jmp 0011,所以发送2件事情
- 出栈0011
- jmp ip
所以程序执行到当时调用函数的下一行此时的sp为0x07110
我们发现一个问题,当你的sum函数调用完成后0x07113~0x07110
这段内存一直没有释放,当再有数据push进来sp又会-2这样内存会越来越少,所以我们写的函数是有问题的,为了解决这个问题,我们增加下面的代码
由于我们push参数导致栈sp减少了4个字节那么我们调用完函数主动回到sp之前的位置,这样堆栈就平衡了。
到这里我们还没有使用我们传入的参数,接下来接着开发代码使用传入的2个参数,按照刚才的理解只要将ss:sp的地址+2就可以了,很自然的想到是下面的写法
运行之后发现貌似不对,直接这样写是不可以的,这个时候需要借助一个新的寄存器bp
来暂时保存sp的值
运行之后发现确实保存到ax
、bx
中了
接下来我们实现a
+ b
了
增加代码,将计算的结果放到ax中
最后计算的结果为4466
现在我们的需求变了,当我们sum函数要变成下面的样子
1 | int sum(int a,int b) { |
也就是函数的内部多了2个局部变量,此时又该如何实现呢,做法是先给函数的局部变量申请一定的空间,比如我们这里申请10个字节的长度
这样就实现了,但是还有问题,我们一开始将sp-10了,那函数调用完毕,应该要恢复,由于bp保存着sp一开始的值所以
其实上面的程序还是有些瑕疵的,那就是bp,目前我们是只有一个函数,假如sum函数内部还要掉用其他的函数,那么bp就会指向新函数内部的sp,当新函数调用完成的时候返回到sum函数中,当执行到mov sp, bp的时候那么就会又到了新函数的内部了,那就乱套了,为了解决这个问题,方案如下,就是每次进入一个函数之前先保存旧值,调用完成后恢复
到这里距离我们完整的函数越来越近了,下面再来优化下,当初我们为局部变量申请的空间,我们会填充一些有意义的东西在win32里面填充的是CCCC
还有最后一个地方,当函数内部有用到需要用到的寄存器的时候也要先保护再恢复,套路都是一样的
至此整个一个完整的函数调用就完成了,我们将bp和sp之间的部分叫做某个函数的栈帧,网上关于函数栈帧的图片都大同小异,选了一张来自《深入理解计算机系统》