【问题标题】:Assembly Language - How to do Modulo?汇编语言 - 如何取模?
【发布时间】:2011-12-22 17:37:25
【问题描述】:

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

【问题讨论】:

标签: assembly x86 modulo integer-division


【解决方案1】:

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

永远不要使用 div 来计算 2 的已知幂:它比 and 慢得多,或者右移来除法。查看 C 编译器输出以获取无符号或有符号除以 2 的幂的示例,例如on the Godbolt compiler explorer。如果您知道运行时输入是 2 的幂,请使用 lea eax, [esi-1] ; and eax, edi 或类似的事情 x & (y-1)。模 256 的效率更高:movzx eax, cl 在最新的 Intel CPU (mov-elimination) 上具有零延迟,只要两个寄存器是分开的。


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

DIV instruction(及其对应的有符号数 IDIV)同时给出商和余数。对于无符号数,余数和模数是一回事。对于签名的idiv,它会给你the remainder (not modulus),它可以是负数:
例如-5 / 2 = -2 rem -1。 x86 除法语义完全匹配 C99 的 % 运算符。

DIV r32 将EDX:EAX 中的 64 位数字除以 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 将 DX:AX 中的 32 位操作数除以 BX。请参阅英特尔的Architectures Software Developer’s Manuals 了解更多信息。

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

对于已签名的除法,在idiv 之前使用cdq 以签名-将 EAX 扩展到 EDX:EAX。另见Why should EDX be 0 before using the DIV instruction?。对于其他操作数大小,请使用 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。示例见8086 assembly on DOSBox: Bug with idiv instruction?。

有符号 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 未定义的行为:它在 x86 等 2 的补码系统上溢出有符号商。有关 x86 与 ARM 的示例,请参阅 Why does integer division by -1 (negative one) result in FPE?。 x86 idiv 在这种情况下确实出错了。

x86 异常是#DE - 除法异常。在 Unix/Linux 系统上,内核将 SIGFPE 算术异常信号传递给导致#DE 异常的进程。 (On which platforms does integer divide by zero trigger a floating point exception?)

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

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

【讨论】:

  • 但是GCC不使用div,因为它很慢:stackoverflow.com/questions/4361979/…
  • 我扩展了很多,因为关于div 的问题很常见,所以这看起来是一个很好的地方。我认为这个例子仍然很明显,可以让人们快速找到他们想要的东西。
【解决方案2】:

如果您以 2 的幂为模计算,则使用按位与比执行除法更简单且通常更快。如果b 是2 的幂,则a % b == a & (b - 1)。

例如,让我们在寄存器 EAX,模 64 中取一个值。
最简单的方法是AND EAX, 63,因为 63 在二进制中是 111111。

我们对蒙面的高位数字不感兴趣。试试看!

类似地,移位是要走的路,而不是使用具有 2 次方的 MUL 或 DIV。不过要小心有符号整数!

【讨论】:

  • 如果b=0 怎么办?
【解决方案3】:

如果你不太在意性能,想使用直接的方式,可以使用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

【讨论】:

    【解决方案4】:

    使用 Godbolt Compiler Explorer 可以轻松查看模数运算符在各种架构上的样子。

    https://godbolt.org/z/64zKGr

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-12-24
      • 1970-01-01
      • 2014-10-07
      • 1970-01-01
      • 2013-01-17
      • 2013-09-04
      • 1970-01-01
      相关资源
      最近更新 更多