Skip to content
FreeDOSDeep Dive Published Updated 8 min readViews unavailable

Real-Mode x86 Exceptions: Divide Errors, IVT Vectors, and Safe DOS Recovery

Distinguish processor exceptions from DOS interrupts, understand the original 8086 vector contract, and build handlers that preserve state and return safely.

A real-mode DOS program can fail before it reaches a DOS service. The processor may transfer control to a vector because an instruction produced a divide error, an INTO instruction observed overflow, a debugger raised a breakpoint, or hardware asserted NMI. These events use the same real-mode interrupt-vector table as software INT instructions, but they are not interchangeable with DOS APIs such as INT 21h. Correct recovery depends on the CPU generation, the exact saved state, existing BIOS handlers, and whether the program can safely resume.

This article focuses on the original 8086/8088 interrupt model and the conventions that matter to DOS software. Later processors add exception classes and behaviors; do not project 386 protected-mode error-code rules onto an 8086 real-mode handler. Use the Intel manual for the processor you actually target.

The interrupt vector table is a shared ABI

In 8086 real mode, the interrupt vector table (IVT) begins at physical address 00000h. Each entry is a four-byte far pointer: a 16-bit offset followed by a 16-bit segment. The CPU selects vector n by multiplying it by four and reading the corresponding pointer. On interrupt entry, the processor pushes FLAGS, CS, and IP, clears the interrupt and trap flags, then transfers control through the selected vector. IRET restores IP, CS, and FLAGS from the stack.

The table belongs to the whole machine. BIOS installs handlers during initialization, DOS and device drivers may hook vectors, and applications can temporarily wrap a handler. A program must not overwrite a vector without first preserving the previous pointer and deciding how to chain or restore it. DOS provides get/set vector services for normal application use; direct IVT writes can race a hooker or leave an invalid pointer after process exit.

The vector number alone does not identify the event on every PC generation. The original 8086 defines processor-originated conditions among the early vectors, while IBM BIOS and DOS software also use interrupt numbers as software service entry points. Later hardware and BIOS conventions occupy additional vectors. On IBM PC-compatible systems, software should consult the processor manual and the platform interrupt map rather than assuming that every number below 20h is available for application use.

Divide error is not just division by zero

The DIV and IDIV instructions raise the divide-error condition when the divisor is zero or when the quotient does not fit in the destination register. Quotient overflow is a frequent bug in 16-bit code: a mathematically valid division can still fail if the programmer did not prepare the high half of the dividend or selected a destination that is too narrow. Treat both cases as one processor error path and validate operands before executing the instruction whenever possible.

Do not assume the failure means “the divisor was zero.” Log the instruction context and the relevant dividend and divisor registers if the handler has a safe way to capture them. A debugger or emulator may label the event as divide-by-zero even when the actual cause is quotient overflow. Regression tests should include zero divisor and maximum quotient cases for every operand width used by the program.

Exception return semantics differ across x86 generations and execution modes. The modern Intel SDM describes the divide-error exception as a fault whose saved instruction pointer identifies the faulting instruction; historical 8086 behavior and PC BIOS handlers must be interpreted from the 8086-family documentation. A handler should not blindly increment the stacked IP by an assumed instruction length: instructions may include prefixes, addressing modes, or operands, and skipping the wrong number of bytes can execute corrupted code. If the application cannot prove a safe recovery point, report the failure and terminate through a controlled path instead of pretending to resume.

INTO, INT3, single-step, and NMI are different cases

INTO is an instruction that requests vector 4 only when the overflow flag is set. It is a deliberate software check for signed arithmetic overflow, not an automatic exception after every overflowing ADD or SUB. The application must execute INTO where it wants that check. A compiler may instead emit conditional branches or runtime helpers, so inspect generated code before expecting a handler to run.

The breakpoint instruction INT 3 is used by debuggers and can be used by diagnostic software. It is distinct from a processor divide error. Single-step uses the trap flag and vector 1, which means a debugger or TSR may already own that vector. A program that temporarily enables trace mode must preserve the prior trap and handler state and must account for reentrant entry.

NMI uses vector 2 and is not masked by clearing the ordinary interrupt flag. IBM PC hardware can route critical platform conditions through NMI, such as parity-related or channel-check conditions, depending on system design. A DOS application must not repurpose the NMI vector as if it were a private callback. The event can arrive while another handler is running and may indicate a hardware fault that makes ordinary recovery unsafe.

Handler design: capture first, decide later

A robust diagnostic handler has a deliberately narrow role. It should save the registers it uses, capture a bounded record into memory whose lifetime is guaranteed, set a flag, and transfer control to a known recovery path. Avoid DOS calls, console output, file writes, memory allocation, or blocking input from an exception context. Those services may rely on the same stack, device driver, or interrupted state that caused the event.

If the handler needs to chain to a previous vector, use the exact calling convention required by that handler and ensure the stack frame matches. A tail-chain and a call-then-return chain are not equivalent. A handler that consumes the event must document what original behavior it suppresses. Always restore the prior vector on normal termination, and use DOS’s keep-resident mechanism only when the handler’s code and data remain valid after the process exits.

Do not use IRET as a generic recovery strategy. Returning to the same faulting instruction can cause an infinite loop; changing saved IP without decoding the instruction can skip into the middle of code; returning from an NMI can resume a system after an unrecoverable bus condition. Recovery policy should be chosen per event and processor. For an application-level divide error, a safer pattern is often to display a diagnostic from the mainline after the handler marks the fault, then exit cleanly.

Install and restore vectors through DOS

For a temporary application handler, use the DOS get-vector service to save the old far pointer and the set-vector service to install the new one. Perform vector updates with interrupt state handled according to DOS and compiler conventions; an interrupt could otherwise arrive between reading and writing the pointer. Do not hold interrupts disabled around arbitrary application work, and do not assume vector installation alone makes the handler reentrant-safe.

The handler’s segment must remain resident for as long as the vector points to it. A transient program that exits and returns memory to DOS while leaving its address in the IVT creates a dangling code pointer. On cleanup, verify that the vector still points to your own handler before restoring the saved value; another program may have chained on top of it. If so, blindly restoring the old pointer would remove the newer handler and corrupt the chain.

Keep any per-handler state in a resident block or a stable location and document ownership. A copied data record can avoid dependence on a local stack frame that no longer exists. If an interrupt can nest, protect shared counters with a short critical section and account for the target CPU’s atomicity guarantees. Simpler is safer: capture a single event and defer interpretation to normal program flow.

CPU generation and mode caveats

The 8086, 8088, 286, 386, and later x86 processors do not share identical exception behavior. Later processors define additional exceptions, error codes, restart rules, and protected-mode mechanisms. A real-mode DOS extender may virtualize interrupts or emulate I/O, while a virtual-8086 environment can intercept software interrupts before they reach the physical IVT. Therefore, a handler validated on one 8086 emulator does not establish correctness on a 386 DOS extender.

Document the minimum CPU, execution mode, memory model, assembler, and debugger assumptions. If an application supports multiple CPU generations, keep processor-specific entry stubs separate and use the relevant Intel manuals. Do not write code that conditionally parses an error code unless the execution mode and exception type guarantee that such a code was pushed.

Test and evidence checklist

Exercise each supported condition in a disposable emulator or test machine. For divide error, test zero divisor and quotient overflow. For INTO, test both overflow-flag states. For INT 3, confirm the debugger’s vector chain remains intact. For NMI, do not synthesize the event on live hardware; test a simulator or documented diagnostic environment. Verify stack balance, captured register values, termination behavior, and vector restoration after normal and abnormal exits.

Capture the processor model, DOS and extender versions, vector before/after, event type, raw saved frame, and whether another debugger or TSR was installed. A printout that says “divide by zero” without the machine state is weak evidence. A controlled failure record should help distinguish arithmetic overflow, vector corruption, and an unsupported execution environment.

The core distinction is simple: a CPU exception reports a processor condition, while a DOS interrupt is an operating-system call or platform service. Both use vectors in real mode, but their ownership and return contracts differ. Check the CPU manual, preserve the vector chain, and never resume by guessing at saved state.

Related:

Sources:

Comments