加载有效地址(load effective address)指令 leaq 实际上是 movq 指令的变形。虽然它的指令形式是从内存读取数据到寄存器,但实际上它根本没有引用内存。它的第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数(必须是一个寄存器)。这条指令可以为后面的内存引用产生指针,而且它还可以简洁地描述普通的算术操作。例如,如果寄存器 %rdx 的值为 x,那么指令“leaq 7(%rdx, %rdx, 4), %rax”将把寄存器 %rax 的值设置为 5x+7。
第二组中的操作数是一元操作,只有一个操作数,既是源又是目的。这个操作数可以是一个寄存器,也可以是一个内存位置。比如,指令“incq(%rsp)”会使栈顶的 8 字节元素加 1,类似于 C 语言中的自增运算符。
第三组是二元操作,其中,第二个操作数既是源又是目的。类似于 C 语言中的赋值运算符,如 x-=y。第一个操作数可以是立即数、寄存器或是内存位置,第二个操作数可以是寄存器或是内存位置。当第二个操作数是内存地址时,处理器必须从内存读出值,执行操作,再把结果写回内存。
最后一组是移位操作:先给出移位量,然后第二项给出的是要移位的数。移位量可以是一个立即数,或者放在特定的单字节寄存器 %cl 中。x86-64 中,移位操作对 w 位长的数据值进行操作,移位量是由 %cl 寄存器的低 m 位决定的,这里 2^m=w,高位则会被忽略。例如,当 %cl 寄存器的值为 0xFF 时,指令 salb 会移 7 位,salw 会移 15 位,sall 会移 31 位,而 salq 会移 63 位。移位操作的目的操作数可以是一个寄存器或是一个内存位置。
上面这些都属于常规的算术指令,其实还有一类特殊的算术操作。通常,两个 64 位有符号或无符号整数相乘得到的乘积需要 128 位(16字节,称为八字)来表示,x86-64 指令集对此提供了有限的支持,如下表所示。
注意,这里又出现了一个 imulq 指令,不过它是单操作数的,汇编器能够通过计算操作数的数目来分辨出想用哪条指令。单操作数的 imulq 指令和 mulq 指令分别是计算两个 64 位值的全 128 位乘积,前者计算补码乘法,后者计算无符号数乘法。它们都要求一个参数必须在寄存器 %rax 中,而另一个作为指令的源操作数给出,然后乘积存放在寄存器 %rdx(高 64 位)和 %rax(低 64 位)中。下面这段 C 代码说明了如何从两个无符号 64 位数字 x 和 y 生成 128 位的乘积。
#includetypedef unsigned __int128 uint128_t; void store_uprod(uint128_t *dest, uint64_t x, uint64_t y){ *dest = x * (uint128_t)y; }
这里的 unsigned __int128 类型是 GCC 提供的 128 位整数支持,改为 uint128_t 类型别名是为了沿用 inttypes.h 中其他数据类型的命名规律。最终的乘积存放在指针 dest 指向的 16 字节处。这段代码对应的汇编代码大致如下(对应的是小端法机器)。
; void store_uprod(uint128_t *dest, uint64_t x, uint64_t y) ; dest in %rdi, x in %rsi, y in %rdx store_uprod: movq %rsi, %rax ; Copy x to multiplicand mulq %rdx ; Multiply by y movq %rax, (%rdi) ; Store lower 8 bytes at dest movq %rdx, 8(%rdi) ; Store upper 8 bytes at dest+8 ret
除了 64 位乘法指令,这里还提供了之前的算术表里没有的除法和取模操作。它们也是单操作数的。其中,有符号除法指令 idivq 将寄存器 %rdx(高 64 位)和 %rax(低 64 位)中的 128 位数作为被除数,而将操作数作为除数,最终得出的商存储在寄存器 %rax 中,余数则存储在寄存器 %rdx 中。对于 64 位的被除数,其值应该存放在寄存器 %rax 中,而把 %rdx 的位设置为全 0(无符号运算)或者 %rax 的符号位(有符号运算)。后面这个操作可以用图中的指令 cqto 来完成,该指令不需要操作数,它会隐含读出 %rax 的符号位,并将它复制到 %rdx 的所有位。
下面这个 C 函数说明了 x86-64 是如何实现除法的,它计算了两个 64 位有符号数的商和余数。
void remdiv(long x, long y, long *qp, long *rp){ long q = x / y; long r = x % y; *qp = q; *rp = r; }
该函数对应的汇编代码大致如下。
; void remdiv(long x, long y, long *qp, long *rp) ; x in %rdi, y in %rsi, qp in %rdx, rp in %rcx remdiv: movq %rdx, %r8 ; Copy qp movq %rdi, %rax ; Move x to lower 8 bytes of dividend cqto ; Sign-extend to upper 8 bytes of dividend idivq %rsi ; Divide by y movq %rax, (%r8) ; Store quotient at qp movq %rdx, (%rcx) ; Store remainder at rp ret