Interrupt Handling in Assembly Language: How CPUs Respond to the Unexpected

Explain the concept of interrupt handling in Assembly language

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:

ArchitectureReturn InstructionWhat It Restores
x86 / x86-64IRET / IRETQInstruction pointer, flags register, code segment
ARM (32-bit)MOVS PC, LR (in interrupt context)Program counter and processor mode/flags
ARM64ERETProgram 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) or CPSID (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 RET instead of IRET/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

ApproachAdvantagesDisadvantages
PollingSimple to implement, predictable timingWastes CPU cycles constantly checking device status
Interrupt-drivenEfficient, CPU free to do other work until neededMore 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/IRET historically and SYSCALL/SYSRET in modern 64-bit systems; ARM uses vector tables, SVC, and ERET.
  • 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
Total
0
Shares

Leave a Reply

Previous Post
What are the advantages of using Assembly language

What Are the Advantages of Using Assembly Language? A Practical Look at Why Low-Level Coding Still Matters

Next Post
Describe the role of the stack pointer register

The Role of the Stack Pointer Register: A Complete Guide to Understanding the Assembly Stack

Related Posts