Chapter 11: Exception Handling
- 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:
- Saves the return address to
ELR_EL1 - Saves the processor state to
SPSR_EL1 - Updates
ESR_EL1with the reason for the exception - Changes to EL1 (if coming from EL0)
- 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:
| Type | Cause | Example |
|---|---|---|
| Synchronous | Instruction execution caused the exception | SVC, page fault, undefined instruction, breakpoint |
| IRQ | Normal interrupt from a device | Timer, UART, keyboard |
| FIQ | Fast interrupt (higher priority) | Used for fast devices or inter-processor interrupts |
| SError | System 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 Value | Exception Class |
|---|---|
| 0x00 | Undefined instruction |
| 0x01 | SVC (system call from AArch64) |
| 0x15 | SVC (system call from AArch32) |
| 0x20 | Instruction abort (page fault on instruction fetch) |
| 0x21 | Instruction abort (same, EL0) |
| 0x24 | Data abort (page fault on data access) |
| 0x25 | Data abort (same, EL0) |
| 0x30 | Breakpoint (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:
- vectors.S: The vector table (16 entries, each a branch to a handler)
- entry.S: Context save/restore macros and handler entry points
- 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.