Math error handling on x87
From Open Watcom
Just like too many other aspects of the PC architecture, the low-level details of x87 FPU error handling are highly complex and unintuitive, largely for reasons of backwards compatibility.
Contents |
History
The first member of the Intel x87 floating-point unit family was the 8087. The design of this chip started in the late 1970s and was done in parallel with the proposed IEEE 754 standard for binary floating-point arithmetic. As a consequence, the 8087 is not 100% compatible with the final IEEE 754 standard, but the differences aren't major.
The 8087 FPU was a chip designed to work in conjunction with an Intel 8088 or 8086 CPU. Later it was followed by 80287 and 80387 chips; the 387 is fully compatible with the approved IEEE 754 standard. Since the arrival of the i486 CPU, the FPU is no longer a separate chip. This affects the compatibility with earlier designs in subtle ways.
IEEE 754 Exceptions
It is important to not confuse exceptions as defined by the IEEE 754 standard with exceptions as defined by Intel 80286 and later CPUs. The concepts are related but far from identical.
IEEE 754 defines five classes of exceptions:
- Invalid Operation - for example square root of a negative operand
- Division by Zero - occurs for a zero divisor and finite non-zero dividend
- Overflow - the destination format is not large enough to store the result of an operation
- Underflow - the result of an operation is too tiny
- Inexact - the result of an operation is not exact and cannot be represented without some loss of accuracy
An IEEE 754 exception is the result of an 'exceptional' operation and may require some intervention. It is expected that the user of an IEEE 754 implementation may want to handle each class of exception explicitly or use the default handling for 'uninteresting' exceptions. In particular, the inexact exception occurs quite frequently and does not cause any problems for most calculations.
IEEE 754 also defines the concept of a trap. This roughly corresponds to the x86 concept of an interrupt or exception. An IEEE 754 exception may be set to set a flag, take a trap, or both.
x87 Implementation of IEEE 754 Exceptions
To properly understand the current x87 math error handling, it is necessary to follow its evolution since the earliest chip.
8087 Error Handling
The Intel 8087 chip (introduced in 1980) and all its successors use the floating-point control word (FPCW) to control aspects of IEEE 754 exception processing. The low six bits of the FPCW are mask bits for FPU exceptions:
- Invalid operation - IEEE invalid operation or x87 stack fault
- Divide by zero
- Denormalized operand - a subset of IEEE underflow exception
- Numeric overflow
- Numeric underflow
- Inexact result
In addition, bit 7 of 8087's FPCW is the Interrupt Mask bit. If this bit is set, 8087 interrupts are disabled.
If an exception occurs in the 8087 and the corresponding exception class is masked, the FPU will set a corresponding bit in the floating-point status word, perform on-chip default exception handling, and continue execution.
If an exception occurs whose class is unmasked, and the Interrupt Mask bit is clear (ie. FPU interrupts are enabled), the 8087 will use the INT (interrupt) pin to signal the condition to the system.
8087 in the IBM PC and XT
The 8087 design allows the system designer some degree of flexibility in how the INT line of the 8087 is used to trigger an 8086/8088 interrupt. The expected design would use the 8087 INT line as an input to the 8259A Programmable Interrupt Controller (PIC) which would then trigger one of the CPU's 255 available interrupts.
However, that is not the design the IBM PC used. Even though the PC did have an 8259A PIC, the 8087's INT signal to the NMI (Non-Maskable Interrupt) pin of the 8088/8086 CPU. Perhaps this solution was chosen because the 8087 was a not-too-frequently used add-on and hardware interrupt request lines (IRQs) were in short supply (since there was just a single PIC with only 8 IRQ lines available).
As a result, software for the IBM PC had to hook INT 2 (the NMI) if it wished to handle 8087 exceptions. A properly written NMI handler would then have to check if the interrupt was raised as a result of an 8087 exception or a genuine NMI.
It is worth pointing out that there were several ways to disable 8087 exceptions/interrupts, and hence ample opportunity for problems with the floating-point exceptions not being delivered:
- The exceptions had to be unmasked in the FPCW. This mechanism was retained in all later x87 designs.
- Interrupts had to be enabled in the FPU (FENI/FDISI instructions). The interrupt enable bit is not used (effectively always enabled) in 80287 and later designs, and FENI/FDISI instructions are ignored.
- NMIs (clearly a misnomer) had to be unmasked on the motherboard via port 0A0h. This mechanism was specific to the PC and XT (the AT had a different NMI enable/disable mechanism, but that was not relevant to the coprocessor).
- Finally a DIP switch on the system board had to be in the correct position.
80287 Error Handling
The 287 (introduced in 1983) contained few changes versus the 8087. However, more important were the changes in the 80286 CPU. The 286 was designed to run primarily in protected mode, with significantly different method for math error handling.
The 287 no longer has an INT line meant to be connected to an interrupt controller. Instead, it has an ERROR line designed for direct connection to a 286 CPU. When the 286 detects an ERROR signal, it will raise exception 16 or Math Fault (#MF). This is how an IEEE 754 exception/trap may be translated to an x86 CPU exception.
The 287 no longer has the Interrupt Mask bit, and as a result the FENI/FDISI (enable/disable coprocessor interrupt) instructions are ignored.
The way the 80387 FPU handles math errors is substantially the same as the method used by the 287.
80287 in the IBM AT
Due to the market reality of the vast majority of PC users still running PC DOS and requiring IBM PC compatibility, the way the IBM AT handled math errors was not straightforward. Because IBM ignored Intel's recommendation when designing the PC, 286's Math Fault or interrupt 16 conflicted with BIOS video service interrupt 10h (16 decimal). On top of that, existing software expected math exceptions to arrive through INT 2.
Instead of connecting the CPU and FPU ERROR pins, the IBM AT used motherboard circuitry to route the 287 ERROR signal to the cascaded second 8259A PIC and used IRQ 13 to signal math errors to the CPU. The default BIOS IRQ 13 handler (that is, INT 75h vector - remember that IRQ 8, the first IRQ line of the second PIC, corresponds to interrupt vector 70h) contains code to invoke INT 2 for compatibility with existing software. Software on the AT thus still can hook the NMI vector and run unchanged on the PC or AT.
External circuitry in the IBM AT drives the 286's BUSY input pin active when a 287 asserts its ERROR signal. This prevents execution of further FPU instructions and is required to avoid problems in the time window after the 287 signaled an error and before the time the 286 starts processing the resulting interrupt.
Compared to the PC and XT, in the AT there were only two mechanisms influencing the delivery of coprocessor exceptions:
- Exception mask bits in the FPCW.
- Interrupt mask bits in both PICs (IRQ 13 and IRQ 2, the cascade interrupt).
Applications (or their runtime libraries) were responsible for manipulation the FPCW, especially because FINIT/FNINIT masks all math exceptions. The BIOS was responsible for unmasking IRQ 13, which saved applications from having to include system-specific code.
i486 Error Handling
The Intel 486DX processor contained an on-chip FPU. There was a FPU-less variant called 486SX, but the 487SX 'coprocessor' was in fact a full 486 CPU with an integrated FPU. Consequently, the 486SX is irrelevant for this discussion and the 487SX is no different from the 486DX.
By the time the i486 was released (1989), Intel had realized the importance of the PC compatible market. Therefore the i486 contains features designed specifically to provide compatibility with existing PC software and hardware designs.
i486 in AT compatibles
Like the 286/287 and 386/387 systems, the native math error handling on the i486 uses CPU exception 16 (#MF). However, this is not the boot-up default in PCs. To achieve backwards compatibility, the i486 provides the following software and hardware features.
The NE (Numerics Exception) bit, that is bit 5 in CR0, controls floating-point error handling. If this bit to set to one, FPU errors are routed through exception 16. If the NE bit is set to zero, FPU errors are handled through external interrupts. The i486 also contains two pins, FERR# and IGNNE#, to provide compatibility with the IBM AT in conjunction with external circuitry.
Math Error Handling in Modern x86 CPUs
The mechanism introduced in the i486 is still used in today's CPUs and will be described in detail. The method is described as MS-DOS compatibility sub-mode in current Intel literature.
When the NE bit is clear, a FPU error will set the appropriate bits in the FPU status word and activate the FERR# signal. This signal is routed to IRQ 13 in hardware and to INT 2 in software, as was the case with the 287.
When the IGNNE# signal is inactive, the CPU will freeze before executing the next floating-point instruction or a WAIT instruction. This is necessary because routing the FERR# signal through external circuitry takes a non-zero amount of time. If the CPU wasn't frozen, it could execute further floating-point instructions and it would be impossible to detect and possibly correct the cause of the original math error.
The IRQ 13 handler (INT 75h) will will send an EOI (End-Of-Interrupt) to both PICs and call INT 2 (typically hooked by a compiler runtime's math library). However, as long as the FERR# signal is asserted, it is impossible to execute any non-waiting FPU instructions. This is necessary, but a software math error handler might need to execute some floating-point instructions before clearing the error condition.
To help with this problem, the IGNNE# (IGNore Numeric Error) signal is used. When this signal is active, it is possible to execute FPU instructions even when the FERR# signal is active. When the FERR# signal is eventually cleared, the IGNNE# signal must be cleared as well.
External Support Provided by Motherboard Chipset
As noted above, the backwards compatible error handling cannot rely on the CPU alone, it requires external circuitry. In modern systems, this circuitry is typically built into the motherboard chipset. Let's examine the support provided by Intel's PIIX4, a typical southbridge implementation (its predecessors PIIX and PIIX3 were the same in this regard).
The PIIX4 chip includes, among other things, two 8259A-compatible interrupt controllers. PIIX4 can be connected directly to the CPU's FERR# and IGNNE# pins, which means that no other chips are needed to implement backwards compatible math error handling. The PIIX4 support for math error handling is disabled by default, but a BIOS POST routine would normally enable it (Coprocessor Error Function Enable, bit 5 in X-Bus Chip Select Register at offset 4Eh in PIIX4 device 0's PCI config space).
When a CPU asserts FERR#, PIIX4 takes all the necessary actions to raise IRQ 13 and signal the interrupt to the CPU. When the INT 75h handler takes control, it writes to port 0F0h (CERR or Coprocessor Error Register). PIIX4 decodes this write which clears the IRQ 13 request and also asserts the IGNNE# signal.
Then the IRQ 13 handler clears the interrupt in the PICs by sending EOI to both controllers. At this point, the FPU software exception handler (typically hooked to INT 2) is ready to run. Note that writing to port 0F0h only has an effect when the FERR# signal is active.
When the exception handler finally clears the exception within the FPU, the FERR# signal is deactivated. The PIIX4 chip sees this and deactivates the IGNNE# signal as well. At this point, the system is in its default state again and ready to execute further FPU instructions in a normal manner.
Conclusion
The backwards compatible math error handling is byzantine, perhaps even cruel and unusual. However, it begins to make sense once its evolution is understood. Fortunately, few programmers have to deal with math error handling on the lowest level, and therefore can be blissfully unaware of its intricacies.

