2

I know that one has to be very careful when dividing in assembly, i.e. doing this:

          mov ah, 10h
          mov al, 00h ; dividend = 1000h
          mov bl, 10h ; divisor = 10h
          div bl      ; Integer overflow exception, /result 100h cannot fit into al

I've written some probably non-gotcha-proof logic to create a more friendly environment for division:

          mov ah, 10h
          mov al, 00h
          mov bl, 10h 
TryDivide:
          cmp bl,ah
          jna CatchClause
          div bl
          clc
          jmp TryEnd
CatchClause:
          stc
TryEnd:
     

Does anyone have a clue as to the technical reasons that something like this wasn't implemented and we have exceptions instead of flags set / registers truncated ?

tinmanjk
  • 301
  • 1
  • 9
  • 2
    This question would probably be a better fit at [Retrocomputing Stackexchange](https://retrocomputing.stackexchange.com/). It seem off-topic here, as it is looking for the rationale for a hardware design decision. – njuffa Apr 09 '21 at 22:17
  • Someone successfully disassembled the 8086 microcode recently, but while their [blog post](https://www.reenigne.org/blog/8086-microcode-disassembled/) describing this effort mentions the `IDIV` instruction, it does not have any information that would shed light on the flag vs exception decision (such as lack of microcode ROM space). – njuffa Apr 09 '21 at 22:24
  • 2
    I think that check actually *is* gotcha-proof. `high_half < divisor` is the exact condition for the quotient to fit for unsigned division, including ruling out division by zero. (Signed division also has the `INT_MIN / -1` overflow corner case, and the high-half check might have to be on absolute values.) – Peter Cordes Apr 09 '21 at 22:34
  • 1
    Note that other ISAs have made different choices; for example ARM truncates instead of raising exceptions. (And I assume sets flags). [Why does integer division by -1 (negative one) result in FPE?](https://stackoverflow.com/q/46378104) – Peter Cordes Apr 09 '21 at 22:37
  • so division by zero is a special case in a sense? – tinmanjk Apr 09 '21 at 23:02
  • 1
    @PeterCordes: Division on ARM (both 32 and 64) never sets flags. There's no narrowing divide, so the only way it can overflow is signed `INT_MIN / - 1`, which is indeed silently wrapped to `INT_MIN`. – Nate Eldredge Apr 09 '21 at 23:11
  • @tinmanjk: Yes, division by zero is a special case of this special case: there is no mathematically-correct result to truncate if you wanted truncating division, and any iterative algorithm might get stuck in an infinite loop (so hardware couldn't just do it and discard or detect overflow in the quotient to catch bad operands). But the same `high_half < divisor` check would catch it, too. unsigned `x < 0` for any x. – Peter Cordes Apr 09 '21 at 23:15
  • The exception makes a certain amount of sense for divide by zero: it's "always erroneous", so this way you get it detected without having to waste precious memory on extra checking code. The fact that overflow does the same thing may very well have been to simplify the implementation. – Nate Eldredge Apr 09 '21 at 23:18
  • 1
    This is similar to the "should I return an error code or throw an exception" debate in higher level languages. – 1201ProgramAlarm Apr 09 '21 at 23:23
  • @PeterCordes : well, my math is not as fresh, but is there a set of standards for when truncating is okay? IMUL will truncate everything. So I think the DIV case shouldn't be so much about correctness than about something else. – tinmanjk Apr 09 '21 at 23:32
  • @tinmanjk: 8086 didn't include `imul reg, reg`, only widening one-operand `mul`/`imul` which don't truncate, so every other 8086 instruction produces a full result. (in DX:AX for mul/imul, or in CF:reg for add/sub). Except for shift-by-cl which can shift out multiple bits, but that's more of a "logical" instruction than arithmetic. DIV / IDIV might be the only math instructions where there could be a wider result and nowhere to put it. Still, they *could* have just decided to truncate and set FLAGS, especially on non-zero divisors. – Peter Cordes Apr 09 '21 at 23:37
  • @PeterCordes so at the time when MUL and DIV were added, MUL was guaranteed to not overflow and the question for DIV was to truncate or raise exception. When more operands MUL/IMUL were included, they decided to truncate and not raise exception for those. – tinmanjk Apr 10 '21 at 00:03

1 Answers1

7

For a definite answer, you'd have to ask Stephen Morse, the designer of the 8086 instruction-set.

Other Intel engineers worked on the actual implementation, but apparently the ISA was designed on paper first, almost entirely by just one guy. He's also credited as principal architect of 8086. PC World interviewed him in 2008, for the 30th anniversary of 8086, and more importantly he wrote a book, The 8086/8088 Primer (1982). I haven't read it, but apparently he discusses some design decisions as well as how to program it. If you're lucky, maybe he wrote something about choosing to have div/idiv trap.


There's no reason it had to be this way; setting CF and/or OF and truncating would have been valid designs. But you still need to choose some value to put in the quotient/remainder output registers in the divide-by-zero case1. (I think it's fairly common for ISAs with HW division to have a divide-by-error exception for at least divide by zero, but On which platforms does integer divide by zero trigger a floating point exception? unfortunately only mentions x86 as an ISA with traps. If division does trap and a POSIX OS delivers a signal at all, it must be SIGFPE for an arithmetic exception.)

Note that other ISAs in do make different choices. For example ARM division never faults, and doesn't set flags either. (Although it doesn't provide a double-width dividend, so only the INT_MIN / -1 signed overflow and division by 0 cases are special for it.)

IDK if building a hardware divide unit (or microcode) that could get the correctly-truncated quotient for overflow cases (when the exact quotient is wider than 16-bit) would be harder than simply detecting overflow and bailing out. If so, that would be a fairly good reason.

(Leaving garbage in the output registers and setting FLAGS would be possible but not great; every division would need to check the result afterwards if it wanted to avoid the possibility of using garbage.)

Note 1: div by 0 in some ways is a special case of this: high_half < divisor is false for divisor=0 for any dividend. But there's no well-defined mathematical result to truncate. IEEE FP division resolves this by treating as the limit as divisor approaches 0, i.e. +- infinity. But integer 0 should be assumed to be exactly 0, not some tiny number, and there's no in-band NaN or Inf value to use anyway, only a finite 0xFFFF...


No other 8086 math instructions need to truncate, if you consider FLAGS

Note that 8086 only included the one-operand forms of mul and imul, which do widening multiply: DX:AX = AX * src. (CF and OF are set if the high half is non-zero (for mul), or if the high half isn't the sign-extension of the low half (for imul)). Only later CPUs introduced truncating forms like imul r, r/m, imm (186) and imul r, r/m (386) that don't waste time writing the high half anywhere, although still setting FLAGS so you could detect signed wrapping if you wanted. (Most uses don't, so later CPUs only provided imul, versions of mul that would be the same except for FLAGS.)

add/sub can carry/borrow, but the full result of an add is available as CF : reg with the extra bit in the carry flag.

If you consider sar / shr / shl reg, cl as a bitwise logical operation, not math, then it doesn't count even though it can shift out multiple bits without leaving them anywhere. (The last bit is left in CF, so shift-by-1 can be undone with a rotate-through-carry.)

That leaves DIV / IDIV as I think the only arithmetic instructions where there could be a wider result and nowhere to put it. That might have been part of the motivation for choosing to have them fault.


high_half < divisor is gotcha-proof for unsigned division

That's the exact condition for the quotient fitting in the operand-size. 1:0 (e.g. 0x0100 for 8-bit operand-size) is smallest quotient that doesn't fit, so 0x0100 * divisor is the smallest dividend that would produce a quotient that doesn't fit in 8 bits.

That dividend is divisor:0 when split up into hi:lo halves of the same width as the dividend.

Any number smaller than that would have to "borrow" from the high half, leaving it strictly smaller than divisor.

(Signed division also has the INT_MIN / -1 overflow corner case, and the high-half check might have to be on absolute values.)

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • What about IDIV: 9100h/92h -> exception, 9200h/91h ->also exception? – tinmanjk Apr 10 '21 at 14:36
  • @tinmanjk: I didn't make any definite claims about `idiv` overflow; my math only proves anything about `div`. Apparently it's not as simple as absolute value. – Peter Cordes Apr 10 '21 at 16:44
  • yes, I think I have found a similar question [link](https://stackoverflow.com/questions/37085162/overcoming-the-x86-idiv-de-exception) which is not really definitive either, but a good starting point – tinmanjk Apr 11 '21 at 03:05