Tutorial · 03 · AArch64

Where did we wake up?

Our code is running — but at what privilege? ARMv8-A has four exception levels, and the firmware drops us into one of them. Today we use our new voice to ask the silicon exactly where we stand.

[o-o] |=| /_\

Yesterday I learned to speak. Today I ask my first real question — not of you, of myself: "Which privilege level am I in?" A machine that does not know its own standing cannot safely change it. Before we move, we orient.

Goal

Make the board print the exception level it booted into. One line of output — but it turns our UART from a greeting card into an instrument. For the first time the machine reports a fact about itself that we could not have known otherwise.

Four floors of privilege

An ARMv8-A processor does not treat all code as equal. It runs at one of four exception levels, EL0 through EL3, each more privileged than the last. Think of them as floors in a building, where the higher floors can see and control the lower ones:

Code only moves down these levels deliberately, and only moves up through a controlled event — an exception. That one-way structure is the backbone of every protection guarantee an operating system makes. We will lean on it heavily later; for now we just need to know which floor we started on.

Where the firmware leaves us

Here is a subtlety that bites everyone eventually: where you wake up depends on who booted you. On a real Raspberry Pi 4, the GPU firmware hands our kernel control at EL2. Some emulators and boot paths may start higher, at EL3. Same question, different starting floor.

This is not a detail to memorise — it is a reason to measure instead of assume. Rather than hardcode "we are at EL2," we ask the processor and let it tell us. Code that checks beats code that guesses.

Asking the processor

The current exception level is not something we infer — the CPU keeps it in a dedicated system register called CurrentEL. The level sits in bits [3:2], so shifting right by two gives a plain number from 0 to 3. We reach system registers from C through a one-line inline-assembly mrs ("move from system register").

// el.c — read the exception level we are running in

#include <stdint.h>

uint32_t current_el(void) {
    uint64_t v;
    __asm__ volatile ("mrs %0, CurrentEL" : "=r"(v));
    return (uint32_t)((v >> 2) & 3);   // bits [3:2] hold the level
}

That is the whole new idea. Everything else is the UART driver we already wrote in the last article, reused unchanged.

// kernel.c — report where we woke up

void     uart_init(void);
void     uart_putc(char c);
void     uart_puts(const char *s);
uint32_t current_el(void);

void kernel_main(void) {
    uart_init();
    uart_puts("Alquist is awake.\n");

    uart_puts("Booted at EL");
    uart_putc('0' + current_el());   // 0-3 fits in one digit
    uart_putc('\n');

    for (;;) { }
}

The downloadable example keeps this in one tiny kernel.c, next to the UART code from Tutorial 02. The boot file, linker script, and config.txt stay the same as the tested UART example, including the core_freq=500 mini-UART clock setting.

What actually happens

The firmware runs our entry, we set up the stack and drop into C, and uart_init brings the serial line up — all just like last time. Then current_el reads one register the CPU has been maintaining all along, and we turn that 0–3 value into a single ASCII digit by adding it to '0'.

Run it on real Raspberry Pi 4 hardware and you should see Booted at EL2. The important part is not the number itself; it is that the kernel asked the processor instead of guessing. We now have a way to know rather than hope.

This article is the measuring half of the exception-level story. The next one is the acting half: once we know where firmware left us, we can deliberately move the kernel to the level where it belongs.

[^-^] |=| /_\

EL2 on the bench — good, now I know my own footing. But the kernel I am meant to be belongs one floor lower, at EL1: that is where memory translation, system calls, and the timer are designed to live. Next we make the deliberate climb down — from wherever we woke up to exactly where we want to stand.

Example package

The full package includes the source files, config.txt, a Makefile, and a prebuilt kernel8.img: download where-did-we-wake-up-example.zip.

Try it

Wire up the serial adapter exactly as in the previous article (GND–GND, adapter RX to Pi GPIO14), open your terminal at 115200 8N1, and power the board. You should see two lines:

Alquist is awake.
Booted at EL2

From now on, Alquist orients itself by measurement, not assumption. Next, we put that knowledge to work and drop down to EL1 — the floor a kernel calls home.