栈论 : 递归与栈式访问,如何用栈实现所有递归操作(函数调用底层篇)

2020-09-27 10:26:25 浏览数 (1)

重大错误说明 : 栈顶的指针始终是指向最后一个入栈元素的位置的,不是最后一个入栈元素的位置上面!请读者留意 (PS : 后来又看了一下,好像也不是什么大问题...)

上一篇 :

栈论 : 递归与栈式访问,如何用栈实现所有递归操作(基础知识篇)

2.函数调用底层篇(了解递归调用的硬件实现)

一开始,main函数没有调用add之前他的栈帧如下图,当然,下面只是简略介绍,实际上内存布局比下面更复杂(省略了寄存器等)。

当要调用add函数的时候main 将 自己的变量拷贝后压入栈中,我们称之为“形参”

上图中变量c 和变量d的拷贝就是所谓的”形参“

接下来将main函数的ebp地址压入栈中保存,以便add函数调用完之后恢复main在内存中的栈帧

接着 就是重要的环节,add函数的栈帧创建,add函数的栈帧创建在add函数自己的操作里。

没想到吧?add函数的栈帧是add函数自己创建的。一般的思维都是父对象为子对象创建空间,再让子对象自己发挥,可能这是比较袒护孩子的行为吧,你看函数调用却是让自己的孩子去开创天地,值得学习。(当然 这是win10下汇编的得出的结果,可能不同系统不一样)

add函数本身操作 :

1.将esp 的值赋给ebp,这里的ebp就是add函数自己栈帧的栈底了。

2.让esp = esp - X ; X是一个位移量,表示esp要上移,esp上移的这个位移量差不多是add函数栈帧的大小。(还有一些寄存器之类的会占用空间,忽略不计)

如图:

这时候的栈应该是这样的

接下来,涉及到最重要环节!栈帧之间的通信

add函数的内部操作是 两个数相加,这两个数是形参,难道在add函数的栈帧中要访问在main函数栈帧中的形参吗?没错,就是直接访问。

我们来看看a b 的汇编过程

对汇编不了解的同学可以先把 eax理解成一个变量,这个变量不在内存中(当然也就不在我们的栈区中)。mov是放进去的意思,理解把逗号右边的值放到(赋给)左边变量上(eax)去。 add是把逗号左右两边的数加起来,放到左边去。

我们发现,a b 无非是把 ebp 8, ebp 12(十六进制数0Ch的十进制数)读取到的值加起来并且放到eax变量里而已。

而从 ebp 8 和 ebp 12 读取到的正好是main函数栈帧中的形参

栈帧通信总结1.

子函数直接调用父函数栈帧内的形成,访问父函数

这是子向父索求信息,那么父向子索取信息呢?聪明的你可能已经猜到了,返回值!

子函数返回过程:

子函数完成之后,子函数的栈帧会被废弃掉

上面大圈里的小圈,两句汇编就是把栈顶和栈底移动回原来的main栈帧处。

在我们刚刚看到的a b之后,子函数已经没什么大动作了,也就是说我们操作完之后的数是放在eax里的。

父函数就是通过访问子函数结束后遗留在eax中的数来和子函数通信,也就是说,eax里的是子函数的返回值!

从汇编也可以看到main在调用完add函数之后,为e赋值的时候直接访问了eax;

add    esp,8

这句还是要好好说一说的,子函数返回之后esp还在形参的上面,既然子函数完成了,形参也没必要存在,于是需要把他们废弃掉,废弃的方法是把他们移除esp和ebp之间,也就是让esp下降就好了。

栈帧通信总结2.

父函数直接访子函数在EAX中遗留的返回值

综上,我们得出以下几点结论。

1.子函数直接调用父函数栈帧内的形成,访问父函数

2.父函数直接访子函数在EAX中遗留的返回值

3.父函数调用子函数,子函数创建栈帧,子函数完成后子函数的栈帧销毁

下一篇 :

栈论 : 递归与栈式访问,如何用栈实现所有递归操作(幼儿园题目篇)

护眼绿:

没人看的结语:

首先很感谢你看到这里,辛苦了。

文章中某些地方可能不正确或不准确,代码也可能不够高效可读,希望读者能够帮忙指正,共同学习进步。

0 人点赞