Skip to content

除法

在掌握了乘法之后,学习 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


五、 除法最容易触发的两个“崩溃”

除法指令非常危险,一旦出错,操作系统会直接杀死你的程序:

  1. 除以 0 (Division by Zero): 这是最出名的错误。执行 div 时如果除数为 0,程序立刻崩溃。

  2. 除法溢出 (Division Overflow): 如果商大到目标寄存器装不下,程序也会崩溃。

    • 例子:用 AX 除以 BL。
    • 如果 AX = 1000 (0x03E8),BL = 2。
    • 商应该是 500。
    • 但是 500 超过了 AL 能表示的最大值 (255)。
    • 结果:CPU 抛出异常,程序崩溃。

六、 总结与技巧

  1. 清空/扩展高位:
    • 做 DIV 之前,用 xor rdx, rdx。
    • 做 IDIV 之前,用 cqo。
  2. 获取余数: 汇编中没有专门的 % 取模运算符。执行完 div 后,去 RDX (或 EDX/DX/AH) 里拿就行了。
  3. 寄存器保护: 记住 div 会同时覆盖 RAX 和 RDX。如果你之前在 RDX 里存了什么重要的东西,一定要先 push 备份。

思考题: 如果你想快速计算 RAX / 2,除了用 DIV 指令,还有什么更高效的指令可以实现?(提示:和二进制位移有关)