递归工作栈的原理 汇编理解

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

  

  

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

pushl %ebp  //压栈,压入一个栈顶元素并存到寄存器ebp中
movl %esp, %ebp  //将ebp内存单元中的数据传给esp,让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,利用到call指令,这里讲一下call指令的作用。常见的CPU的CALL指令(“调用”指令)的功能,就是以下两点:

  (1)将下一条指令的所在地址(即当时程序计数器PC的内容)入栈

  (2)将子程序的地址送入PC(即开始执行子程序)

  这时候会将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函数,执行前两条指令,得到的结果如下:

  

  

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

  

 

  然后ret执行,ret执行时会把栈顶元素弹到eip中,即把在这里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,就可以通过栈的机制来获得参数。

 

转载于:https://www.cnblogs.com/Bw98blogs/p/7594542.html

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值