**个人主页:欢迎大家光临——>**沙漠下的胡杨
** 各位大帅哥,大漂亮**
如果觉得文章对自己有帮助
可以一键三连支持博主
** 你的每一分关心都是我坚持的动力**
** **
☄:本期重点:我们今天讲解下函数栈帧的形成和销毁
** 希望大家每天都心情愉悦的学习工作**
函数栈帧讲解我们通过汇编的一些分析讲解,所以我们先了解下一些寄存器和汇编指令来进一步学习栈帧吧。
相关的寄存器:
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈低寄存器
esp:栈顶寄存器
eip:指令寄存器,保留当前指令的下一条指令地址。
相关的汇编命令:
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用 1.压入返回值 2.转入目标函数
jmp:通过修改eip,转入目标函数,进行调用
ret :恢复返回值地址,压入eip,类似pop eip命令
首先我们了解下C程序地址空间(VS版)
C程序地址空间的图如下:
不同的编译器可能方向不同,增长方向是一样的,必如栈都是向低地址去,堆区向高地址去。
接着我们看下函数调用的逻辑
main函数也是函数,是被谁调用的呢?
是被 __tmainCRTStartup()这个函数调用的,
然后这个函数 __tmainCRTStartup()又被 mainCRTStartup()这个函数调用
这个函数 mainCRTStartup() 是被操作系统进行直接调用的。
下面我们以一个简单的例子来进行理解:
int MyAdd(int x, int y) { int c = 0; c = x + y; return c; } int main() { int a = 0xA; int b = 0xB; int z = 0; z = MyAdd(a, b); printf("z = %x\n", z); return 0; }
很简单的一个函数,就是两数之和然后封装为一个函数来进行调用。
创建main函数栈帧
这是上面就是main函数的栈帧的创建,但是不重要。
分析main函数中的代码
代码进行汇编分析:
先看a变量,是把 0A的值赋给 ebp - 8这个空间,同理 b 和 z 也是。
看图解:
调用MyAdd函数前的准备:
我们看下调用函数前,汇编指令都做些什么吧。
首先是把ebp - 14h(变量b的值)的值放入eax中,其实就是b的值放入寄存器中,然后压入eax,同理压入ebp - 8(变量 a的值)。
看示意图,观察此时的寄存器的指向,最下面为内存布局:
上面的过程证明了:
1.函数调用前,就已经形成了临时变量。
2.形参实例化的顺序是从右向左的。
进行函数调用:
执行 call 命令,进入函数体内。
call要做的是 :
1:压入返回值 2.转入目标函数
为什么要压入返回值呢?因为函数可能会调用结束,那么就需要返回了。
压入返回值,就是压入 call 指令的下一条指令的地址。
看下压入前的内存和地址:
压入后内存,重点看是不是 call 的下一条指令的地址是否压入了:
如下图所示,确实已经压入啦
另外说一下,jmp 后的值,就是MyAdd函数的地址,就是跳转到该函数处。
所以eip的值也就由原来的 main函数值 变为 MyAdd函数的值啦。
看示意图:
开辟MyAdd的栈帧空间:
我们 jmp 之后就该进入函数中了,也就是执行函数啦,
我们先看下汇编:
这些汇编就是为MyAdd开辟战帧空间
首先逐步分析,push ebp,把ebp压入栈中,ebp就是main函数的栈低位置,mov 把 esp的地址移动到ebp中,ebp其实是MyAdd的栈顶,最后把 esp 的值减去0CC。其实就是把esp的当前位置向上移动,然后和 ebp 围成一段空间,为了MyAdd函数使用。
如图所示:
示意图为:
其中 ebp 和 esp围成的空间就是MyAdd函数的栈帧。
这是 esp 和 ebp 都指向了,main函数的栈顶,那么栈低指针呢?
不用担心main函数栈低的地址找不到,我们刚才把main函数栈低指针压入了MyAdd的栈帧空间中啦,到时候可以直接返回了。
分析MyAdd代码
还是先进行反汇编代码查看:
逐个分析:
mov ebp - 8, 0,其实就是在ebp -8的位置处赋值为0,接着把 ebp + 8的值放入eax中,再把ebp + 0C 的值和原来eax中的值相加,最后把eax中的值放入ebp - 8空间中去。
翻译下就是:
开辟空间 c 变量,初始化为0,把原来形参实例化中的a的值放入eax中,把形参实例化中的b也放进去与a相加,最后的结果在eax中,把eax的值放入变量 c 的空间中。这就是上述的汇编。下面看下示意图:
准备返回
说下返回值把,返回值其实是别放在了eax寄存器中进行了返回,所以eax的值,就是0xC
接着我们继续分析汇编:
分析下:首先是把变量 c 的值放入eax寄存器中,接着我们弹出edi,esi,ebx这些都不管,重点看下,mov esp ,ebp这句代码,这个就是把ebp的地址赋值给esp,相当于esp 和 ebp同时指向了MyAdd的栈低处,也就是释放了栈帧空间, 然后pop ebp,就是弹栈,ebp处是栈底,栈底就是main函数的栈底,也就是说pop后,ebp又回到了main函数的栈底,而esp就在call的下一条指令处啦。
如图所示:
ret之后
ret其实就是类似pop一样,就是把esp的值再向下移动
如图:
返回call的下一条指令处
我们ret之后返回到了call的下一条指令处
接着看汇编,把esp的值加 8 ,其实就是把所形成的临时变量销毁。
把eax的值移动到 ebp - 20,其实上就是把寄存器中0xC的值移动到变量z中去。
看示意图:
剩下的打印不在叙述啦。
剩下一个有意思的证明:
形参实例化时,是从左到右,连续排布的。
int MyAdd(int x, int y) { printf("Before:%d\n", y); *(&x + 1) = 100; printf("After: %d\n", y); return 0; }
本篇总结:
首先我们从汇编底层来看和了解了函数栈帧的创建和销毁,其中每个细节,每个汇编指令做出了解释和相对性的示意图,希望大家有所收获吧。
下期预告:
下期会更新可变参数列表的相管内容。
版权归原作者 沙漠下的胡杨 所有, 如有侵权,请联系我们删除。