armviz

Exception levels and what they cost

AArch64 defines four exception levels: EL0, EL1, EL2 and EL3. The number is one higher than you might expect because the architecture supports virtualisation, and the hypervisor needs a level of its own between the kernel and the hardware.

CurrentEL reports which one you are running at, and it is read-only from every level.

The levels

EL0 is where ordinary application code runs. It has no privileges at all: access to system registers is trapped, and by default the only system registers it can reach are the ones an operating system deliberately allows.

EL1 is the operating system kernel. It owns physical memory, interrupts and processes. On a system without virtualisation this is the highest level in use.

EL2 is the hypervisor. It exists so that a guest operating system can be given what looks like the whole machine, while the hypervisor keeps the hardware to itself. HCR_EL2 decides what a guest is allowed to do.

EL3 is the most privileged level, holding the keys to the machine. It is what lets firmware and a secure monitor coexist without trusting each other, and it is the reason that most systems run their boot firmware at this level.

What actually changes at a boundary

Crossing between levels is not a function call. It is an exception: the processor saves state, switches stacks and privilege, and jumps to a vector table. The code that resumes afterwards is not the code that was interrupted, and it does not simply return.

On taking an exception the hardware writes two registers. ELR_EL1 holds the address to come back to, and SPSR_EL1 holds the saved processor state. VBAR_EL1 says where the vector table lives, and one of sixteen entries is chosen based on the exception type and which level it came from.

The cost is that a handler is a separate execution context with its own stack. It cannot trust anything in registers, and it cannot return the way a function returns. That is why handlers start by pushing registers onto their own stack and end with an explicit exception return instruction.

Why not just have two levels

Because a hypervisor without its own level has to be EL1, sharing the kernel's privilege. A bug in the hypervisor then compromises the kernel, and a guest can attack the hypervisor's own memory. Giving EL2 its own register file for the system controls it needs means a compromised guest can only attack the guest's own view of the machine.

The cost is a real complexity cost. Every system register has a documented access rule per level, and code that wants to be portable across virtualised systems has to check CurrentEL rather than assuming. The access tables throughout the system register pages in this project exist because that distinction shows up again and again.

Reading the access rules

When a system register page here shows a dash for a level, it means the register is not reachable from that level at all, rather than that access is forbidden but recoverable. Trapping is the common case: TTBR0_EL1 is reachable from EL0 in the sense that attempting it raises an exception which the kernel can service on the program's behalf. That indirection is what lets a sandboxed program ask the kernel to read its own page tables without being able to write them.