x86 汇编中是否有类似模运算符或指令的东西?
4 回答
如果您的模数/除数是已知常数,并且您关心性能,请参阅this和 this。对于直到运行时才知道的循环不变值,甚至可以进行乘法逆运算,例如,请参阅https://libdivide.com/ (但没有 JIT 代码生成,这比仅硬编码所需的步骤效率低持续的。)
永远不要使用div
2 的已知幂:它比and
求余数或除法右移要慢得多。查看 C 编译器输出中的无符号或有符号除以 2 的幂的示例,例如在 Godbolt compiler explorer 上。如果您知道运行时输入是 2 的幂,请使用lea eax, [esi-1]
; and eax, edi
或类似的事情x & (y-1)
。Modulo 256 甚至更高效:只要两个寄存器是分开的,movzx eax, cl
在最近的 Intel CPU ( mov-elimination ) 上就具有零延迟。
在简单/一般情况下:运行时的未知值
该DIV
指令(及其对应IDIV
的有符号数)给出了商和余数。对于无符号数,余数和模数是一回事。对于 signed idiv
,它会为您提供可以为负数的余数(不是模数)
:
例如-5 / 2 = -2 rem -1
. x86 除法语义完全匹配 C99 的%
运算符。
DIV r32
将 64 位数字EDX:EAX
除以 32 位操作数(在任何寄存器或内存中)并将商存储在 中EAX
,余数存储在EDX
. 它在商溢出时出错。
无符号 32 位示例(适用于任何模式)
mov eax, 1234 ; dividend low half
mov edx, 0 ; dividend high half = 0. prefer xor edx,edx
mov ebx, 10 ; divisor can be any register or memory
div ebx ; Divides 1234 by 10.
; EDX = 4 = 1234 % 10 remainder
; EAX = 123 = 1234 / 10 quotient
在 16 位汇编中,您可以div bx
将 32 位操作数DX:AX
除以BX
. 有关详细信息,请参阅英特尔架构软件开发人员手册。
通常总是使用xor edx,edx
before unsigneddiv
将 EAX 零扩展为 EDX:EAX。 这就是你如何进行“正常”的 32 位 / 32 位 => 32 位除法。
对于有符号除法,使用cdq
beforeidiv
将EAX符号扩展为 EDX:EAX。另请参阅为什么在使用 DIV 指令之前 EDX 应该为 0?. 对于其他操作数大小,使用cbw
(AL->AX)、cwd
(AX->DX:AX)、cdq
(EAX->EDX:EAX) 或cqo
(RAX->RDX:RAX) 将上半部分设置为0
或-1
根据低半部分的符号位。
div
/idiv
可用于 8、16、32 和(在 64 位模式下)64 位的操作数大小。在当前的 Intel CPU 上,64 位操作数大小比 32 位或更小要慢得多,但 AMD CPU 只关心数字的实际大小,而不管操作数大小。
请注意,8 位操作数大小是特殊的:隐式输入/输出在 AH:AL(又名 AX)中,而不是 DL:AL。请参阅DOSBox 上的 8086 程序集:带有 idiv 指令的错误?例如。
有符号 64 位除法示例(需要 64 位模式)
mov rax, 0x8000000000000000 ; INT64_MIN = -9223372036854775808
mov ecx, 10 ; implicit zero-extension is fine for positive numbers
cqo ; sign-extend into RDX, in this case = -1 = 0xFF...FF
idiv rcx
; quotient = RAX = -922337203685477580 = 0xf333333333333334
; remainder = RDX = -8 = 0xfffffffffffffff8
限制/常见错误
div dword 10
不可编码为机器代码(因此您的汇编程序将报告有关无效操作数的错误)。
与mul
/不同imul
(您通常应该使用更快的 2 操作数imul r32, r/m32
或 3 操作数imul r32, r/m32, imm8/32
,而不是浪费时间编写高半结果),没有更新的操作码用于除以立即数或 32 位/32-位 => 32 位除法或余数,没有高半被除数输入。
除法是如此缓慢且(希望)很少见,以至于他们没有费心添加一种方法来让您避免 EAX 和 EDX,或者直接使用立即数。
如果商不适合一个寄存器(AL / AX / EAX / RAX,与被除数相同的宽度),则 div 和 idiv 将出错。这包括除以零,但也会发生在非零 EDX 和较小除数的情况下。这就是 C 编译器只是零扩展或符号扩展而不是将 32 位值拆分为 DX:AX 的原因。
还有为什么INT_MIN / -1
C 的行为是未定义的:它溢出了 2 的补码系统(如 x86)上的有符号商。请参阅为什么整数除以 -1(负一)会导致 FPE?对于 x86 与 ARM 的示例。在这种情况下, x86idiv
确实出错了。
x86 异常是#DE
- 除法异常。在 Unix/Linux 系统上,内核将 SIGFPE 算术异常信号传递给导致#DE 异常的进程。(在哪些平台上整数除以零会触发浮点异常?)
对于div
,使用与 的股息high_half < divisor
是安全的。eg0x11:23 / 0x12
小于0xff
所以它适合 8 位商。
一个大数除以一个小数的扩展精度除法可以通过使用一个块的余数作为下一个块的高半除数 (EDX) 来实现。这可能就是为什么他们选择了余数=EDX 商=EAX 而不是相反的原因。
如果您以 2 的幂为模计算,则使用按位与比执行除法更简单且通常更快。如果b
是 2 的幂,a % b == a & (b - 1)
.
例如,让我们在寄存器EAX 中取一个值,以 64 为模。
最简单的方法是AND EAX, 63
,因为 63 是二进制的 111111。
我们对蒙面的更高数字不感兴趣。试试看!
类似地,不是使用 MUL 或 DIV 的幂次方,而是使用位移位。不过要小心有符号整数!
查看模数运算符在各种架构上的样子的一种简单方法是使用 Godbolt Compiler Explorer。
如果你不太在意性能,想用直截了当的方式,可以使用DIV
或IDIV
。
DIV
或者IDIV
只取一个操作数,将某个寄存器与该操作数相除,操作数只能是寄存器或内存位置。
当操作数为字节时: AL = AL / 操作数,AH = 余数(模数)。
前任:
MOV AL,31h ; Al = 31h
DIV BL ; Al (quotient)= 08h, Ah(remainder)= 01h
当操作数是一个字时: AX = (AX) / 操作数,DX = 余数(模数)。
前任:
MOV AX,9031h ; Ax = 9031h
DIV BX ; Ax=1808h & Dx(remainder)= 01h