If you’re under 30, you might never have encountered BIOS and DOS interrupts directly. But if you’ve ever wondered how operating systems actually talk to hardware before they have their own drivers loaded — or if you’re writing a bootloader, a BIOS-based firmware utility, or reverse-engineering a legacy system — this article is for you.
BIOS and DOS interrupts are the original system call interface. Before there was Windows API, before there was Linux syscalls, there were INT 0x10, INT 0x16, and INT 0x21. These interrupts let real-mode code access video, keyboard, disk, and dozens of other services through a simple, well-documented interface.
In this article, we’re going to dissect every important BIOS and DOS interrupt, understand exactly how they work, and write real code that uses them. By the end, you’ll be able to write a complete TUI (Text User Interface) program that runs in real mode.
What Are Interrupts? (Hardware Interrupts vs Software Interrupts — and Why INT n Is Special)
Let’s start with the fundamentals. An interrupt is a mechanism that causes the CPU to stop what it’s doing, save its current state, and jump to a special handler function. After the handler finishes, the CPU restores its state and continues where it left off.
Hardware Interrupts (External Interrupts)
Hardware interrupts are triggered by external devices. For example:
- Timer interrupt (IRQ 0): The PIT (Programmable Interval Timer) triggers this 18.2 times per second (by default). It lets the OS do task switching, update the system clock, etc.
- Keyboard interrupt (IRQ 1): Triggered when you press or release a key. The keyboard controller signals the PIC (Programmable Interrupt Controller), which signals the CPU.
- Disk interrupt (IRQ 6 for floppy, IRQ 14/15 for IDE): Triggered when a disk operation completes.
Hardware interrupts are asynchronous — they can happen at any time, regardless of what the CPU is currently executing.
Software Interrupts (INT n)
Software interrupts are triggered by the INT instruction in your code. They’re like function calls into the BIOS or DOS. When you execute INT 0x21, you’re telling the CPU: “Jump to the interrupt handler for interrupt 0x21 (which is the DOS function dispatcher).”
; Example: print 'A' to screen using BIOS teletype output
mov ah, 0x0E ; BIOS teletype function
mov al, 'A' ; Character to print
xor bx, bx ; Page number (0)
mov bl, 0x07 ; Text attribute (light gray on black)
int 0x10 ; Call BIOS video services
The INT n instruction:
- Pushes the flags register (EFLAGS) onto the stack
- Pushes the current CS (code segment) onto the stack
- Pushes the current IP (instruction pointer) onto the stack
- Disables interrupts (clears IF flag)
- Jumps to the interrupt handler address stored in the Interrupt Vector Table (IVT)
The handler ends with IRET (interrupt return), which pops IP, CS, and FLAGS from the stack, restoring the CPU to its pre-interrupt state.
Why INT n Is Special
The INT n instruction is special because it’s the original system call mechanism for x86 real-mode operating systems. It lets user-level code call privileged functions (like reading from disk or changing video modes) without needing to know the details of the hardware.
Think of INT n as the original API. Instead of calling ReadFile() (Windows API) or read() (POSIX), you set up registers and call INT 0x21 with AH = 0x3F (read from file). It’s a different era of computing, but the concept is the same: a well-defined interface between application code and system services.
The Interrupt Vector Table (IVT) in Real Mode: 256 Interrupt Vectors, Each 4 Bytes
In real mode (the CPU mode that runs right after power-on), the Interrupt Vector Table (IVT) is a 1KB table at the very bottom of memory (address 0x0000:0x0000 to 0x0000:0x03FF). It contains 256 interrupt vectors, each 4 bytes long.
IVT Layout
Address Contents
──────── ─────────
0000:0000 Vector for INT 0 (Division by zero)
0000:0004 Vector for INT 1 (Single-step)
0000:0008 Vector for INT 2 (NMI)
0000:000C Vector for INT 3 (Breakpoint)
...
0000:0080 Vector for INT 0x20 (DOS program terminate)
...
0000:0100 Vector for INT 0x40 (Hard disk services - original PC had only floppy)
...
0000:03FC Vector for INT 0xFF (User-defined)
Each vector is a FAR pointer (16:16 segmented address):
- Offset (2 bytes): The IP (instruction pointer) value to jump to
- Segment (2 bytes): The CS (code segment) value to load
So the vector for INT n is at memory address n * 4:
- Segment:
[n * 4 + 2] - Offset:
[n * 4]
Visual Representation of IVT
Interrupt Vector Table (1024 bytes at address 0x0000)
┌─────────────────────────────────┐
│ INT 0 Vector (4 bytes) │ Address: 0x0000:0x0000
├─────────────────────────────────┤
│ INT 1 Vector (4 bytes) │ Address: 0x0000:0x0004
├─────────────────────────────────┤
│ INT 2 Vector (4 bytes) │ Address: 0x0000:0x0008
├─────────────────────────────────┤
│ ... │
├─────────────────────────────────┤
│ INT 0x10 Vector (4 bytes) │ Address: 0x0000:0x0040 ← BIOS Video Services
├─────────────────────────────────┤
│ INT 0x13 Vector (4 bytes) │ Address: 0x0000:0x004C ← BIOS Disk Services
├─────────────────────────────────┤
│ INT 0x16 Vector (4 bytes) │ Address: 0x0000:0x0058 ← BIOS Keyboard Services
├─────────────────────────────────┤
│ INT 0x21 Vector (4 bytes) │ Address: 0x0000:0x0084 ← DOS Function Dispatcher
├─────────────────────────────────┤
│ ... │
├─────────────────────────────────┤
│ INT 0xFF Vector (4 bytes) │ Address: 0x0000:0x03FC
└─────────────────────────────────┘
How the CPU Uses the IVT
When the CPU executes INT n (or when a hardware interrupt occurs), it:
- Computes the IVT address:
n * 4 - Reads the offset from
[n * 4] - Reads the segment from
[n * 4 + 2] - Pushes FLAGS, CS, IP onto the stack
- Loads CS:IP from the IVT entry
- Disables interrupts (clears IF flag in FLAGS)
- Executes the interrupt handler
The handler must end with IRET to return to the interrupted code.
Modifying the IVT
In real mode, you can modify the IVT directly (it’s just memory at address 0x0000). This is how TSR (Terminate and Stay Resident) programs hook interrupts.
; Hook INT 0x09 (keyboard hardware interrupt)
; Save the old vector first!
mov ax, 0x3509 ; AH=0x35 (get interrupt vector), AL=0x09 (interrupt number)
int 0x21 ; DOS returns vector in ES:BX
mov [old_int09_seg], es
mov [old_int09_off], bx
; Now install our handler
mov ax, 0x2509 ; AH=0x25 (set interrupt vector), AL=0x09
lea dx, [my_keyboard_handler]
mov ds, dx_seg ; DS:DX = pointer to handler
int 0x21
; Later, when your program exits, restore the old vector!
Warning: Modifying the IVT is dangerous. If you don’t restore the old vector, the system will crash when the interrupt fires after your program has exited.



