访问信息
压入和弹出栈数据
最后两个数据传送操作可以将数据压入程序栈中,以及从程序栈中弹出数据,如 表-1 所示。栈在处理过程调用中起着至关重要的作用。栈(stack)是一种数据结构,可以添加或者删除值,不过要遵循“先进后出”的原则(FILO)。通过 push 操作把数据压入栈中,通过 pop 操作删除数据;它具有一个属性:弹出的值永远是最近被压入且仍存在栈中的值。栈可以实现为一个数组,总是从一端插入和删除元素。这一段称作栈顶。在 x86-64 中,程序栈存放在内存中的某个区域。如 图-1 所示,栈向下增长,这样一来,栈顶元素的地址是所有栈中元素最低的。(根据惯例,我们的栈是倒过来的,栈“顶”在图的底部)栈指针 %rsp 保存着栈顶元素的地址。
| 指令 | 效果 | 描述 |
|---|---|---|
| pushq S | R[%rsp]←R[%rsp]-S; M[R[%rsp]]←S | 将四字压入栈 |
| popq D | D←M[R[%rsp]]; R[%rsp]←R[%rsp]+S | 将四字弹出栈 |
pushq 指令的功能是把数据压入到栈上,而 popq 指令是弹出数据。这些指令都只有一个操作数——压入的数据源和弹出的数据目的。
将一个数据入栈,首先要将栈指针减去 8,然后将值写入到新的栈顶地址。因此,指令 pushq %rbp 的行为等价于下面两条指令:1
2subq $8, %rsp ; Decrement stack pointer
movq %rbp, (%rsp) ; Store %rbp on stack
它们的区别是在机器码中,pushq 指令编码为 1 个字节,而上面的两条代码一共需要 8 个字节。下图给出的是,当 %rsp 为 0x108,%rax 为 0x123 时,执行 pushq %rax 的效果。首先 %rsp 会减 8,得到 0x100,然后会将 0x123 存放到 0x100 处。
而弹出一个四字的操作包括从栈顶位置读出数据,然后将栈指针加 8。因此,popq %rax 等价于下面两条指令:1
2movq (%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 (%rsp), %rdx 会将第二个四字从栈中复制到寄存器 %rdx。
算数和逻辑操作
表-2 列出了 x86-64 的一些整数和逻辑操作。大多数操作都分成了指令类,这些指令类带有各种带不同大小操作数的变种(只有 leaq 没有其他大小的变种)。例如,ADD 由四条加法指令组成:addb、addw、addl 和 addq,分别是字节加法、字加法、双字加法和四字加法。事实上,给出的每个指令类都有对这四种不同大小数据得指令。这些操作被分为四组:加载有效地址、一元操作、二元操作和以为。二元操作有两个操作数,而一元操作有一个操作数。这些操作数的描述方法与之前提到的一致。
| 指令 | 效果 | 描述 |
|---|---|---|
| leaq S, D | $D ← \&S$ | 加载有效地址 |
| INC D | $D ← D + 1$ | 加 1 |
| DEC D | $D ← D - 1$ | 减 1 |
| NEG D | $D ← -D$ | 取负 |
| NOT D | $D ← \, \sim D$ | 取补 |
| ADD S, D | $D ← D + S$ | 加 |
| SUB S, D | $D ← D - S$ | 减 |
| IMUL S, D | $D ← D * S$ | 乘 |
| XOR S, D | $D ← D \wedge S$ | 异或 |
| OR S, D | $D ← D | S$ | 或 |
| AND S, D | $D ← D \& S$ | 与 |
| SAL k, D | $D ← D << k$ | 左移 |
| SHL k, D | $D ← D << k$ | 左移(等同于SAL) |
| SAR k, D | $D ← D >>_A k$ | 算术右移 |
| SHR k, D | $D ← D>>_L k$ | 逻辑右移 |
