递归工作栈

寄存器与栈的关系

  这里必须提及寄存器与栈的概念,这里以c和汇编的程序为例。图片来源于博主Casualet

  

  

  为什么用汇编去分析呢?因为汇编更为底层,能够深入操作系统的内容。这儿给出了C与汇编的对比,很明显有两个调用函数和一个main函数,首先对main函数的汇编进行分析,先是头三行:

pushl %ebp  //压栈,压入一个栈顶元素并存到寄存器ebp中
movl %esp, %ebp  //ebp=esp,栈顶指针赋给ebp
subl $4, %esp  //esp=esp-4,即esp向下移动4个单位,相当于开辟了1个局部int,同时默认了ebp为栈底

  这三条用于保存栈的信息,ebp寄存器指向栈底,esp寄存器指向栈顶,栈底是高地址而栈底是低地址。执行完这三行后,栈就为main开辟了一个新空间,新空间从ebp开始到esp结束。开辟前与开辟后寄存器的位置关系如下图:

        

                 开辟前

  

      开辟后

  当main执行完后,我们需要消除它的栈,并返回原来的状态,如何返回呢?通过保存ebp=100这个信息。所以我们返回ebp=100,esp=88。这也就是为什么要做上面这三个步骤。然后继续,movl  $8,(%esp) ,将数值8放在esp所指向的位置。

  

      效果图

  接下将调用函数f,这时候会将EIP寄存器压入栈(EIP用来存储CPU要读取指令的地址), eip指向的是call的下一条指令,,addl $1, %eax(eax是X86汇编语言上cpu通用的寄存器名称,是32位寄存器,用来暂时存储数值或地址),随后进入函数并执行头三行:

pushl %ebp
movl %esp, %ebp
subl $4, %esp

  

      效果图

  

movl    8(%ebp) , %eax
movl    %eax , (%esp)
call    g

  继续看调用函数f中的代码,第一行表示将ebp+8地址所在位置的值放入eax中,而由图解可知ebp+8的值实际上是8。这儿的8又正好是C语言里f(int x)的参数传递。所以,在32位X86的情况下,函数的参数传递是通过栈来实现的。我们在用call命令调用函数之前,先把需要的参数压入栈,然后再使用call命令将eip压栈,然后进入新的函数后,旧的ebp压栈,新的ebp指向的位置存了这个刚压栈的旧的ebp,所以我们可以通过新的ebp指向的位置,通过计算得到函数需要的参数值。接下来,movl  %eax, (%esp),会把eax的值放入esp所指向的内存的位置,然后调用 g函数,,又可以压栈call指令的下一条指令的地址。

  

      效果图

  

  进行g函数,执行前两条指令,得到的结果如下:

  

  

  第三条指令,movl  8(%ebp), %eax ,与之前的代码一致,将ebp+8位置的值存储在eax中。第四条指令,将eax+3,此时eax = 11。第五条指令,popl  %ebp,将栈顶的那个数取出并存入到ebp寄存器中,ebp变成了72,因为这个时候esp执行的位置存放的值就是72,而这个值也是上一个函数中ebp的值。所以得到下图:

  

  然后ret执行,会把leave的地址弹到eip中,就可以执行leave 指令了,得到的图是:

  

  leave 指令类似一条宏指令, 等价于movl %ebp, %esp  popl %ebp。由已知,ebp=72中存取的值是84,这又是上一个的旧ebp的值,所以继续leave,弹出,得到下图:

  

  

  这一步后,又遇到了一次ret,开始执行addl  $1,%eax,由于之前的eax=11,所以现在变成了12。然后又碰到了leave指令,弹出,达到清栈的目的。效果图如下:

  

  于是栈恢复了状态。此时main中2还剩下一条ret指令,由于之前一开始我们没考虑过main的地址压栈,所这部分问题留给操作系统。

总结

  一个函数的执行过程,会有自己的一段从ebp 到esp的栈空间。对于一个函数,ebp指向的位置的值是调用这个函数的上一个函数的栈空间的ebp的值, 这种机制使得leave指令可以清空一个函数的栈、达到调用之前的状态。由于在这个栈设置之前,有一个eip压栈的过程,所以leave 以后的ret正好对应了上一个函数的返回地址,也就是返回上一个函数时要执行的指令的地址,另外,由于对于一个函数的栈空间来说,ebp指向的位置存了上一个ebp的值, 再往上是上一个函数的返回地址,再往上是上一个函数压栈传参数的值,所以我们知道了自己的当前ebp,就可以通过栈的机制来获得参数。

  

还将介绍具体实例hanio towel(非尾递归),未完待续

时间: 2024-08-02 13:19:04

递归工作栈的相关文章

C#=>递归反转栈

原理,递归反转栈 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 6 namespace StacksReverse 7 { 8 public class Program 9 { 10 public void Main(string[] args) 11 { 12 StacksReverses stacksReverses = ne

函数递归与栈的关系

首先通过反汇编语言,我们来了解一下最简单的递归函数与栈之间的关系. 如何获得反汇编语言,在visual studio 2008中,在debug环境下,在debug/windows/disassembly中可以查看反汇编之后的语言.现在我们看一下阶乘n!的实现 其C语言实现代码如下 [cpp] view plaincopy #include <stdio.h> int factorial(int n); int main(void) { int fact; fact = factorial(4)

39.递归颠倒栈

http://zhedahht.blog.163.com/blog/static/25411174200943182411790/ 分析:乍一看到这道题目,第一反应是把栈里的所有元素逐一pop出来,放到一个数组里,然后在数组里颠倒所有元素,最后把数组中的所有元素逐一push进入栈.这时栈也就颠倒过来了.颠倒一个数组是一件很容易的事情.不过这种思路需要显示分配一个长度为O(n)的数组,而且也没有充分利用递归的特性. 我们再来考虑怎么递归.我们把栈{1, 2, 3, 4, 5}看成由两部分组成:栈顶

Python旅途——函数的递归和栈的使用

Python--函数之递归.栈的使用 今天主要和大家分享函数的递归,同时引入一个新的概念--栈 1.递归 1.定义 函数的递归指的就是函数自己调用自己,什么是函数自己调用自己呢?我们来看一个栗子: 这里给大家一个数学中的一个数列:斐波那契数列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,它指的是前两项的和等于第三项,那么我们对于这

进一步讨论递归函数——递归与栈

递归函数,在函数的执行函数中,需多次进行自我调用.那么,递归函数是如何执行的?先看任意两个函数之间进行调用的情形.用函数和被调用函数[若在函数A中调用了函数B,则称函数A为调用函数,称函数B为被调用函数.]之间的链接及信息交换需通过栈来进行.在上一篇递归函数的讲解中主要对递归的定义和一些应用进行了介绍,最近学习了一点数据结构的知识,看到了递归函数的工作原理其实使用栈来实现的我才恍然大悟.知识学多了,真的可以连接到一起的. 通常,当在一个函数的运行期间调用另一个函数时,在运行被调用函数之前,系统需

NYOJ305 表达式求值(递归or栈)

题目意思: http://acm.nyist.net/JudgeOnline/problem.php?pid=305 Dr.Kong设计的机器人卡多掌握了加减法运算以后,最近又学会了一些简单的函数求值,比如,它知道函数min(20,23)的值是20 ,add(10,98) 的值是108等等.经过训练,Dr.Kong设计的机器人卡多甚至会计算一种嵌套的更复杂的表达式. 假设表达式可以简单定义为: 1. 一个正的十进制数 x 是一个表达式. 2. 如果 x 和 y 是 表达式,则 函数min(x,y

打印完整的递归调用栈

之前在写0-1背包问题的递归解法时,想要弄出完整的递归栈.尝试了使用debug工具手工追踪并画出调用栈,发现太麻烦了,又试了一下使用visual studio的code map功能,发现对于递归,它只会显示递归函数不断调用自己,并不会自动展开成为树的形式.所以我就使用了最简陋的办法,就是自己写了一个类,依赖C++的constructor和destructor来自动将栈输入到一个vector中,并且在main函数结束的地方添加一个语句将其内容输出到文件中. 这里使用了一些C++11的特性,我使用m

只使用递归实现栈的逆序操作

2017-06-23 20:36:02 刚开始思考这个问题的时候陷入了一个误区,就是以为只能用一个递归完成. 然而事实上,可以使用两个递归来完成这个功能. 具体思考是这样的,首先递归其实是将问题规模缩小了,每次处理更小的问题.针对这题来说,更小的问题应该是去除底部那个数后的逆序再加上最后的数. 当然,你可能会问,什么不是去掉最上面的数,然后将它再放到最前面,理由很简单,栈是一种后进先出的数据结构,这种类型的数据结构适合的是在尾部添加数据,而非在首部添加数据,所以更清晰的或者说更适合栈的操作是先把

二叉树遍历,递归,栈,Morris

一篇质量非常高的关于二叉树遍历的帖子,转帖自http://noalgo.info/832.html 二叉树遍历(递归.非递归.Morris遍历) 2015年01月06日 |  分类:数据结构 |  标签:二叉树遍历 |  评论:8条评论 |  浏览:6,603次 二叉树遍历是二叉树中最基本的问题,其实现的方法非常多,有简单粗暴但容易爆栈的递归算法,还有稍微高级的使用栈模拟递归的非递归算法,另外还有不用栈而且只需要常数空间和线性时间的神奇Morris遍历算法,本文将对这些算法进行讲解和实现. 递归