函数堆栈

栈是一个很重要的编程概念(编译课和程序设计课都讲过相关内容),与编译器和编程语言有紧密的联系。理解调用栈最重要的两点是:栈的结构,EBP 寄存器的作用。一个函数调用动作可分解为:零到多个 PUSH 指令(用于参数入栈),一个 CALL 指令。CALL 指令内部其实还暗含了一个将返回地址(即 CALL 指令下一条指令的地址)压栈的动作(由硬件完成)。几乎所有本地编译器都会在每个函数体之前插入类似如下的汇编指令:

pushl   %ebp
movl   %esp , %ebp

这样在程序执行到一个函数的实际指令前,已经有以下数据顺序入栈:参数、返回地址、ebp 寄存器。由此得到类似如下的栈结构(参数入栈顺序跟调用方式有关,这里以 C 语言默认的 CDECL 为例):

+|  栈底方向        | 高位地址
 |    ...        |
 |    ...        |
 |  参数3        |
 |  参数2        |
 |  参数1        |
 |  返回地址        |
 |  上一层[ebp]    | <-------- [ebp]
 |  局部变量        |  低位地址

图 7 函数调用栈结构

这两条汇编指令的含义是:首先将 ebp 寄存器入栈,然后将栈顶指针 esp 赋值给 ebp。“mov ebp esp”这条指令表面上看是用 esp 覆盖 ebp 原来的值,其实不然。因为给 ebp 赋值之前,原 ebp 值已经被压栈(位于栈顶),而新的 ebp 又恰恰指向栈顶。此时 ebp 寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原 ebp 入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的 ebp 值。

一般而言,ss:[ebp+4]处为返回地址,ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用 4 字节内存),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层 ebp 值。由于 ebp 中的地址处总是“上一层函数调用时的 ebp 值”,而在每一层函数调用中,都能通过当时的 ebp 值“向上(栈底方向)”能获取返回地址、参数值,“向下(栈顶方向)”能获取函数局部变量值。如此形成递归,直至到达栈底。这就是函数调用栈。

提示:练习 5 的正确实现取决于对这一小节的正确理解和掌握。

results matching ""

    No results matching ""