没有32位寄存器的32位/ 16位有符号整数除法?

时间:2019-03-30 08:48:13

标签: assembly x86-16 integer-division

我正在尝试将32位有符号整数除以16位有符号整数,以获得有符号32位商和16位余数。

我的目标是没有fpu的286。

我过去已经写过代码来进行32位无符号除法:

DIV32 PROC

;DIVIDES A 32-BIT VALUE BY A 16-BIT VALUE.

;ALTERS AX
;ALTERS BX
;ALTERS DX

;EXPECTS THE 32-BIT DIVIDEND IN DX:AX
;EXPECTS THE 16-BIT DIVISOR IN BX

;RETURNS THE 32-BIT QUOTIENT IN DX:AX
;RETURNS THE 16-BIT REMAINDER IN BX

    push di
    push si


    mov di, ax ;di -> copy of LSW of given dividend
    mov ax, dx ;ax -> MSW of given dividend
    xor dx, dx ;dx:ax -> 0:MSW  
    div bx     ;ax:dx -> ax=MSW of final quotient, dx=remainder

    mov si, ax ;si -> MSW of final quotient
    mov ax, di ;dx:ax -> dx=previous remainder, ax=LSW of given dividend
    div bx     ;ax:dx -> ax=LSW of final quotient, dx=final remainder  
    mov bx, dx ;bx -> final remainder
    mov dx, si ;dx:ax -> final quotient


    pop si
    pop di
    ret

DIV32 ENDP 

到目前为止,我已经尝试做显而易见的事情,只是通过将XOR DX, DX换成CWD并将DIV换成IDIV来修改我现有的代码:< / p>

IDIV32 PROC

;DIVIDES A SIGNED 32-BIT VALUE BY A SIGNED 16-BIT VALUE.

;ALTERS AX
;ALTERS BX
;ALTERS DX

;EXPECTS THE SIGNED 32-BIT DIVIDEND IN DX:AX
;EXPECTS THE SIGNED 16-BIT DIVISOR IN BX

;RETURNS THE SIGNED 32-BIT QUOTIENT IN DX:AX
;RETURNS THE 16-BIT REMAINDER IN BX

    push di
    push si


    mov di, ax ;di -> copy of LSW of given dividend
    mov ax, dx ;ax -> MSW of given dividend
    cwd        ;dx:ax -> 0:MSW, or ffff:MSW  
    idiv bx    ;ax:dx -> ax=MSW of final quotient, dx=remainder

    mov si, ax ;si -> MSW of final quotient
    mov ax, di ;dx:ax -> dx=previous remainder, ax=LSW of given dividend
    idiv bx    ;ax:dx -> ax=LSW of final quotient, dx=final remainder  
    mov bx, dx ;bx -> final remainder
    mov dx, si ;dx:ax -> final quotient


    pop si
    pop di
    ret

IDIV32 ENDP 

这适用于某些计算,例如-654,328 / 2 = -327164(0xfff60408 / 2 = fffb0204)。但是它不适用于某些输入,-131,076 / 2返回-2余数0的错误结果。除数为1或-1似乎导致除法误差,而与除数无关。

我已经测试了许多不同的正,负红利和除数,以试图找到某种形式的正确和错误结果,我注意到它无法正确返回-65538的结果。

我有一种预感,我应该根据输入条件有条件地使用CWD,但是似乎XOR DX, DX经常返回错误的结果。当除数和除数均为正且商小于0x7fffffff时,都可以工作。

4 个答案:

答案 0 :(得分:3)

我不知道有什么算法可以将较大的负数分成几部分并为IDIV做准备。我将计算除数和除数的绝对值,使用函数DIV32并最后根据存储的符号处理结果:

IDIV32 PROC      ; DX:AX / BX = DX/AX rem BX
    ; 99 / 5   =  19 rem 4
    ; 99 / -5  = -19 rem 4
    ; -99 / 5  = -19 rem -4
    ; -99 / -5 =  19 rem -4

    mov ch, dh          ; Only the sign bit counts!
    shr ch, 7           ; CH=1 means negative dividend
    mov cl, bh          ; Only the sign bit counts!
    shr cl, 7           ; CL=1 means negative divisor

    cmp ch, 1           ; DX:AX negative?
    jne J1              ; No: Skip the next two lines
    not dx              ; Yes: Negate DX:AX
    neg ax              ; CY=0 -> AX was NULL
    cmc
    adc dx, 0           ; Adjust DX, if AX was NULL
    J1:

    cmp cl, 1           ; BX negative?
    jne J2              ; No: Skip the next line
    neg bx              ; Yes: Negate BX
    J2:

    push cx             ; Preserve CX
    call DIV32
    pop cx              ; Restore CX

    cmp ch, cl          ; Had dividend and divisor the same sign?
    je J3               ; Yes: Skip the next two lines
    not dx
    neg ax              ; CY=0 -> AX was NULL
    cmc
    adc dx, 0           ; Adjust DX, if AX was NULL
    J3:

    cmp ch, 1           ; Was divisor negative?
    jnz J4              ; No: Skip the next line
    neg bx              ; Negate remainder
    J4:

    ret
IDIV32 ENDP

答案 1 :(得分:2)

您的算法不能更改为简单地签名。

让我们以计算(+1)/(-1)为例:

(+ 1)/(-1)=(-1),余数0

在算法的第一步中,您将高位除以除数:

(+ 1)的高位为0,因此您正在计算:

0 /(-1)= 0,余数0

但是,整个32位除法的正确高位是0FFFFh,而不是0。第二个除法要求的提示也是0FFFFh,而不是0。

  

哦,所以第二个IDIV应该是DIV。好的,明天早上起床,我会对其进行测试。如果可以使用,我会添加一个答案。

第一部门已经没有产生想要的结果。因此,更改第二师将无济于事...

  

除数为1或-1会导致除法误差,与分红无关。

仅当设置了股息的第15位并且:

  • ...除以1或
  • ...除以-1,并设置了股息的低15位中的至少一位

在这些情况下,您正在划分:

  • ...在000008000h ... 00000FFFFh范围内的数字乘以1
    结果将在+ 08000h ... + 0FFFFh范围内
  • ...- 1范围为000008001h ... 00000FFFFh的数字
    结果将在-0FFFFh ...- 08001h
  • 范围内

...但是,结果是一个带符号的16位值,因此必须在-8000h ... + 7FFFh范围内。

我刚刚在运行DOS的虚拟机中尝试了12345678h /(+ 1)和12345678h /(-1):

未设置12345678h的第15位;两次我都没有得到除法错误。 (但除以-1会得出错误的结果!)

答案 2 :(得分:1)

使用2x idiv有一个基本问题:我们需要进行2除法以产生商的下半部分,该部分是无符号的,可以是0到0xffff之间的任何值。

只有一个多字整数的最高字包含符号位,所有低于该位的位均具有正位置值。 idiv的商范围是-2^15 .. 2^15-1,而不是0 .. 65535。是的,idiv可以产生所有必需的值,但不能从我们只能通过对第一除法结果进行简单修正而得到的输入中得出。例如,0:ffff / 1将导致idiv出现#DE异常,因为该商不适合 signed 16位整数。 / p>

因此,第二个除法指令必须div ,使用除数的绝对值和适当的高一半。 ({div要求两个输入都必须是无符号的,因此第一个idiv的有符号余数也是一个问题。)

也许仍然可以将idiv用于第一个除法,但只能对div之前的结果使用修正,此外,仍然必须采用除数和第一个除法的绝对值,其余部分用于填充未签名的div。这是一种有趣的可能性,但是实际上,保存和重新应用未签名的分区周围的符号会更便宜。

正如@Martin所指出的,+1 / -1与天真idiv的第一除法给出了错误的高半商(0 / -1 = 0而不是-1),并且第二次输入错误除法(0%-1 = 0,而不是-1)。待办事项:弄清楚实际需要什么修正。也许只是有条件的+ -1,但我们知道余数的大小不能大于除数,因为high_half < divisor对于div而言是必要且足够的,不会出错。

您的-131,076 / 2 = -2(可能是巧合)仅以其结果的一半减去1:
应该是0xfffefffe = -2:-2而不是-1:-2。


@rkhb函数的优化版本,内嵌DIV32。

我们记录输入符号,然后对绝对值进行无符号除法,然后再恢复符号。 (如果不需要余数,我们可以简化;商数仅取决于xor dividend,divisor

或者如果股息足够小,我们可以使用一个idiv 。但是,我们必须避免-2^15 / -1溢出情况,因此快速检查DX作为AX的符号扩展的情况不仅会丢失某些安全情况(具有较大的除数),还会尝试该不安全情况。如果数量很少(例如大多数计算机程序中的情况),那么基于cwd的快速测试可能仍然是一个好主意,在绝对值计算之后进行另一个测试。

分支在286上价格便宜,因此我主要保留分支,而不是使用无分支abs()。 (例如,对于单个寄存器:使用cdq(或sar reg,15)/ xor / sub,like compilers make,基于-x = ~x + 1的2的补码身份)。而且mov / neg / cmovl当然要等到P6家族才可用。如果您需要与286兼容,但主要关心现代CPU的性能,则可以选择不同的选择。但是事实证明,32位无分支ABS的代码大小比分支小。但是,对于正输入,它可能会比分支慢一些,在某些情况下,这些指令将被跳过。 Assembler 8086 divide 32 bit number in 16 bit number有一个有趣的想法,即为被除数和除数创建0 / -1整数,然后为了以后重新应用这些符号,您可以将这些符号XOR在一起,并使用相同的XOR / SUB 2的补码bithack进行符号翻转结果与否。

样式:本地标签(在函数内)以@@为前缀。我认为这对TASM来说是正常的,例如NASM .label本地标签。

   ; signed 32/16 => 32-bit division, using 32/16 => 16-bit division as the building block
   ; clobbers: CX, SI
IDIV32 PROC      ; DX:AX / BX = DX/AX rem BX
;global IDIV32_16       ; for testing with NASM under Linux
;IDIV32_16:
    ; 99 / 5   =  19 rem 4
    ; 99 / -5  = -19 rem 4
    ; -99 / 5  = -19 rem -4
    ; -99 / -5 =  19 rem -4

    mov   cx, dx          ; save high half before destroying it

;;; Check for simple case
    cwd                   ; sign-extend AX into DX:AX
    cmp   cx, dx          ; was it already correctly sign-extended?
    jne   @@dividend_32bit

    ; BUG: bx=-1  AX=0x8000 overflows with #DE
    ; also, this check rejects larger dividends with larger divisors
    idiv  bx
    mov   bx, dx
    cwd
    ret

;;; Full slow case: divide CX:AX by BX
   @@dividend_32bit:
    mov   si, ax                ; save low half
    mov   ax, cx                ; high half to AX for first div

                                ; CH holds dividend sign
    mov   cl, bh                ; CL holds divisor sign

 ;;; absolute value of inputs
    ; dividend in  AX:SI
    cwd                         ; 0 or -1
    xor   si, dx                ; flip all the bits (or not)
    xor   ax, dx
    sub   si, dx                ; 2's complement identity: -x = ~x - (-1)
    sbb   ax, dx                ; AX:SI = abs(dividend)

    test  bx, bx          ; abs(divisor)
    jnl  @@abs_divisor
    neg   bx
   @@abs_divisor:

 ;;; Unsigned division of absolute values
    xor   dx, dx
    div   bx             ; high half / divisor
    ; dx = remainder = high half for next division
    xchg  ax, si
    div   bx
 ;;; absolute result: rem=DX  quot=SI:AX
    mov   bx, dx
    mov   dx, si


 ;;; Then apply signs to the unsigned results
    test  cx,cx          ; remainder gets sign of dividend
    jns  @@remainder_nonnegative
    neg   bx
  @@remainder_nonnegative:

    xor   cl, ch         ; quotient is negative if opposite signs
    jns  @@quotient_nonnegative
    neg   dx
    neg   ax             ; subtract DX:AX from 0
    sbb   dx, 0          ; with carry
  @@quotient_nonnegative:

    ret
IDIV32 ENDP

优化:

  • 使用x86的内置Sign Flag(从结果的MSB设置),可以更简单地进行符号保存和符号测试,如果SF == 1,则跳js。避免将符号位下移到8位寄存器的底部。可以使用xor / jns来测试相同符号,因为相同符号将“取消”并使SF = 0不管是0还是都为1。 (通常,XOR可用于比较相等性,但通常只有在您关心一位而不关心其他位的情况下,这种方式才有用。)

  • 请避免自己编写CH,这是为了在这种情况下进行部分寄存器重命名的现代Intel CPU的好处。此函数永远不会与其他ECX分开重命名。 (在像286这样的旧式CPU上,mov cx,dxmov ch,dh没有缺点。)我们还避免读取高8个部分寄存器(例如test cx,cx而不是test ch,ch),因为这在最近的Intel Sandybridge系列CPU上具有更高的延迟。 (How exactly do partial registers on Haswell/Skylake perform? Writing AL seems to have a false dependency on RAX, and AH is inconsistent)。在P6系列中,写入低8位部分寄存器会将其与完整寄存器分开重命名,因此最好在写入任何内容后只读取8位部分寄存器。

    当然,在现代CPU上,cx 之类的16位寄存器都是局部寄存器,即使在16位模式下(因为那里有32位寄存器),所以{ {1}}对ECX的旧值有错误的依赖性。


在386 +

很明显,在386+上,可以使用32位寄存器/操作数大小,即使在16位模式下,您也可以使用它:

mov cx,dx

这可以从BX = 0到#DE,或者从DX:AX = -2 ^ 31和BX = -1(;; i386 version ;; inputs: DX:AX / BX shl edx, 16 mov dx, ax ; pack DX:AX into EDX mov eax, edx movsx ebx, bx ; sign-extend the inputs to 32 bit EBX cdq ; and 64-bit EDX:EAX idiv ebx ; results: quotient in EAX, remainder in EDX mov ebx, edx ; remainder -> bx mov edx, eax sar edx, 16 ; extract high half of quotient to DX ;; result: quotient= DX:AX, remainder = BX )溢出时


测试工具:

NASM包装器可以从32位模式调用

LONG_MIN/-1

C程序,编译为32位并与asm链接:

%if __BITS__ = 32
global IDIV32
IDIV32:
    push   esi
    push   ebx
    push   edi      ; not actually clobbered in this version
    movzx  eax, word [esp+4  + 12]
    movzx  edx, word [esp+6  + 12]
    movzx  ebx, word [esp+8  + 12]
    call   IDIV32_16

    shl    edx, 16
    mov    dx, ax
    mov    eax, edx

    movsx  edx, bx       ; pack outputs into EDX:EAX "int64_t"

    pop    edi
    pop    ebx
    pop    esi
    ret
%endif

(这在#include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <limits.h> // returns quotient in the low half, remainder in the high half (sign extended) int64_t IDIV32(int32_t dxax, int16_t bx); static int test(int a, short b) { // printf("\ntest %d / %d\n", a, b); int64_t result = IDIV32(a,b); int testrem = result>>32; int testquot = result; if (b==0 || (a==INT_MIN && b==-1)) { printf("successfully called with inputs which overflow in C\n" "%d/%d gave us %d rem %d\n", a,b, testquot, testrem); return 1; } int goodquot = a/b, goodrem = a%b; if (goodquot != testquot || goodrem != testrem) { printf("%d/%d = %d rem %d\t but we got %d rem %d\n", a,b, goodquot, goodrem, testquot, testrem); printf("%08x/%04hx = %08x rem %04hx\t but we got %08x rem %04hx\n" "%s quotient, %s remainder\n", a,b, goodquot, goodrem, testquot, testrem, goodquot == testquot ? "good" : "bad", goodrem == testrem ? "good" : "bad"); return 0; } return 1; } int main(int argc, char*argv[]) { int a=1234, b=1; if(argc>=2) a = strtoll(argv[1], NULL, 0); // 0x80000000 becomes INT_MIN instead of saturating to INT_MAX in 32-bit conversion if(argc>=3) b = strtoll(argv[2], NULL, 0); test(a, b); test(a, -b); test(-a, b); test(-a, -b); if(argc>=4) { int step=strtoll(argv[3], NULL, 0); while ( (a+=step) >= 0x7ffe) { // don't loop through the single-idiv fast path // printf("testing %d / %d\n", a,b); test(a, b); test(-a, -b); test(a, -b); test(-a, b); } return 0; } } int之间草率,因为我只关心它在相同类型的x86 Linux上运行。)

使用

编译
int32_t

运行 nasm -felf32 div32-16.asm && gcc -g -m32 -Wall -O3 -march=native -fno-pie -no-pie div32-test.c div32-16.o ,以除数= -2检验从该值到0x7ffe(步长= -1)的所有股息。 (对于./a.out 131076 -2 -1-a / -b等的所有组合)

我没有为商和除数做嵌套循环;您可以使用外壳来做到这一点。我也没有做任何聪明的事情来测试最高点附近和范围底部附近的一些股息。

答案 3 :(得分:0)

我重新编写了idiv32程序,以便它将负股息或除数取反为正/无符号形式,执行无符号除法,如果股息XOR除数为true,则取反商。

编辑:使用jsjns而不是针对80h的位掩码进行测试。不要打扰返回剩余。余数应该共享分红的符号,但是由于我真的不需要余数,因此我不会为使程序正确处理而烦恼。

idiv32 proc

;Divides a signed 32-bit value by a signed 16-bit value.

;alters ax
;alters bx
;alters dx

;expects the signed 32-bit dividend in dx:ax
;expects the signed 16-bit divisor in bx

;returns the signed 32-bit quotient in dx:ax

push cx
push di
push si

    mov ch, dh      ;ch <- sign of dividend
    xor ch, bh      ;ch <- resulting sign of dividend/divisor

    test dh, dh     ;Is sign bit of dividend set?  
    jns chk_divisor ;If not, check the divisors sign.
    xor di, di      ;If so, negate dividend.  
    xor si, si      ;clear di and si   
    sub di, ax      ;subtract low word from 0, cf set if underflow occurs
    sbb si, dx      ;subtract hi word + cf from 0, di:si <- negated dividend
    mov ax, di      
    mov dx, si      ;copy the now negated dividend into dx:ax

chk_divisor:
    xor di, di
    sbb di, bx      ;di <- negated divisor by default
    test bh, bh     ;Is sign bit of divisor set?
    jns uint_div    ;If not, bx is already unsigned. Begin unsigned division.
    mov bx, di      ;If so, copy negated divisor into bx.

uint_div:
    mov di, ax      ;di <- copy of LSW of given dividend
    mov ax, dx      ;ax <- MSW of given dividend
    xor dx, dx      ;dx:ax <- 0:MSW  
    div bx          ;ax:dx <- ax=MSW of final quotient, dx=remainder

    mov si, ax      ;si <- MSW of final quotient
    mov ax, di      ;dx:ax <- dx=previous remainder, ax=LSW of given dividend
    div bx          ;ax:dx <- ax=LSW of final quotient, dx=final remainder
    mov dx, si      ;dx:ax <- final quotient

    test ch, ch     ;Should the quotient be negative?
    js neg_quot     ;If so, negate the quotient.
pop si              ;If not, return. 
pop di
pop cx
    ret

neg_quot:
    xor di, di      
    xor si, si
    sub di, ax
    sbb si, dx
    mov ax, di
    mov dx, si      ;quotient negated
pop si
pop di
pop cx
    ret

idiv32 endp