# 3.5.5 特殊的算术操作
正如我们在 2.3 节中看到的,两个 64 位有符号或无符号整数相乘得到的乘积需要 128 位来表示。x86-64 指令集对 128 位(16 字节)数的操作提供有限的支持。延续字(2 字节)、双字(4 字节)和四字(8 字节)的命名惯例,Intel 把 16 字节的数称为八字(oct word)。图 3-12 描述的是支持产生两个 64 位数字的全 128 位乘积以及整数除法的指令。
| 指令 | 效果 | 描述 |
|---|---|---|
imulq S | R[%rdx]:R[%rax] ← S × R[%rax] | 有符号全乘法 |
mulq S | R[%rdx]:R[%rax] ← S × R[%rax] | 无符号全乘法 |
cqto | R[%rdx]:R[%rax] ← 符号扩展(R[%rax]) | 转换为八字 |
idivq S | R[%rdx] ← R[%rdx]:R[%rax] mod S;R[%rax] ← R[%rdx]:R[%rax] ÷ S | 有符号除法 |
divq S | R[%rdx] ← R[%rdx]:R[%rax] mod S;R[%rax] ← R[%rdx]:R[%rax] ÷ S | 无符号除法 |
图 3-12 特殊的算术操作。这些操作提供了有符号和无符号数的全 128 位乘法和除法。一对寄存器 %rdx 和 %rax 组成一个 128 位的八字
imulq 指令有两种不同的形式。其中一种,如图 3-10 所示,是 IMUL 指令类中的一种。这种形式的 imulq 指令是一个“双操作数”乘法指令。它从两个 64 位操作数产生一个 64 位乘积,实现了 2.3.4 和 2.3.5 节中描述的操作 *u64 和 *t64。(回想一下,当将乘积截取到 64 位时,无符号乘和补码乘的位级行为是一样的。)
此外,x86-64 指令集还提供了两条不同的“单操作数”乘法指令,以计算两个 64 位值的全 128 位乘积——一个是无符号数乘法(mulq),而另一个是补码乘法(imulq)。这两条指令都要求一个参数必须在寄存器 %rax 中,而另一个作为指令的源操作数给出。然后乘积存放在寄存器 %rdx(高 64 位)和 %rax(低 64 位)中。虽然 imulq 这个名字可以用于两个不同的乘法操作,但是汇编器能够通过计算操作数的数目,分辨出想用哪条指令。
下面这段 C 代码是一个示例,说明了如何从两个无符号 64 位数字 x 和 y 生成 128 位的乘积:
#include <inttypes.h>
typedef unsigned __int128 uint128_t;
void store_uprod(uint128_t *dest, uint64_t x, uint64_t y) {
*dest = x * (uint128_t) y;
}
在这个程序中,我们显式地把 x 和 y 声明为 64 位的数字,使用文件 inttypes.h 中声明的定义,这是对标准 C 扩展的一部分。不幸的是,这个标准没有提供 128 位的值。所以我们只好依赖 GCC 提供的 128 位整数支持,用名字 __int128 来声明。代码用 typedef 声明定义了一个数据类型 uint128_t,沿用的 inttypes.h 中其他数据类型的命名规律。这段代码指明得到的乘积应该存放在指针 dest 指向的 16 字节处。
GCC 生成的汇编代码如下:
void store_uprod(uint128_t *dest, uint64_t x, uint64_t y)
dest in %rdi, x in %rsi, y in %rdx
1 store_uprod:
2 movq %rsi, %rax Copy x to multiplicand
3 mulq %rdx Multiply by y
4 movq %rax, (%rdi) Store lower 8 bytes at dest
5 movq %rdx, 8(%rdi) Store upper 8 bytes at dest+8
6 ret
可以观察到,存储乘积需要两个 movq 指令:一个存储低 8 个字节(第 4 行),一个存储高 8 个字节(第 5 行)。由于生成这段代码针对的是小端法机器,所以高位字节存储在大地址,正如地址 8(%rdi) 表明的那样。
前面的算术运算表(图 3-10)没有列出除法或取模操作。这些操作是由单操作数除法指令来提供的,类似于单操作数乘法指令。有符号除法指令 idivq 将寄存器 %rdx(高 64 位)和 %rax(低 64 位)中的 128 位数作为被除数,而除数作为指令的操作数给出。指令将商存储在寄存器 %rax 中,将余数存储在寄存器 %rdx 中。
对于大多数 64 位除法应用来说,除数也常常是一个 64 位的值。这个值应该存放在 %rax 中,%rdx 的位应该设置为全 0(无符号运算)或者 %rax 的符号位(有符号运算)。后面这个操作可以用指令 cqto1 来完成。这条指令不需要操作数——它隐含读出 %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
1 remdiv:
2 movq %rdx, %r8 Copy qp
3 movq %rdi, %rax Move x to lower 8 bytes of dividend
4 cqto Sign-extend to upper 8 bytes of dividend
5 idivq %rsi Divide by y
6 movq %rax, (%r8) Store quotient at qp
7 movq %rdx, (%rcx) Store remainder at rp
8 ret
在上述代码中,必须首先把参数 qp 保存到另一个寄存器中(第 2 行),因为除法操作要使用参数寄存器 %rdx。接下来,第 3~4 行准备被除数,复制并符号扩展 x。除法之后,寄存器 %rax 中的商被保存在 qp(第 6 行),而寄存器 %rdx 中的余数被保存在 rp(第 7 行)。
无符号除法使用 divq 指令。通常,寄存器 %rdx 会事先设置为 0。
练习题 3.12 考虑如下函数,它计算两个无符号 64 位数的商和余数:
void uremdiv(unsigned long x, unsigned long y,
unsigned long *qp, unsigned long *rp) {
unsigned long q = x / y;
unsigned long r = x % y;
*qp = q;
*rp = r;
}
修改有符号除法的汇编代码来实现这个函数。
1 在 Intel 的文档中,这条指令叫做 cqo,这是指令的 ATT 格式名字和 Intel 名字无关的少数情况之一。