ARM64 OS Handbook
🔍

Chapter 11: Exception Handling

What You Will Learn in This Chapter
  • What exceptions are and what types exist on ARM64
  • How the exception vector table works in detail
  • How to save and restore context on exception entry and exit
  • How to read the exception syndrome register (ESR_EL1)
  • How to write a complete exception handler in assembly
  • How to dispatch exceptions to C handlers

11.1 What is an Exception?

An exception is an event that causes the CPU to stop normal execution and jump to a handler. Exceptions include system calls (SVC), interrupts (IRQ, FIQ), hardware errors (SError), and fault conditions (page faults, undefined instructions, alignment faults).

When an exception occurs at EL0 or EL1, the CPU:

  1. Saves the return address to ELR_EL1
  2. Saves the processor state to SPSR_EL1
  3. Updates ESR_EL1 with the reason for the exception
  4. Changes to EL1 (if coming from EL0)
  5. Jumps to the appropriate vector in the exception vector table

11.2 Exception Types

ARM64 classifies exceptions into four types, each with its own vector entry:

TypeCauseExample
SynchronousInstruction execution caused the exceptionSVC, page fault, undefined instruction, breakpoint
IRQNormal interrupt from a deviceTimer, UART, keyboard
FIQFast interrupt (higher priority)Used for fast devices or inter-processor interrupts
SErrorSystem error (asynchronous hardware error)Memory error, bus error

When the exception occurs at a given source level and stack pointer mode, the CPU picks the correct vector entry. The 16 entries cover 4 exception types x 4 source/target combinations.

11.3 The ESR_EL1 Register

The Exception Syndrome Register (ESR_EL1) tells us why the exception happened. The key fields are:

  • EC[31:26]: Exception Class (what type of exception)
  • IL[25]: Instruction Length (0 = 16-bit, 1 = 32-bit)
  • ISS[24:0]: Instruction Specific Syndrome (additional details)
EC ValueException Class
0x00Undefined instruction
0x01SVC (system call from AArch64)
0x15SVC (system call from AArch32)
0x20Instruction abort (page fault on instruction fetch)
0x21Instruction abort (same, EL0)
0x24Data abort (page fault on data access)
0x25Data abort (same, EL0)
0x30Breakpoint (hardware breakpoint or watchpoint)
/* Reading ESR_EL1 to determine exception cause */
mrs x0, ESR_EL1
lsr x1, x0, #26        /* extract EC field */
cmp x1, #0x01          /* is it an SVC? */
b.eq handle_svc
cmp x1, #0x24          /* is it a data abort? */
b.eq handle_data_abort
/* ... */

11.4 Context Save and Restore

When entering an exception handler, we must save all registers that we might modify. When returning, we must restore them. This is called context save/restore.

.macro save_context
    /* Allocate stack space for 34 registers (x0-x29, LR, SP, ELR, SPSR) */
    sub sp, sp, #(34 * 8)

    /* Save general-purpose registers */
    stp x0, x1, [sp, #16 * 0]
    stp x2, x3, [sp, #16 * 1]
    stp x4, x5, [sp, #16 * 2]
    stp x6, x7, [sp, #16 * 3]
    stp x8, x9, [sp, #16 * 4]
    stp x10, x11, [sp, #16 * 5]
    stp x12, x13, [sp, #16 * 6]
    stp x14, x15, [sp, #16 * 7]
    stp x16, x17, [sp, #16 * 8]
    stp x18, x19, [sp, #16 * 9]
    stp x20, x21, [sp, #16 * 10]
    stp x22, x23, [sp, #16 * 11]
    stp x24, x25, [sp, #16 * 12]
    stp x26, x27, [sp, #16 * 13]
    stp x28, x29, [sp, #16 * 14]

    /* Save the link register (x30) */
    mrs x0, ELR_EL1
    mrs x1, SPSR_EL1
    stp x0, x1, [sp, #16 * 15]
.endm

.macro restore_context
    /* Restore ELR_EL1 and SPSR_EL1 */
    ldp x0, x1, [sp, #16 * 15]
    msr ELR_EL1, x0
    msr SPSR_EL1, x1

    /* Restore general-purpose registers */
    ldp x0, x1, [sp, #16 * 0]
    ldp x2, x3, [sp, #16 * 1]
    ldp x4, x5, [sp, #16 * 2]
    ldp x6, x7, [sp, #16 * 3]
    ldp x8, x9, [sp, #16 * 4]
    ldp x10, x11, [sp, #16 * 5]
    ldp x12, x13, [sp, #16 * 6]
    ldp x14, x15, [sp, #16 * 7]
    ldp x16, x17, [sp, #16 * 8]
    ldp x18, x19, [sp, #16 * 9]
    ldp x20, x21, [sp, #16 * 10]
    ldp x22, x23, [sp, #16 * 11]
    ldp x24, x25, [sp, #16 * 12]
    ldp x26, x27, [sp, #16 * 13]
    ldp x28, x29, [sp, #16 * 14]

    /* Deallocate stack space */
    add sp, sp, #(34 * 8)
.endm

11.5 Complete Exception Handler in Assembly

.align 7
el1h_sync:
    save_context
    mrs x0, ESR_EL1       /* pass ESR_EL1 as argument */
    mrs x1, ELR_EL1       /* pass return address as argument */
    bl sync_handler       /* call C handler */
    restore_context
    eret

.align 7
el1h_irq:
    save_context
    bl irq_handler
    restore_context
    eret

.align 7
el1h_fiq:
    save_context
    bl fiq_handler
    restore_context
    eret

.align 7
el1h_serror:
    save_context
    bl serror_handler
    restore_context
    eret

11.6 Dispatching to C Handlers

#define ESR_EC_SHIFT  26
#define ESR_EC_MASK   0x3F

#define EC_SVC_64      0x15
#define EC_DATA_ABORT  0x24
#define EC_INST_ABORT  0x20
#define EC_UNKNOWN     0x00

void sync_handler(uint64_t esr, uint64_t elr) {
    uint64_t ec = (esr >> ESR_EC_SHIFT) & ESR_EC_MASK;

    switch (ec) {
        case EC_SVC_64:
            handle_syscall();
            break;
        case EC_DATA_ABORT:
            handle_page_fault(elr, esr);
            break;
        case EC_INST_ABORT:
            handle_inst_fault(elr);
            break;
        case EC_UNKNOWN:
            handle_undefined_instruction(elr, esr);
            break;
        default:
            uart_puts("Unhandled synchronous exception\r\n");
            uart_puthex(esr); uart_puts("\r\n");
            while (1) __asm__("wfi");
    }
}

void irq_handler(void) {
    /* Read the GIC to find which interrupt fired */
    volatile uint32_t *icc_hppir0 = (uint32_t *)0x08000000 + 0x4000 + 0x0018;
    uint32_t irq_id = *icc_hppir0 & 0x3FF;

    /* Dispatch to the appropriate handler */
    switch (irq_id) {
        case 30:  /* timer interrupt */
            timer_handler();
            break;
        case 33:  /* UART interrupt */
            uart_irq_handler();
            break;
        default:
            uart_puts("Unhandled IRQ: ");
            uart_puthex(irq_id);
            uart_puts("\r\n");
    }

    /* Signal end of interrupt to the GIC */
    volatile uint32_t *icc_eoir0 = (uint32_t *)0x08000000 + 0x4000 + 0x0010;
    *icc_eoir0 = irq_id;
}

11.7 Our Implementation

Our kernel's exception handling is structured in three layers:

  1. vectors.S: The vector table (16 entries, each a branch to a handler)
  2. entry.S: Context save/restore macros and handler entry points
  3. exception.c: C dispatchers that interpret ESR_EL1 and call appropriate handlers

The exception vector table is installed in kernel_main via msr VBAR_EL1, x0. Once installed, every exception (including the timer interrupt) is routed through our handlers. This is the foundation for system calls, page fault handling, and device drivers.

/* In kernel_main */
extern char exception_vectors[];
install_exception_vectors();  /* sets VBAR_EL1 */
__asm__("msr DAIFClr, #2");   /* unmask IRQs */

11.8 Exercises

Exercise 1: Print ESR on Exception

Modify your synchronous handler to print the ESR_EL1 value in hex for every exception. Trigger an SVC and observe the EC field.

Exercise 2: Undefined Instruction Handler

Add a handler for undefined instructions (EC=0x00) that prints the instruction address from ELR_EL1 and halts. Then execute an undefined instruction to test it.

Exercise 3: Skip the Faulting Instruction

For an SVC exception, advance ELR_EL1 by 4 bytes before returning so the SVC instruction is not re-executed. Verify by making a loop of SVC calls.

Exercise 4: Count Exceptions

Add a counter for each exception type. Print the counts every time an SVC is handled. Use static variables in the C handler.

11.9 Summary

Exceptions are the mechanism by which the CPU transfers control from user space to the kernel, and by which the kernel handles hardware events. The exception vector table at VBAR_EL1 directs each exception type to its handler. Context save/restore ensures that we can handle exceptions without corrupting the interrupted program's state. The ESR_EL1 register tells us exactly why the exception occurred, allowing us to dispatch to the correct C handler.