Every computer needs a way to react instantly to events it didn’t plan for — a key press, a timer expiring, a disk finishing a read, or a division by zero. This is the job of interrupt handling, one of the most elegant mechanisms in computer architecture. In this post, I’ll break down what interrupts are, how the CPU handles them at the instruction level, and how to work with them in x86 and ARM assembly.
What Is an Interrupt?
An interrupt is a signal that temporarily pauses the normal flow of instruction execution so the CPU can respond to an event, run a small piece of code called an interrupt handler (or interrupt service routine, ISR), and then resume exactly where it left off.
Interrupts generally fall into three categories:
- Hardware interrupts: triggered by external devices (keyboard, timer, network card, disk controller)
- Software interrupts: triggered deliberately by a program, often to request an operating system service (like a syscall)
- Exceptions/faults: triggered by the CPU itself due to an error condition (divide by zero, invalid memory access, illegal instruction)
Why Interrupts Matter
Without interrupts, a CPU would have to constantly poll every device to check if something happened, wasting enormous amounts of processing time. Interrupts flip this model: devices notify the CPU only when something actually needs attention, letting the processor spend the rest of its time doing useful work.
The Interrupt Handling Process
sequenceDiagram
participant Device as Hardware Device
participant CPU as CPU Core
participant Stack as Stack Memory
participant Handler as Interrupt Handler
Device->>CPU: Interrupt request signal
CPU->>Stack: Push flags, CS/segment info, instruction pointer
CPU->>Handler: Jump to interrupt handler address (via IDT/vector table)
Handler->>Handler: Execute interrupt service routine
Handler->>Stack: Pop saved state (IRET/ERET)
Stack->>CPU: Restore instruction pointer and flags
CPU->>CPU: Resume original program exactly where it left off
The Interrupt Vector Table (IVT) / Interrupt Descriptor Table (IDT)
The CPU doesn’t know in advance which code handles which interrupt — it looks this up in a table:
- x86 real mode: uses the Interrupt Vector Table (IVT), a simple array of 4-byte far pointers located at memory address 0
- x86 protected mode / x86-64: uses the Interrupt Descriptor Table (IDT), a more sophisticated structure containing gate descriptors with privilege levels and handler addresses
- ARM: uses a vector table at a fixed base address, containing branch instructions to each exception handler
Each entry in these tables corresponds to a specific interrupt number, and the CPU automatically indexes into the table using that number when the interrupt occurs.
Software Interrupts on x86: The Classic INT Instruction
In older x86 assembly (16-bit real mode, commonly taught with DOS-style examples), software interrupts were triggered directly:
mov ah, 0x09 ; DOS function: print string
lea dx, [message]
int 0x21 ; trigger software interrupt 0x21 (DOS services)
INT 0x21 causes the CPU to look up entry 0x21 in the IVT, jump to that handler, and run it — in this case, a DOS service routine that prints a string.
On modern 64-bit Linux, software interrupts have largely been replaced by the faster SYSCALL/SYSRET instruction pair, but the conceptual idea (a program deliberately triggers a controlled trap into privileged code) remains identical:
section .text
global _start
_start:
mov rax, 1 ; syscall number for sys_write
mov rdi, 1 ; file descriptor: stdout
lea rsi, [message]
mov rdx, msg_len
syscall ; enter kernel mode, execute the syscall handler
mov rax, 60 ; syscall number for sys_exit
xor rdi, rdi
syscall
section .data
message db "Hello from assembly!", 10
msg_len equ $ - message
Interrupt Handling in ARM Assembly
ARM uses exception levels and a vector table containing branch instructions. A simplified 32-bit ARM vector table might look like:
vector_table:
LDR PC, reset_handler_addr
LDR PC, undefined_handler_addr
LDR PC, swi_handler_addr ; software interrupt handler
LDR PC, prefetch_abort_addr
LDR PC, data_abort_addr
NOP
LDR PC, irq_handler_addr ; hardware interrupt
LDR PC, fiq_handler_addr ; fast interrupt
Each entry corresponds to a different type of exception, and the CPU automatically jumps to the appropriate one based on what triggered the exception.
In ARM64 (AArch64), exceptions are organized around Exception Levels (EL0–EL3), with dedicated vector tables per level, and handled with instructions like SVC (Supervisor Call) for software-triggered exceptions:
mov x8, #93 ; syscall number (exit, on Linux ARM64)
mov x0, #0 ; exit code
svc #0 ; trigger supervisor call, enters kernel handler
Saving and Restoring State: IRET vs ERET
When an interrupt handler finishes, the CPU must restore exactly what it saved before jumping into the handler:
| Architecture | Return Instruction | What It Restores |
|---|---|---|
| x86 / x86-64 | IRET / IRETQ | Instruction pointer, flags register, code segment |
| ARM (32-bit) | MOVS PC, LR (in interrupt context) | Program counter and processor mode/flags |
| ARM64 | ERET | Program counter (from ELR_ELx), processor state (from SPSR_ELx) |
Using a regular RET instead of IRET/ERET after an interrupt handler is a common and serious bug, since it fails to restore the processor flags correctly.
Interrupt Priority and Masking
Not all interrupts are treated equally. Most architectures support:
- Maskable interrupts: can be temporarily disabled (masked) using instructions like
CLI(Clear Interrupt Flag, x86) orCPSID(ARM), useful for protecting critical sections of code - Non-maskable interrupts (NMI): cannot be disabled, reserved for critical events like hardware failures
- Interrupt priority levels: higher-priority interrupts can preempt lower-priority ones currently being handled
cli ; disable interrupts (x86)
; critical section - safe from interruption
sti ; re-enable interrupts
Practical Use Cases
- Operating system scheduling: timer interrupts drive preemptive multitasking, forcing periodic context switches between processes
- Device drivers: hardware interrupts notify the OS when data is ready from a disk, network card, or peripheral
- System calls: software interrupts (or their modern equivalents) let user programs request privileged operations safely
- Fault handling: exceptions like page faults let the OS implement virtual memory, lazily loading pages only when accessed
Debugging Interrupt-Related Code
- Use a hardware-level debugger or emulator (like QEMU with GDB) to set breakpoints inside interrupt handlers, since regular debuggers sometimes struggle with instant context switches.
- Watch the flags/status register carefully — bugs in interrupt handlers frequently stem from failing to preserve or restore flags correctly.
- Check for stack corruption first when an interrupt handler misbehaves — since interrupts often use the current stack (or a dedicated one), a handler that pushes more than it pops will corrupt the return sequence.
Common Mistakes
- Returning with
RETinstead ofIRET/ERET, losing saved processor flags. - Not saving/restoring general-purpose registers used inside the handler, corrupting the interrupted program’s state.
- Leaving interrupts disabled for too long, causing missed events or degraded responsiveness.
- Re-enabling interrupts too early inside a handler, allowing reentrant interrupts to corrupt shared state.
Comparison: Polling vs. Interrupt-Driven I/O
| Approach | Advantages | Disadvantages |
|---|---|---|
| Polling | Simple to implement, predictable timing | Wastes CPU cycles constantly checking device status |
| Interrupt-driven | Efficient, CPU free to do other work until needed | More complex to implement correctly, risk of race conditions and reentrancy bugs |
Best Practices
- Keep interrupt handlers as short as possible — defer heavy processing to a later, non-interrupt context when feasible (a pattern operating systems call “bottom halves” or deferred procedure calls).
- Always save and restore every register the handler modifies.
- Be extremely careful with shared data accessed both inside and outside interrupt handlers — use appropriate synchronization or interrupt masking.
- Test handlers using an emulator (QEMU, Bochs) before deploying to real hardware, since interrupt bugs can be very hard to reproduce reliably.
FAQs
What’s the difference between an interrupt and an exception? An interrupt is typically triggered by an external event (hardware) or explicit request (software), while an exception is triggered by the CPU itself due to an error condition during instruction execution, like dividing by zero.
Why can’t I just use RET at the end of an interrupt handler? Because RET only restores the instruction pointer, not the processor flags and other state that were automatically saved when the interrupt occurred. You need IRET (x86) or ERET (ARM64) to restore everything correctly.
What is a non-maskable interrupt (NMI)? It’s an interrupt that cannot be disabled through normal interrupt masking, reserved for critical situations like hardware failure detection.
How do modern operating systems handle syscalls if INT is considered legacy? Most modern x86-64 systems use the dedicated SYSCALL/SYSRET instruction pair, which is significantly faster than the older INT 0x80-style software interrupt mechanism used in 32-bit Linux.
Summary and Key Takeaways
- Interrupts let a CPU respond to hardware events, software requests, and error conditions without constantly polling.
- The CPU automatically saves critical state (instruction pointer, flags) before jumping to a handler, looked up via an interrupt vector table or descriptor table.
- x86 uses
INT/IREThistorically andSYSCALL/SYSRETin modern 64-bit systems; ARM uses vector tables,SVC, andERET. - Interrupt masking allows critical sections of code to run without being interrupted, at the cost of potentially delaying important events if overused.
- Writing correct interrupt handlers requires careful attention to register preservation, stack balance, and minimizing time spent inside the handler.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manuals, Volume 3 (System Programming Guide) — Intel Corporation
- AMD64 Architecture Programmer’s Manual, Volume 2: System Programming — AMD
- ARM Architecture Reference Manual (ARMv7-A and ARMv8-A) — ARM Ltd.
- GNU Assembler (GAS) Documentation — Free Software Foundation