rust-raspberrypi-OS-tutorials/0C_exception_levels
Andre Richter f13e6e4513
Change to new aarch64-unknown-none-softloat target.
Also, add safety docs where demanded by clippy.
2019-10-03 21:04:20 +02:00
..
.cargo Change to new aarch64-unknown-none-softloat target. 2019-10-03 21:04:20 +02:00
raspi3_boot Change to new aarch64-unknown-none-softloat target. 2019-10-03 21:04:20 +02:00
src Use refactored Makefile from JTAG tutorial everywhere. 2019-03-10 17:06:29 +01:00
Cargo.lock Update toolchain and bump dependency versions 2019-09-20 13:55:49 +02:00
Cargo.toml Update toolchain and bump dependency versions 2019-09-20 13:55:49 +02:00
kernel8 Change to new aarch64-unknown-none-softloat target. 2019-10-03 21:04:20 +02:00
kernel8.img Update toolchain and bump dependency versions 2019-09-20 13:55:49 +02:00
link.ld tutorials++ to account for JTAG tutorial 2019-03-10 17:06:29 +01:00
Makefile Change to new aarch64-unknown-none-softloat target. 2019-10-03 21:04:20 +02:00
README.md Change to new aarch64-unknown-none-softloat target. 2019-10-03 21:04:20 +02:00

Tutorial 0C - Exception Levels

In AArch64, there are four so-called exception levels:

Exception Level Typically used for
EL0 Userspace applications
EL1 OS Kernel
EL2 Hypervisor
EL3 Low-Level Firmware

If you are familiar with the x86 architecture, ELs are the counterpart to privilege rings.

At this point, I strongly recommend that you glimpse over Chapter 3 of the Programmers Guide for ARMv8-A before you continue. It gives a concise overview about the topic.

Scope of this tutorial

If you set up your SD Card exactly like mentioned in the repository's top-level README, our binary will start executing in EL2. Since we have an OS-focus, we will now write code that will cause a transition into the more appropriate EL1.

Checking for EL2 in the entrypoint

First of all, we need to ensure that we actually run in EL2 before we can call respective code to transition to EL1:

/// Entrypoint of the processor.
///
/// Parks all cores except core0 and checks if we started in EL2. If
/// so, proceeds with setting up EL1.
#[link_section = ".text.boot"]
#[no_mangle]
pub unsafe extern "C" fn _boot_cores() -> ! {
    use cortex_a::{asm, regs::*};

    const CORE_0: u64 = 0;
    const CORE_MASK: u64 = 0x3;
    const EL2: u32 = CurrentEL::EL::EL2.value;

    if (CORE_0 == MPIDR_EL1.get() & CORE_MASK) && (EL2 == CurrentEL.get()) {
        setup_and_enter_el1_from_el2()
    }

    // if not core0 or EL != 2, infinitely wait for events
    loop {
        asm::wfe();
    }
}

If this is the case, we continue with preparing the EL2 -> EL1 transition in setup_and_enter_el1_from_el2().

Transition preparation

Since EL2 is more privileged than EL1, it has control over various processor features and can allow or disallow EL1 code to use them. One such example is access to timer and counter registers. We are already using them since tutorial 09_delays, so we want to keep them. Therefore we set the respective flags in the Counter-timer Hypervisor Control register and additionally set the virtual offset to zero so that we get the real physical value everytime:

// Enable timer counter registers for EL1
CNTHCTL_EL2.write(CNTHCTL_EL2::EL1PCEN::SET + CNTHCTL_EL2::EL1PCTEN::SET);

// No offset for reading the counters
CNTVOFF_EL2.set(0);

Next, we configure the Hypervisor Configuration Register such that EL1 should actually run in AArch64 mode, and not in AArch32, which would also be possible.

// Set EL1 execution state to AArch64
HCR_EL2.write(HCR_EL2::RW::EL1IsAarch64;

Returning from an exception that never happened

There is actually only one way to transition from a higher EL to a lower EL, which is by way of executing the ERET instruction.

This instruction will copy the contents of the Saved Program Status Register - EL2 to Current Program Status Register - EL1 and jump to the instruction address that is stored in the Exception Link Register - EL2.

This is basically the reverse of what is happening when an exception is taken. You'll learn about it in tutorial 10_exception_groundwork.

// Set up a simulated exception return.
//
// First, fake a saved program status, where all interrupts were
// masked and SP_EL1 was used as a stack pointer.
SPSR_EL2.write(
    SPSR_EL2::D::Masked
        + SPSR_EL2::A::Masked
        + SPSR_EL2::I::Masked
        + SPSR_EL2::F::Masked
        + SPSR_EL2::M::EL1h,
);

// Second, let the link register point to reset().
ELR_EL2.set(reset as *const () as u64);

As you can see, we are populating ELR_EL2 with the address of the reset() function that we earlier used to call directly from the entrypoint.

Finally, we set the stack pointer for SP_EL1 and call ERET:

// Set up SP_EL1 (stack pointer), which will be used by EL1 once
// we "return" to it.
SP_EL1.set(STACK_START);

// Use `eret` to "return" to EL1. This will result in execution of
// `reset()` in EL1.
asm::eret()

Are we stackless?

We just wrote a big rust function, setup_and_enter_el1_from_el2(), that is executed in a context where we do not have a stack yet. We should double-check the generated machine code:

ferris@box:~$ make objdump
cargo objdump --target aarch64-unknown-none-softfloat -- -disassemble -print-imm-hex kernel8

kernel8:	file format ELF64-aarch64-little

Disassembly of section .text:
raspi3_boot::setup_and_enter_el1_from_el2::hf5d23e5bead7ee4e:
   808bc:	e8 03 1f aa 	mov	x8, xzr
   808c0:	e9 07 00 32 	orr	w9, wzr, #0x3
   808c4:	09 e1 1c d5 	msr	CNTHCTL_EL2, x9
   808c8:	68 e0 1c d5 	msr	CNTVOFF_EL2, x8
   808cc:	08 00 00 90 	adrp	x8, #0x0
   808d0:	ea 03 01 32 	orr	w10, wzr, #0x80000000
   808d4:	0a 11 1c d5 	msr	HCR_EL2, x10
   808d8:	ab 78 80 52 	mov	w11, #0x3c5
   808dc:	0b 40 1c d5 	msr	SPSR_EL2, x11
   808e0:	ec 03 0d 32 	orr	w12, wzr, #0x80000
   808e4:	08 21 22 91 	add	x8, x8, #0x888
   808e8:	28 40 1c d5 	msr	ELR_EL2, x8
   808ec:	0c 41 1c d5 	msr	SP_EL1, x12
   808f0:	e0 03 9f d6 	eret

Looks good! Thanks zero-overhead abstractions in the cortex-a crate! 😍

Testing

In main.rs, we added some tests to see if access to the counter timer registers is actually working, and if the mask bits in SPSR_EL2 made it to EL1 as well:

ferris@box:~$ make raspboot

[0] UART is live!
[1] Press a key to continue booting... Greetings fellow Rustacean!
[i] Executing in EL: 1

Testing EL1 access to timer registers:
    Delaying for 3 seconds now.
    1..2..3
    Works!

Checking interrupt mask bits:
    D: Masked.
    A: Masked.
    I: Masked.
    F: Masked.