8086 Interrupt Handling

Interrupt Vector Table

The 8086 supports 256 interrupts, each requiring a corresponding Interrupt Service Routine (ISR). The starting addresses (CS:IP) of the 256 ISRs are arranged in order to form an Interrupt Vector Table (IVT). Each interrupt vector consists of 4 bytes. These 4 bytes specify the segment value (assigned to CS) and the intra-segment offset (assigned to IP) of an ISR. Therefore the total length of the vector table is 1 KB = 256 × 4, occupying the physical address range 0x00000 to 0x003FF. When the 8086 CPU boots, a program in the BIOS initializes and sets up the IVT at physical memory address 0x0000:0x0000, and the default ISRs for each interrupt are provided by the BIOS. Since the vectors in the IVT are ordered by interrupt number, given an interrupt number N, the corresponding interrupt vector’s location in memory is 0x0000:N×4, meaning the ISR entry address for that interrupt is stored at physical memory location 0x0000:N×4.

xx

Interrupt Classification

All 256 interrupts fall into two major categories: exceptions and interrupts. Exceptions are further divided into faults, traps, and aborts. The common characteristic of exceptions is that they neither use the interrupt controller (they do not raise interrupt signals through the 8259A) nor can they be masked. Interrupts are further divided into external maskable interrupts (INTR) and external non-maskable interrupts (NMI). Interrupt requests (IRQ) generated by all I/O devices cause maskable interrupts, while critical events (such as hardware failures) cause non-maskable interrupts.

The vectors for non-maskable interrupts and exceptions are fixed, whereas the vectors for maskable interrupts can be changed by programming the interrupt controller. But everyone usually follows a convention. For example, Linux assigns the 256 vectors as follows.

  • Vectors 0 to 31 correspond to exceptions and non-maskable interrupts.
  • Vectors 32 to 47 (i.e., interrupts caused by I/O devices, 16 in total, corresponding to the maximum number of interrupts produced by two cascaded 8259As) are assigned to maskable interrupts. 32–47 is also 0x20 to 0x2F.
  • The remaining vectors 48 to 255 are used to identify software interrupts. Linux uses only one of these (i.e., vector 128 or 0x80) to implement system calls. When a user-mode process executes an int 0x80 assembly instruction, the CPU switches to kernel mode and begins executing the system_call() kernel function. (Note: the latest Linux systems no longer use this approach, but remain compatible with it.)

Interrupt Sources and the 8259A

The 8086 uses the 8259A as its interrupt controller. At the time it was a separate chip, and as computers evolved it was integrated into the CPU, but the logic of this chip still exists in modern x86 computers (integrated in the southbridge). A single 8259A chip has 8 interrupt input pins, and two chips are usually cascaded for a total of 15 interrupt input signals.

The 8259A’s interface with the CPU consists of three parts: first, the interrupt vector number, which is transferred to the CPU via the system bus; second, the interrupt request signal, connected as a separate pin to the CPU’s INTR pin; third, the INTA pin, used to acknowledge the interrupt from the CPU and clear it.

The general execution flow is as follows: an I/O device generates an interrupt, producing a level change to the 8259A. The 8259A encodes the interrupt vector number (note that this is the interrupt vector number, not the IRQ number — explained in detail later), places it on the system bus, and then issues an INT signal.

After receiving the INT signal, the CPU reads the interrupt vector number, then queries the IDT for the corresponding interrupt handler address, and executes that interrupt handler. The interrupt handler usually first issues an INTA signal to notify the 8259A that the interrupt has been acknowledged and can be cleared.

The assignment of interrupt signal sources to each pin of the 8259A follows certain rules, or rather conventions — that is, your computer’s hardware may not necessarily be arranged this way, but it very likely is. The usual rules are shown in the figure.

First, when cascading, IRQ2 (some machines use IRQ9) is used as the cascade pin, connected to the output of the second 8259A. This means IRQ2 (or IRQ9) normally does not generate interrupts.

Second, the usual order of hardware-generated IRQs is:
IRQ0 Timer
IRQ1 Keyboard
IRQ3 tty2
IRQ4 tty1
IRQ5 XT Winchester
IRQ6 Floppy disk
IRQ7 Printer
IRQ8 Real-time clock
IRQ9 Redirected IRQ2

IRQ and Interrupt Vector Number

As mentioned earlier, by convention IRQ0 corresponds to interrupt vector number 0x20, i.e., 32. However, a convention is just a convention, and it can be set through hardware configuration. This setting is done via the ICW3 control word during 8259A initialization. This control word takes an 8-bit number, but its lower three bits are meaningless; only the upper 5 bits are meaningful. When an interrupt occurs in the 8259A, a pin number 0–7 is obtained, forming a three-digit number as the lower three bits, which is then combined with the upper 5 bits passed in via ICW3 to form a byte — this is the final interrupt vector number placed on the system bus.

In other words, under the convention described above, the two 8259As need to have their ICW3 set independently: the ICW3 of the first (master) 8259A is set to 0x20, and the ICW3 of the second (slave) 8259A is set to 0.

If the ICW3 of the first 8259A is set to 0x30, then the keyboard interrupt’s vector number becomes 0x31. When the BIOS sets up the IVT, it must place the keyboard interrupt handler’s entry address at the 0x31th interrupt vector slot.

Since the ICW3, the IVT, and the interrupt handlers are all within the BIOS’s control, no matter how they are set, the BIOS only needs to remain consistent with itself.

80286 Interrupt Handling

In the 8086, interrupts are dispatched through the IVT, which provides ISR entry points. The IVT is essentially a set of instructions placed at a fixed location in memory, usually jump instructions, that jump to the actual ISR.

In the 8086, this address is completely unprotected — whether a program tampers with it depends entirely on goodwill. This CPU design is flawed: for complex software organization, the operating system must have the ability to protect these interrupt vectors so that they cannot be easily tampered with, and cannot be executed by programs without privilege.

Protecting interrupt service code can be handled by the segmented memory management mechanism, i.e., segment descriptors, but how to prevent unauthorized access to user software interrupts remained a problem. This was solved by adding task gate descriptors on top of the Task State Segment descriptor. For interrupts, interrupt gate descriptors were also added.

The Interrupt Descriptor Table (IDT) is essentially the entry point collection for interrupt vectors. When an interrupt occurs, the interrupt number is used as an index to look up a descriptor in the IDT, and then the program corresponding to that descriptor is executed. This descriptor can be an interrupt gate descriptor, a trap gate descriptor, or a task gate descriptor.

Each descriptor is still 8 bytes, supporting a maximum of 256 interrupts, for a total of 2 KB.

The CPU performs two checks when executing an interrupt handler:

  1. First is the “segment“-level check. The CPU’s current privilege level CPL (stored in the lowest two bits of the CS register) is compared with the RPL in the segment selector of the nth entry in the IDT. If RPL (3) is greater than CPL (0), a “general protection” exception (interrupt vector 13) is generated, because the privilege level of the interrupt handler cannot be lower than that of the program that caused the interrupt. This situation is unlikely because interrupt handlers usually run in kernel mode with a privilege level of 0. Then comes the “gate“-level check, comparing the CPL with the DPL of the nth gate in the IDT. If CPL (3) is greater than DPL (0), the CPU cannot “pass through” this gate, and a “general protection” exception is generated. This is to prevent user applications from accessing special trap gates or interrupt gates. However, please note that this check applies to interrupts caused by ordinary user programs (INT instructions), and does not include interrupts generated by external I/O or exceptions arising from internal CPU conditions — that is, if an interrupt or exception has occurred, the “gate”-level check is waived.

  2. Check whether a privilege level change has occurred. If the CPU is running in user space when the interrupt occurs, but the interrupt handler runs in kernel mode, the privilege level changes, so a stack switch occurs. That is, the user stack is switched to the kernel stack. When the interrupt occurs in kernel mode, i.e., the CPU is running in the kernel, the stack is not switched. The former case typically means a 0x80 system call.

The values of CS : EIP are the segment selector and offset of the nth gate descriptor in the IDT table. At this point, the CPU jumps to the interrupt or exception handler.

Last modified: 2026年7月13日

Author

Comments

Write a Reply or Comment

Your email address will not be published.