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

66

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


2
https://dev59.com/Mm855IYBdhLWcg3wZzff - Robert Harvey
4个回答

105
如果你的模数/除数是已知常量,并且你关心性能,请参阅这个这个。即使是运行时未知的循环不变值,也可以使用乘法逆元,例如请参阅https://libdivide.com/(但是没有JIT代码生成,这比仅针对一个常量硬编码所需步骤更低效)。
永远不要使用div来处理2的幂次方:与其余数相比,它要慢得多,或者使用右移来进行除法。查看C编译器输出以获取2的幂次方的有符号或无符号除法示例,例如在Godbolt编译器资源管理器上。如果您知道运行时输入是2的幂次方,请使用lea eax,[esi-1]and eax,edi或类似的方法来执行x & (y-1)。模256更有效率:movzx eax,cl在最近的英特尔CPU上具有零延迟(mov-elimination),只要两个寄存器是分开的即可。

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

DIV指令(以及其有符号数的对应项IDIV)同时给出商和余数。对于无符号数,余数和模数是相同的。对于带符号的idiv,它会给出余数(而不是模数),这可能是负数:
例如 -5 / 2 = -2 rem -1。x86除法语义与C99的%运算符完全匹配。

DIV r32EDX: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。更多信息请参见Intel的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
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或直接使用立即数的方法。


这也是为什么在2的补码系统(如x86)上,INT_MIN / -1是C未定义行为:它会导致带符号商溢出。请参见为什么整数除以-1(负一)会导致FPE?,了解x86与ARM的示例。在这种情况下,x86的idiv指令确实会出错。

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

对于div,使用具有high_half < divisor的被除数是安全的。例如,0x11:23 / 0x12小于0xff,因此适合于8位商。

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

4
GCC不使用div操作因为它速度较慢:https://dev59.com/Mm855IYBdhLWcg3wZzff - Ciro Santilli OurBigBook.com
我扩展了这个内容,因为关于“div”的问题非常普遍,所以这看起来是一个很好的涵盖事物的地方。我认为示例仍然非常明显,可以快速给人们提供他们正在寻找的内容。 - Peter Cordes
有没有一种高效的方法可以使用x86汇编进行地板除法和规范模数(而不是余数)?例如,-2 // 5-1-2 %% 53(它们一起满足a %% b == a - ((a // b) * b)),而使用截断的除法和余数,我们有-2 / 50-2 % 5-2(它们一起满足a % b == a - ((a / b) * b))。 - Aaron Franke
@AaronFranke:除非某个东西的绝对值对于模数起作用,否则我不会立刻想到任何方法。也许对于一个常数除数来说,通常的乘法逆元实际上会更好地起作用。最好将其作为一个新问题提出(并从这里链接)。 - Peter Cordes

37
如果你需要对2的幂取模,使用按位与比除法更简单且通常更快。如果b是2的幂,则a % b == a & (b - 1)
例如,假设我们有一个在寄存器EAX中的值,对64取模。最简单的方法是AND EAX, 63,因为63在二进制中表示为111111。
掩码后的高位对我们不感兴趣。试一试!
类似地,与其使用2的幂进行MUL或DIV,位移才是正确的方法。但要注意带符号整数!

如果 b=0 呢? - LetsGoBrandon
@LetsGoBrandon 取模运算与除法类似,当 b=0 时未定义。 - Nirvana

4

0

如果您不太关心性能并希望使用简单直接的方法,您可以使用DIVIDIV

DIVIDIV只接受一个操作数,其中它将某个寄存器与此操作数进行除法运算,操作数只能是寄存器内存位置

当操作数为字节时: AL = AL / 操作数,AH = 余数(模数)。

例如:

MOV AL,31h ; Al = 31h

DIV BL ; Al(商)= 08h,Ah(余数)= 01h

当操作数为字时: AX = (AX) / 操作数,DX = 余数(模数)。

例如:

MOV AX,9031h ; Ax = 9031h

DIV BX ; Ax=1808h & Dx(余数)= 01h


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接