zoukankan      html  css  js  c++  java
  • 从汇编角度来理解递归工作栈的原理

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

      注意,阅读本文之前,请先复习一下汇编语言的常见指令。可以通过文章《几种基本汇编指令详解》进行简略复习。

      

      

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

      先是头三行。

    pushl %ebp  // 压栈操作,即 esp 先下移 4 个字节,然后读取 ebp 的值,并存入 esp 的内存单元中
    movl %esp, %ebp  // ebp = esp,栈顶指针等于栈底指针
    subl $4, %esp  // esp = esp - 4,即 esp 向下移动 4 个字节,相当于开辟了 1 个局部 int,同时默认了 ebp 为栈底

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

            

                     开辟前

      

          开辟后

      这样做的原因是,当 main 的全部指令执行完后,我们需要消除它的栈空间,并返回原来的状态,如何返回呢?通过保存 ebp = 100 这个信息。这也就是为什么要做上面这三个步骤。

      然后继续看下一条指令,“movl $8,(%esp)” ,将数值 8 放在 esp 的内存单元中。

      

          效果图

      接下来,程序利用指令 CALL,进入了被调用函数 f()。这里讲一下 call 指令的作用。常见的 CPU 的 CALL 指令(“调用”指令)的功能,就是以下两点:

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

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

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

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

      

          效果图

      接下来再来看看汇编版本 f() 函数的 12 到 14 行。

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

      第一行表示将 ebp + 8 地址单元中的值放入 eax 中,同时由上图可知 ebp+8 单元里的值实际上是 8。这儿的 8 又正好是C语言里 f(int x)的 传参。所以我们可以发现,在32位 X86 的情况下函数的参数传递是通过栈来实现的,更具体地说,参数传递是通过 bsp 向上位移 8 位找到第一个传入的参数(当然,也可能没参数,而且这里的次序“第一”不一定是从左往右,还可能是从右往左)。

      因此,我们在用 call 命令调用函数之前,会先把需要的参数存入栈中,然后再使用 call 命令将 eip 压栈。在进入新的函数后,把旧的 ebp 压栈,然后在新 ebp 的内存单元里存储了旧 ebp 的地址,所以我们可以通过新 ebp 的内存单元得到函数需要的参数值。

      接下来看第二行的指令。“movl %eax, (%esp)” ,会把 eax 的值放入 esp 所指向的内存单元。

      在第三行中,调用 g() 函数,,又可以压入 call 指令的下一条指令的地址,并将子程序的地址送入 PC,开始执行 g() 的片段。

      

          效果图

      

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

      

      

      然后看第三条指令。“movl 8(%ebp), %eax” ,将 ebp + 8 内存单元里的值存储在 eax 中,相当于是将传参 8 存在了 eax 里。

      第四条指令是立即数寻址,相当于 eax = 3 + eax,此时 eax = 11。

      第五条指令,“popl %ebp”,将栈顶的元素取出并赋值给寄存器 ebp,此时 ebp 变成了 72,这个值也是上一个函数 f() 中 ebp 的值。

      得到下图:

      

      然后 ret 执行。ret 执行时会把栈顶元素弹到 eip 中,即把在这里 leave 的地址弹到 eip 中,这样就可以执行 leave 指令了。

      执行leave 前的结果图是如下。

      

      leave 指令类似“movl %ebp, %esp”同时加上“popl %ebp”,起到撤销当前这一层堆栈的作用。

      由已知,ebp = 72 中存取的值是 84,这又是上一个的旧 ebp 的值。弹出 ebp 后,得到下图。

      

      

      此时遇到了f() 的 ret 指令,所以弹出 addl 到 eip,开始执行“addl $1, %eax”,由于之前 eax = 11,所以现在变成了 12。

      然后碰到了f() 的 leave 指令,弹出,达到清栈的目的。效果图如下:

      

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

    总结

      在每一个函数的执行过程中,都会有一段从 ebp 到 esp 的独立栈空间。对于一个函数,ebp 内存单元里的值是调用这个函数的上一个函数的栈空间的 ebp 的值。这种机制使得 leave 指令可以清空一个函数的栈、达到调用之前的状态。

      由于在进入一个新的栈之前,有一个 eip 压栈的过程,所以 leave 指令之后的 ret 指令正好对应了上一个函数的返回地址,也就是返回上一个函数时要执行的下一条指令的地址。

      另外,由于对于一个函数的栈空间来说,在三十二位的环境下,当前 ebp 上移一个内存单元(四字节)是上一个函数的返回地址,上移两个单元(八字节)是当前函数的传参的地址。所以我们知道了当前 ebp 位置的话,就可以通过栈的机制来获得参数。

    ————全心全意投入,拒绝画地为牢
  • 相关阅读:
    PyCharm设置中文字体
    pycharm中设置鼠标滚动放大和缩小页面
    cas5.3.2单点登录-自定义登录页面(十四)
    P1616疯狂的采药
    P2430严酷的训练
    P1164小A点菜
    P1015回文数
    P2871 手链
    《学习OpenCV》课后习题解答6
    《学习OpenCV》课后习题解答5
  • 原文地址:https://www.cnblogs.com/Bw98blogs/p/7594542.html
Copyright © 2011-2022 走看看