栈
每个运行的计算机程序都使用称为栈的内存区域来使函数正常工作。机器使用栈来传递函数参数,存储返回信息,保存寄存器以供以后恢复以及用于局部变量,为单个函数调用分配的栈部分称为栈帧。换句话说,对于每个函数调用,栈上都会创建新的空间(即栈帧)。
计算机的栈位于内存的最高地址。栈的顶部从高地址向低地址增长。我们使用push S
将S
压入栈,使用pop D
从栈中删除顶部值并将其放入目标(即寄存器或内存位置)。使用栈顶寄存器%esp
作为指向栈顶部的指针。
调用函数
在执行函数之前,程序将函数的所有参数以它们记录的相反顺序压入堆栈。然后程序发出一个调用指令,指示希望它启动哪个函数,call
指令做了两个事情:
- 首先它将下一条指令的地址,即返回地址,压入堆栈。
- 然后,它修改指令指针
%eip
指向函数的开头。
栈看来像下面这样;
Argument N ... Argument 2 Argument 1 Return address <--- (%esp)
编写函数
编写函数基本上包含三个部分:设置,设置栈帧执行任务,清理。设置和清理步骤是相同的。
设置
在设置过程,会立即执行以下两条指令。
pushl %ebp
movl %esp, %ebp
第一条指令保存当前的%ebp
基址寄存器,基址寄存器是一个特殊的寄存器,用于访问函数参数和局部变量。栈帧由两个指针分隔:%ebp
和%esp
,%ebp
指向栈帧的底部,%esp
指向栈帧的顶部。每个函数调用都有自己的栈帧,这意味着当我们完成被调用函数时,我们需要恢复调用者的基址指针寄存器%ebp
,因此我们需要保存当前的%ebp
。
完成保存调用者的%ebp
后,我们可以通过执行movl %esp, %ebp
来设置当前栈帧的%ebp
,这样做的原因是,我们现在可以访问调用者函数之前作为固定索引从基址寄存器指针压入栈帧的函数参数。我们不能直接使用栈指针来访问参数,因为栈指针可以在函数执行时移动。
此时,栈如下所示
Argument N <--- N*4+4(%ebp) ... Argument 2 <--- 16 (%ebp) Argument 1 <--- 12(%ebp) Return address <--- 4(%ebp) Old %ebp <--- (%esp) and (%ebp)
设置栈帧执行
一旦完成设置操作,就可以使用栈帧了。
保存寄存器
我们需要按照约定将被调用者保存寄存器压入堆栈,按照惯例,寄存器%rax,%rdx,%rcx
被归类为调用者保存寄存器,而%rbx,%rbp,%r12~%r15
被归类为被调用者保存寄存器。调用者保存寄存器意味着调用者函数负责保存这些寄存器值,因为被调用者可以自由地覆盖这些寄存器值。另一方面,被调用者保存寄存器意味着被调用者函数必须在覆盖它们之前通过将它们压入栈中来保存这些寄存器值,并在返回之前恢复它们,因为调用者可能需要这些值用于未来的计算。
保存寄存器这个步骤不是强制的,如果调用函数不使用这些被调用者保存寄存器,可以跳过这一步。
局部变量
该函数在栈帧上为它需要的任何局部变量保留空间。没有指定初始值的数据空间可以通过简单地将栈指针递减适当的量来在栈上分配。类似的,我们可以通过增加指针来释放空间。假设我们需要两个字的内存来运行一个函数,我们可以简单地将栈指针向下移动两个字来提供空间。
subl $8, %esp # Allocate 8 bytes of space on the stack
虽然可以在函数体中根据需要在栈上腾出空间,但在函数开始时一次性分配这些空间通常更有效。
假定我们保存了%ebx
(被调用者保存寄存器),并使用了两个字用于本地存储,我们的栈帧看起来像这样。
Argument N <--- N*4+4(%ebp) ... Argument 2 <--- 12 (%ebp) Argument 1 <--- 8(%ebp) Return address <--- 4(%ebp) Old %ebp <--- (%esp) and (%ebp) %ebx <--- -4(%ebp) Local variable1 <--- -8(%ebp) Local variable2 <--- -12(%ebp) and (%esp)
清理
当一个函数执行完毕后,它会做以下几件事。
- 它将返回值存储在
%eax
。 - 它通过向栈指针添加相同数量的值来释放它们分配的栈空间。
add $8, %esp
- 它弹出之前保存的寄存器
popl %ebx
- 它将堆栈重置为调用时的状态(它摆脱当前栈帧并使调用者的栈帧重新生效)
- 它将控制权返回到调用它的地方。这是使用
ret
命令完成的。该指令弹出栈顶的任何值,并将%eip
设置为该值。
因此,从函数中返回需要执行以下命令
movl %ebp, %esp # Set stack pointer back to the beginning of the frame
popl %ebp # Restore the caller's base pointer and now stack pointer pointing to Return address
ret # Since stack pointer pointing to return address, we can now call ret