除法
在掌握了乘法之后,学习 DIV (无符号除法) 和 IDIV (有符号除法) 就会容易很多,因为它们的逻辑正好是乘法的逆过程。
除法指令在汇编中是出了名的“挑剔”,因为它对寄存器的要求非常死板,而且极易导致程序崩溃(核心转储)。
一、 除法的基本原理:双倍宽度
在除法中,被除数的位数通常是除数的两倍。 * 如果你除以一个 64 位数,被除数必须是 128 位。 * 结果会被拆分成两部分:商 (Quotient) 和 余数 (Remainder)。
二、 寄存器的固定分配
和 MUL 一样,你只需要提供除数,被除数是隐式写死的。
| 除数大小 (Operand) | 被除数 (固定组合) | 商 存放在 | 余数 存放在 |
|---|---|---|---|
| 8 位 (如 BL) | AX |
AL |
AH |
| 16 位 (如 BX) | DX:AX |
AX |
DX |
| 32 位 (如 EBX) | EDX:EAX |
EAX |
EDX |
| 64 位 (如 RBX) | RDX:RAX |
RAX |
RDX |
三、 DIV (Unsigned Division) —— 无符号除法
在使用 DIV 之前,有一个绝对不能忘的步骤:清空高位寄存器。
如果你想算 RAX / RBX,你不能直接运行 div rbx。因为 CPU 会去读 RDX:RAX。如果 RDX 里有残余的数据,被除数就会变得巨大无比。
正确写法:
mov rax, 100
mov rbx, 3
xor rdx, rdx ; 必须将 RDX 清零!相当于把被除数扩展为 128 位
div rbx ; 计算 RDX:RAX / RBX
; 结果:
; RAX = 33 (商)
; RDX = 1 (余数)
四、 IDIV (Signed Division) —— 有符号除法
有符号除法不能简单地把 RDX 清零,因为如果被除数是负数,RDX 必须填满 1(符号扩展)。
为了方便程序员,x86 提供了专门的指令来做这件事:
* CBW: 把 AL 扩展到 AX (8位 \(\to\) 16位)
* CWD: 把 AX 扩展到 DX:AX (16位 \(\to\) 32位)
* CDQ: 把 EAX 扩展到 EDX:EAX (32位 \(\to\) 64位)
* CQO: 把 RAX 扩展到 RDX:RAX (64位 \(\to\) 128位) —— 64位最常用
正确写法:
mov rax, -100
mov rbx, 3
cqo ; 自动根据 RAX 的符号位填充 RDX
; 此时 RDX 变成了 0xFFFFFFFFFFFFFFFF
idiv rbx
; 结果:
; RAX = -33
; RDX = -1
五、 除法最容易触发的两个“崩溃”
除法指令非常危险,一旦出错,操作系统会直接杀死你的程序:
-
除以 0 (Division by Zero): 这是最出名的错误。执行
div时如果除数为 0,程序立刻崩溃。 -
除法溢出 (Division Overflow): 如果商大到目标寄存器装不下,程序也会崩溃。
- 例子:用
AX除以BL。 - 如果
AX = 1000(0x03E8),BL = 2。 - 商应该是
500。 - 但是
500超过了AL能表示的最大值 (255)。 - 结果:CPU 抛出异常,程序崩溃。
- 例子:用
六、 总结与技巧
- 清空/扩展高位:
- 做
DIV之前,用xor rdx, rdx。 - 做
IDIV之前,用cqo。
- 做
- 获取余数:
汇编中没有专门的
%取模运算符。执行完div后,去RDX(或EDX/DX/AH) 里拿就行了。 - 寄存器保护:
记住
div会同时覆盖 RAX 和 RDX。如果你之前在 RDX 里存了什么重要的东西,一定要先push备份。
思考题:
如果你想快速计算 RAX / 2,除了用 DIV 指令,还有什么更高效的指令可以实现?(提示:和二进制位移有关)