汇编语言 - 如何进行取模?

2023-11-23

x86 汇编中是否有类似模运算符或指令之类的东西?


如果您的模数/除数是已知常数,并且您关心性能,请参阅this and this。对于直到运行时才知道的循环不变值,乘法逆甚至是可能的,例如看https://libdivide.com/(但是如果没有 JIT 代码生成,这比仅对一个常量所需的步骤进行硬编码的效率要低。)

切勿使用div对于已知的 2 次方:much慢于and对于余数,或者右移对于除法。查看 C 编译器输出,了解无符号或有符号除以 2 的幂的示例,例如在 Godbolt 编译器资源管理器上。如果您知道运行时输入是 2 的幂,请使用lea eax, [esi-1] ; and eax, edi或者类似的事情要做x & (y-1)。 Modulo 256 甚至更高效:movzx eax, cl在最新的 Intel CPU 上具有零延迟(移动消除),只要两个寄存器是分开的。


在简单/一般情况下:运行时的未知值

The DIV操作说明(及其对应的IDIV对于有符号数)给出商和余数。对于无符号,余数和模是同一件事。供签署idiv,它给你余数(不是模数)可以为负数:
e.g. -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 by BX。参见英特尔的架构软件开发人员手册了解更多信息。

通常总是使用xor edx,edx未签名之前div将 EAX 零扩展为 EDX:EAX。这就是“正常”32 位/32 位 => 32 位除法的方法。

对于签名分割,use cdq before idiv to sign- 将 EAX 扩展为 EDX:EAX。也可以看看为什么在使用 DIV 指令之前 EDX 应该为 0?。对于其他操作数大小,请使用cbw(AL->AX),cwd(AX->DX:AX),cdq(EAX->EDX:EAX),或cqo(RAX->RDX:RAX) 将上半部分设置为0 or -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,或者直接使用立即数。


如果商不适合一个寄存器,div 和 idiv 将出错(AL / AX / EAX / RAX,与股息宽度相同)。这包括除以零,但也适用于非零 EDX 和较小的除数。这就是为什么 C 编译器只是进行零扩展或符号扩展,而不是将 32 位值拆分为 DX:AX。

还有为什么INT_MIN / -1是 C 未定义行为:它会溢出 2 的补码系统(如 x86)上的有符号商。看为什么整数除以 -1(负一)会得到 FPE?有关 x86 与 ARM 的示例。 x86idiv在这种情况下确实有错误。

x86 例外是#DE- 除法例外。在 Unix/Linux 系统上,内核向导致 #DE 异常的进程传递 SIGFPE 算术异常信号。 (在哪些平台上整数除以零会触发浮点异常?)

For div,使用股息high_half < divisor是安全的。例如0x11:23 / 0x12小于0xff所以它适合 8 位商。

可以通过使用一个块的余数作为下一个块的上半除数 (EDX) 来实现大数除以小数的扩展精度。这可能就是为什么他们选择余数 = EDX 商 = EAX,而不是相反。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

汇编语言 - 如何进行取模? 的相关文章

随机推荐