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:

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:

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:

cli                  ; disable interrupts (x86)
; critical section - safe from interruption
sti                   ; re-enable interrupts

Practical Use Cases

Debugging Interrupt-Related Code

Common Mistakes

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

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

References

Exit mobile version