3.4.4 压入和弹出栈数据

3.4.4 压入和弹出栈数据

最后两个数据传送操作可以将数据压入程序栈中,以及从程序栈中弹出数据,如图 3-8 所示。正如我们将看到的,栈在处理过程调用中起到至关重要的作用。栈是一种数据结构,可以添加或者删除值,不过要遵循“后进先出”的原则。通过 push 操作把数据压入栈中,通过 pop 操作删除数据;它具有一个属性:弹出的值永远是最近被压入而且仍然在栈中的值。栈可以实现为一个数组,总是从数组的一端插入和删除元素。这一端被称为栈顶。

在 x86-64 中,程序栈存放在内存中某个区域。如图 3-9 所示,栈向下增长,这样一来,栈顶元素的地址是所有栈中元素地址中最低的。(根据惯例,我们的栈是倒过来画的,栈“顶”在图的底部。)栈指针 %rsp 保存着栈顶元素的地址。

指令 效果 描述
pushq S R[%rsp] ← R[%rsp] - 8;M[R[%rsp]] ← S 将四字压入栈
popq D D ← M[R[%rsp]];R[%rsp] ← R[%rsp] + 8 将四字弹出栈

图 3-8 入栈和出栈指令

pushq 指令的功能是把数据压入到栈上,而 popq 指令是弹出数据。这些指令都只有一个操作数——压入的数据源和弹出的数据目的。

将一个四字值压入栈中,首先要将栈指针减 8,然后将值写到新的栈顶地址。因此,指令 pushq %rbp 的行为等价于下面两条指令:

subq $8,%rsp          Decrement stack pointer
movq %rbp,(%rsp)      Store %rbp on stack

它们之间的区别是在机器代码中 pushq 指令编码为 1 个字节,而上面那两条指令一共需要 8 个字节。图 3-9 中前两栏给出的是,当 %rsp0x108%rax0x123 时,执行指令 pushq %rax 的效果。首先 %rsp 会减 8,得到 0x100,然后会将 0x123 存放到内存地址 0x100 处。

图 3-9 栈操作说明

弹出一个四字的操作包括从栈顶位置读出数据,然后将栈指针加 8。因此,指令 popq %rax 等价于下面两条指令:

movq (%rsp),%rax      Read %rax from stack
addq $8,%rsp          Increment stack pointer

图 3-9 的第三栏说明的是在执行完 pushq 后立即执行指令 popq %rdx 的效果。先从内存中读出值 0x123,再写到寄存器 %rdx 中,然后,寄存器 %rsp 的值将增加回到 0x108。如图中所示,值 0x123 仍然会保持在内存位置 0x100 中,直到被覆盖(例如被另一条入栈操作覆盖)。无论如何,%rsp 指向的地址总是栈顶。

因为栈和程序代码以及其他形式的程序数据都是放在同一内存中,所以程序可以用标准的内存寻址方法访问栈内的任意位置。例如,假设栈顶元素是四字,指令 movq 8(%rsp),%rdx 会将第二个四字从栈中复制到寄存器 %rdx