Fork me on GitHub

深入理解《深入理解计算机系统》(四)

汇编基础,栈的使用

访问信息

压入和弹出栈数据

最后两个数据传送操作可以将数据压入程序栈中,以及从程序栈中弹出数据,如 表-1 所示。栈在处理过程调用中起着至关重要的作用。栈(stack)是一种数据结构,可以添加或者删除值,不过要遵循“先进后出”的原则(FILO)。通过 push 操作把数据压入栈中,通过 pop 操作删除数据;它具有一个属性:弹出的值永远是最近被压入且仍存在栈中的值。栈可以实现为一个数组,总是从一端插入和删除元素。这一段称作栈顶。在 x86-64 中,程序栈存放在内存中的某个区域。如 图-1 所示,栈向下增长,这样一来,栈顶元素的地址是所有栈中元素最低的。(根据惯例,我们的栈是倒过来的,栈“顶”在图的底部)栈指针 %rsp 保存着栈顶元素的地址。

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

表-1 入栈和出栈指令

pushq 指令的功能是把数据压入到栈上,而 popq 指令是弹出数据。这些指令都只有一个操作数——压入的数据源和弹出的数据目的。
将一个数据入栈,首先要将栈指针减去 8,然后将值写入到新的栈顶地址。因此,指令 pushq %rbp 的行为等价于下面两条指令:

1
2
subq $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
2
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 (%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$逻辑右移

表-2 整数算术操作。加载有效地址(leaq)指令通常用来执行简单的算术操作。其余的指令是更加标准的一元或二元操作。我们用 $>>_A$ 和 $>>_L$ 来分别表示算术右移和逻辑右移。注意,ATT格式的汇编代码中操作数的顺序与一般的直觉相反
0%