Computing History · Microprocessor Revolution
Interrupt Vectors as the First Hardware Abstraction Layer
The real-mode vector table turned firmware entry points into a stable calling convention, letting programs reach disks, video, and keyboards without knowing which adapter was installed.
The first personal computers shipped with a problem that mainframes had solved differently: the machine’s capabilities were not known when the operating system was written. A buyer could install a monochrome adapter or a colour adapter, one disk drive or two, a printer or none. Software that addressed the hardware directly would work on exactly one configuration. The solution was to make the firmware’s entry points addressable through a table that hardware could modify, so that a program could call a service by number rather than by address.
A table of four-byte entries at a fixed address
In real mode an interrupt is dispatched by reading an entry from a table at physical address zero. Each entry is four bytes: a sixteen-bit offset followed by a sixteen-bit segment, giving the address of the handler in the form the processor expects. The table has room for 256 entries, so it occupies the first 1,024 bytes of the address space, and every entry is reachable without knowing anything about the machine other than that it implements the table.
The processor’s own exceptions use the low entries. A divide error is interrupt 0, a single-step trap is 1, a non-maskable interrupt is 2, a breakpoint is 3, and an overflow is 4. Hardware interrupts arrive on the programmable interrupt controller and are mapped to entries above these, conventionally starting at 8 for the first controller’s eight lines and at 112 for the second controller’s, which is how a machine with two controllers keeps sixteen lines distinct. Everything from the top of that range upward is available for software interrupts, and it is that region the firmware claimed.
A software interrupt is an instruction that does nothing but push the flags, push the return address, and jump through the table entry. The handler runs, ends with an instruction that returns from an interrupt, and the processor restores the flags and resumes. From the caller’s side the sequence is a call with a numeric address, and the number is fixed by convention rather than by the machine’s wiring. That is what makes it an abstraction: the number is the same on every machine, and what it reaches is whatever that machine’s firmware installed.
The firmware’s published service numbers
The services that software depended on were assigned numbers and documented, and the assignment is the interface. Interrupt 16h reads the keyboard: a function number in one register selects between waiting for a key, polling for one, and querying the shift state. Interrupt 13h addresses the disk: the function number selects reset, read, write, verify, and format, and the parameters travel in registers that the documentation names. Interrupt 10h drives the display: setting the mode, positioning the cursor, writing a character with an attribute, and reading the current mode.
The convention that carries the parameters is as important as the numbers. A caller places a function code in the high byte of the AX register, arguments in other registers, and reads results from registers on return. Carry flag set means failure, with an error code in a register. That is a calling convention defined by the firmware rather than by a compiler, and it survives because it is documented in a form that assembly programmers can use directly.
The abstraction’s value is that the caller does not know which disk controller is installed. The firmware’s disk service translates a request in terms of cylinder, head, sector, and drive number into whatever command sequence the installed controller requires. A program written against interrupt 13h works on a machine with a different controller, provided the firmware on that machine implements the same function numbers. When a new controller appeared, the adapter’s own ROM could take over the vector: the ROM scanned for a signature, found the existing entry, saved it, and replaced it with its own handler. The old handler remained reachable as the fallback for drives the new ROM did not control.
Chaining as the extension mechanism
That save-and-replace pattern is the reason the table is a table rather than a set of fixed jumps. A handler can be layered: it examines the request, handles what it recognises, and otherwise passes control to the handler it displaced. The displaced address is kept in the new handler’s data, and the pass-through is a jump to a far pointer the handler holds.
Chaining has a cost that shows up under contention. Each layer adds the cost of its own test, and a chain of several layers makes every call slower even when the request is for the original handler. It also makes removal hazardous: a program that wants to unhook must verify that the vector still points to its own handler before restoring the saved value, because another program may have hooked after it. Restoring blindly would discard the later hook and leave its code resident but unreachable.
The chain is also how the table becomes a target. Because the entries are ordinary memory, any program can write them, and a program that writes an entry without saving the old value breaks every later program that expects the original service. The table has no protection and no ownership record, which is a deliberate consequence of real mode having no privilege levels. Everything runs with the same authority, and the firmware’s services are conventions that all software agrees to respect.
; Hook interrupt 1Ch (the timer tick), preserving the previous handler.
; The tail jump hands control down the chain: the displaced handler's
; IRET returns to the interrupted program, not to this code.
old_1c dd 0 ; far pointer saved at install time
hook:
push ax ; flags were saved by the interrupt itself
; ... work on each tick ...
pop ax
jmp far ptr [old_1c] ; continue the chain
The timer tick is the clearest example of the table being used as a scheduler. Interrupt 8 fires at a fixed rate, and the firmware’s handler counts ticks, updates the time-of-day clock, and then transfers control to interrupt 1Ch, which by default points at an instruction that returns immediately. A program that wants periodic work hooks 1Ch rather than 8, because the firmware’s handler has already done the bookkeeping and acknowledged the controller. The rate is fixed by the hardware’s timer, so the hook runs at that rate and no other.
What the abstraction could not hide
The services were defined for the hardware that existed when they were written, and their parameters encode those assumptions. The disk service addresses a drive by a number assigned in the firmware’s configuration, not by a physical connection, so a program that assumes drive 0 is the first floppy and drive 0x80 is the first fixed disk is relying on a convention that a machine with different firmware may not follow. The video service addresses a display in text terms, with a mode number, a page number, and a character position, because that is what the adapters of the time provided; a program that wants pixel-level drawing must go around the service and write the adapter’s registers directly, which is where the framebuffer layout becomes the program’s problem rather than the adapter’s.
The keyboard service returns a character code and a scan code, and the character code depends on the keyboard layout the firmware was configured for. A program that tests for a specific character is testing the layout, not the key. The service also has a fixed-size buffer, and a program that polls less often than keys arrive loses them, because the buffer does not grow and the handler drops input when it is full. The size of that buffer is a firmware constant, and programs that needed to guarantee no loss installed their own handler ahead of the firmware’s.
The most consequential limit is that the services are synchronous. A disk read through the firmware blocks until the transfer completes, and during that time the processor does nothing else. Controllers that could transfer directly to memory without processor involvement existed, but the firmware’s interface did not expose that mode in a way that let a program overlap the transfer with computation. Programs that needed overlap wrote their own controller commands, which meant knowing the controller.
None of these failures invalidated the abstraction. They defined its boundary. A program that stayed inside the documented services could run on machines it had never seen, and a program that needed more reached past them and accepted the cost of knowing the hardware. Resident software that hooked the same vectors had to respect the chain the firmware had already established, which is where the reentrancy constraints of resident programs become unavoidable.
References
- IBM Personal Computer Technical Reference — IBM Corporation, 1981
- Ralf Brown's Interrupt List — Ralf Brown
- IBM Disk Operating System Technical Reference — IBM Corporation, 1987