Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/scripts/check-british-spelling.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ ALLOWED_PATTERNS=(
'FORCE_COLOR|NO_COLOR' # de facto environment-variable standards
'--color' # command-line flags of external tools
'colorspace|ColorSpace' # graphics API names (Vulkan, DRM)
'virtualization=' # QEMU -machine property name, quoted literally
)

allow_re=$(IFS='|'; echo "${ALLOWED_PATTERNS[*]}")
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci_main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ jobs:
set -euo pipefail
cargo xtask boot-test --arch aarch64 --expect "Kaya!"
cargo xtask boot-test --arch aarch64 --release --expect "Kaya!"
cargo xtask boot-test --arch aarch64 --features provoke-exception --expect "BRK instruction"

ci-success:
name: CI Success
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/ci_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,19 @@ jobs:
# -kernel, so that path needs a UEFI stub before it can boot at all.
# `cargo xtask boot-test --arch x86_64` says so explicitly rather
# than failing obscurely.
# Two boots: the ordinary path must greet, and the provoked path
# must fault into the exception reporter and decode the BRK — so the
# vector table is proven on every pull request, not trusted.
- name: Boot in the devcontainer and expect the greeting
uses: devcontainers/ci@513af61f4de4f75d37e4438f184ba4358f0fc1ca # v0.3.1900000450
with:
imageName: ${{ env.DEVCONTAINER_IMAGE }}
cacheFrom: ${{ env.DEVCONTAINER_IMAGE }}
push: never
runCmd: cargo xtask boot-test --arch aarch64 --expect "Kaya!"
runCmd: |
set -euo pipefail
cargo xtask boot-test --arch aarch64 --expect "Kaya!"
cargo xtask boot-test --arch aarch64 --features provoke-exception --expect "BRK instruction"

ci-success:
name: CI Success
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@ Release codenames follow the six Noongar seasons — Birak, Bunuru, Djeran, Maku

### Added

- **Exception vectors, and a reporter that says what went wrong in one line** — the instrument the
soft-float bug had to be diagnosed without. `boot.s` installs a 2 KiB-aligned sixteen-entry vector
table into `VBAR_EL1` before the first Rust instruction runs, so even the earliest fault is reported
rather than vectoring to address zero and dying mute. Every entry routes to a reporter that prints
which vector fired, the exception class decoded into a sentence — EC 0x07 names `CPACR_EL1.FPEN`
outright — and raw `ESR`/`ELR`/`FAR`/`SPSR`, then halts.
- The common stub forces `SPSel` back to SP_ELx first, so even the never-used SP_EL0 group lands
on a real stack instead of the uninitialised SP_EL0.
- **Self-testing in CI**: a `provoke-exception` feature (never on by default) boots, greets, then
executes `brk #0`; the boot job greps the console for the decoded "BRK instruction" report. The
handler is proven on every pull request, not trusted.
- `cargo xtask` grows `--features` passthrough for exactly that.
- Terminal by design: until there is a scheduler, every exception is a report and a halt —
recovery is policy, and there is nothing yet to recover to. When the timer interrupt arrives,
the IRQ entries grow a real save/restore frame; the clobber-freely discipline in `vectors.s` is
documented as ending on that day.
- The EL1 assumption in `boot.s` is now load-bearing (`VBAR_EL1`, `SPSel`) and its header says so:
an explicit CurrentEL check and EL2 descent are owed before real hardware or
`virtualization=on`.
- **The kernel boots.** On QEMU aarch64 `virt` it reaches the PL011 UART and
prints `Kaya!` and the resident critter (constitution §8). Nothing else: no
scheduler, no IPC, no capability table, no MMU. What it establishes is the chain
Expand Down
6 changes: 6 additions & 0 deletions kernel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,11 @@ path = "src/main.rs"
test = false
bench = false

[features]
# Boot, greet, then take a deliberate BRK — so the boot self-test can prove the
# exception vector table and reporter work end to end. Never on by default;
# enabled by `cargo xtask boot-test --features provoke-exception`.
provoke-exception = []

[lints]
workspace = true
23 changes: 18 additions & 5 deletions kernel/src/arch/aarch64/boot.s
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@
// Entry state on QEMU 'virt':
// - The image is loaded at 0x4000_0000 and entered at _start with the MMU off,
// caches off, and interrupts masked.
// - Execution begins at EL1 by default. This stub does not care which
// exception level it is at, because it only touches memory-mapped I/O and
// never programs a system register that is EL-specific. That stops being
// true the moment the MMU or the exception vectors are set up, at which
// point this stub must gain an explicit EL2-to-EL1 descent.
// - Execution begins at EL1: QEMU's built-in loader enters a `-kernel` image
// at EL1 unless the machine is created with virtualization=on, and the
// xtask harness never passes that. This stub now DOES program EL1-specific
// registers (VBAR_EL1 below; SPSel in the vector stub), so the EL1
// assumption is load-bearing. Before real hardware or virtualization=on,
// this stub owes an explicit CurrentEL check and EL2-to-EL1 descent —
// at EL2 the writes below would be legal but useless, and a later fault
// would vector through VBAR_EL2 = 0 into exactly the silent death the
// table exists to prevent.
// - Every core enters here simultaneously. Only the one with MPIDR_EL1.Aff0
// == 0 continues; the rest are parked until there is a scheduler able to
// receive them.
Expand Down Expand Up @@ -53,6 +57,15 @@ _start:
b .Lzero_bss
.Lbss_zeroed:

// Install the exception vector table before entering Rust, so that even
// the first Rust instruction faults diagnosably instead of vectoring to
// address zero. VBAR_EL1 requires 2 KiB alignment; vectors.s guarantees it.
// The isb orders the write before any instruction that could fault.
adrp x1, __vectors
add x1, x1, :lo12:__vectors
msr vbar_el1, x1
isb

// Into Rust. The architecture-specific entry shim calls the kernel proper.
bl rust_entry

Expand Down
111 changes: 111 additions & 0 deletions kernel/src/arch/aarch64/exception.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: GPL-3.0-or-later

//! The exception reporter — the instrument every later subsystem is debugged
//! with.
//!
//! Until there is a scheduler, every exception is terminal: the only job is to
//! say *what* happened, *where*, and *why*, precisely enough that no one ever
//! again spends an afternoon in a QEMU instruction trace working out that
//! `CPACR_EL1.FPEN` was zero. That bug — the kernel dying one instruction
//! before its first console character, on an FP/SIMD trap to a vector table
//! that did not exist — is the reason this module exists and the standard its
//! output is held to: the decoded line for exception class 0x07 names the
//! register that causes it.
//!
//! The reporter prints line by line, on the deliberately lock-free console,
//! for the same reason the panic handler does: when the machine is dying, half
//! a message on the wire is evidence, and a lock is a way to hang instead of
//! speak. It never returns — recovery is a policy decision, and there is
//! nothing yet to recover to.
//!
//! Designated `unsafe` module (`kernel/src/arch/**`): the entry point below
//! carries `#[unsafe(no_mangle)]` so the vector stub in `vectors.s` can reach
//! it by name.
#![allow(unsafe_code)]

use crate::console::kprintln;

/// Names the table entry an exception arrived through.
///
/// The vector index encodes group (bits 3:2) and kind (bits 1:0), matching the
/// architectural table layout in `vectors.s`.
const fn vector_name(index: u64) -> (&'static str, &'static str) {
let group = match index >> 2 {
0 => "current EL, SP_EL0",
1 => "current EL, SP_ELx",
2 => "lower EL, AArch64",
_ => "lower EL, AArch32",
};
let kind = match index & 0b11 {
0 => "synchronous",
1 => "IRQ",
2 => "FIQ",
_ => "SError",
};
(kind, group)
}

/// Decodes `ESR_EL1.EC` (bits 31:26) into the sentence a person needs.
///
/// Only classes this kernel can plausibly meet are named; everything else
/// reports its raw value rather than guessing. The list grows as subsystems
/// do — a decoded class costs one line here and saves a datasheet lookup at
/// the worst possible moment there.
const fn exception_class_name(ec: u64) -> &'static str {
match ec {
0b00_0000 => "unknown reason",
0b00_0001 => "trapped WFI/WFE",
0b00_0111 => {
"FP/SIMD access trapped by CPACR_EL1.FPEN — the kernel is soft-float; \
something emitted a vector instruction"
}
0b00_1110 => "illegal execution state",
0b01_0101 => "SVC (AArch64 supervisor call)",
0b01_1000 => "trapped MSR/MRS or system instruction",
0b10_0000 => "instruction abort from lower EL (execute fault)",
0b10_0001 => "instruction abort, same EL (execute fault — jumped somewhere unmapped?)",
0b10_0010 => "PC alignment fault",
0b10_0100 => "data abort from lower EL (read/write fault)",
0b10_0101 => "data abort, same EL (read/write fault — FAR holds the address)",
0b10_0110 => "SP alignment fault",
0b10_1100 => "trapped floating-point exception (AArch64)",
0b10_1111 => "SError interrupt",
0b11_0000 => "breakpoint from lower EL",
0b11_0001 => "breakpoint, same EL",
0b11_0010 => "software step from lower EL",
0b11_0011 => "software step, same EL",
0b11_0100 => "watchpoint from lower EL",
0b11_0101 => "watchpoint, same EL",
0b11_1100 => "BRK instruction (AArch64)",
_ => "unrecognised exception class — decode ESR against the Arm ARM",
}
}

/// The exception reporter. Reached only from the vector stubs in `vectors.s`.
///
/// Prints the decoded exception, then halts this core for good. Written as
/// separate lines so a fault partway through the report still leaves the
/// earlier lines on the wire.
///
/// # Safety
///
/// Not for calling from Rust — `vectors.s` branches here with the syndrome
/// registers marshalled into the argument registers. The `no_mangle` exists
/// solely so the assembly can name it.
#[unsafe(no_mangle)]
extern "C" fn aarch64_exception(index: u64, esr: u64, elr: u64, far: u64, spsr: u64) -> ! {
let (kind, group) = vector_name(index);
let ec = (esr >> 26) & 0x3f;

kprintln!();
kprintln!("[EXCEPTION] {kind}, {group} (vector {index})");
kprintln!(" EC = {ec:#04x} — {}", exception_class_name(ec));
kprintln!(" ESR = {esr:#018x}");
kprintln!(" ELR = {elr:#018x} (faulting PC)");
kprintln!(" FAR = {far:#018x}");
kprintln!(" SPSR = {spsr:#018x}");
kprintln!();
kprintln!("Core halted.");

super::halt()
}
29 changes: 26 additions & 3 deletions kernel/src/arch/aarch64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
//! `// SAFETY:` justification.
#![allow(unsafe_code)]

mod exception;
mod uart;

// The boot stub is assembled by LLVM as part of this crate rather than by an
// external assembler, so the toolchain stays exactly as pinned — no host `as`
// creeps into the build.
// The boot stub and the exception vector table are assembled by LLVM as part
// of this crate rather than by an external assembler, so the toolchain stays
// exactly as pinned — no host `as` creeps into the build. boot.s installs the
// table into VBAR_EL1 before Rust runs; the table's stubs call into
// `exception::aarch64_exception`.
core::arch::global_asm!(include_str!("boot.s"));
core::arch::global_asm!(include_str!("vectors.s"));

/// The Rust entry point, called by `_start` in `boot.s` once there is a stack
/// and `.bss` has been zeroed.
Expand All @@ -36,6 +40,25 @@ pub(super) fn console_write_str(s: &str) {
uart::write_str(s);
}

/// Deliberately takes a synchronous exception, to prove the vector table and
/// reporter work end to end.
///
/// `brk #0` is the canonical choice: unconditional, undefined-behaviour-free,
/// and it arrives at the reporter as exception class 0x3C ("BRK instruction"),
/// which the boot self-test greps the console for. Compiled only under the
/// `provoke-exception` feature, which nothing enables by default.
#[cfg(feature = "provoke-exception")]
pub(super) fn provoke_exception() -> ! {
// SAFETY: `brk #0` has no operands and no memory effects; its entire
// purpose is to raise a synchronous exception, which the vector table
// installed by boot.s routes to the never-returning reporter. `noreturn`
// is sound because execution cannot proceed past a BRK whose handler
// halts.
unsafe {
core::arch::asm!("brk #0", options(nomem, nostack, noreturn));
}
}

/// Parks this core in a low-power wait state, permanently.
pub(super) fn halt() -> ! {
loop {
Expand Down
118 changes: 118 additions & 0 deletions kernel/src/arch/aarch64/vectors.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// AArch64 exception vector table.
//
// The architecture dictates nearly everything here, which is why this file is
// short: VBAR_EL1 points at a 2 KiB-aligned table of sixteen entries, each 128
// bytes apart, in four groups of four. Groups, in order: exceptions taken from
// the current EL while on SP_EL0; from the current EL on SP_ELx; from a lower
// EL running AArch64; from a lower EL running AArch32. Within each group:
// synchronous, IRQ, FIQ, SError.
//
// Every entry does the same two things — load its own index into x0 and branch
// to the common stub — because at this stage of the kernel every exception has
// the same meaning: something unexpected happened, and the only job is to say
// so on the console with enough precision that nobody spends an afternoon in
// an instruction trace. The handler never returns, so registers are clobbered
// freely and no frame is saved. This table is an instrument, not a
// context-switch path: when IRQs become real (timer, scheduler), the relevant
// entries grow a full register save and a return path, and the clobbering
// stops being acceptable.
//
// Lower-EL entries are unreachable until userspace exists, and the SP_EL0
// group is unreachable while the kernel stays on SP_ELx — but the architecture
// requires the slots, and wiring them to the reporter costs nothing. The
// common stub forces SPSel back to SP_ELx before touching memory, so even a
// freak SP_EL0 entry lands on the boot stack rather than on SP_EL0, which is
// never initialised.
//
// Register discipline in the stub: x0 carries the vector index from the entry;
// x1-x4 receive the four syndrome registers; nothing is preserved, because
// aarch64_exception is declared `-> !`.

.section .text.vectors, "ax"
.balign 2048
.globl __vectors

__vectors:
// --- Group 0: current EL, SP_EL0 ---------------------------------------
.balign 0x80
mov x0, #0 // synchronous
b .Lvector_common
.balign 0x80
mov x0, #1 // IRQ
b .Lvector_common
.balign 0x80
mov x0, #2 // FIQ
b .Lvector_common
.balign 0x80
mov x0, #3 // SError
b .Lvector_common

// --- Group 1: current EL, SP_ELx ---------------------------------------
// The group the kernel actually lives in today.
.balign 0x80
mov x0, #4 // synchronous
b .Lvector_common
.balign 0x80
mov x0, #5 // IRQ
b .Lvector_common
.balign 0x80
mov x0, #6 // FIQ
b .Lvector_common
.balign 0x80
mov x0, #7 // SError
b .Lvector_common

// --- Group 2: lower EL, AArch64 ----------------------------------------
// Unreachable until userspace exists.
.balign 0x80
mov x0, #8 // synchronous
b .Lvector_common
.balign 0x80
mov x0, #9 // IRQ
b .Lvector_common
.balign 0x80
mov x0, #10 // FIQ
b .Lvector_common
.balign 0x80
mov x0, #11 // SError
b .Lvector_common

// --- Group 3: lower EL, AArch32 ----------------------------------------
// Doubly unreachable: no userspace, and AArch32 is out of scope (§6).
.balign 0x80
mov x0, #12 // synchronous
b .Lvector_common
.balign 0x80
mov x0, #13 // IRQ
b .Lvector_common
.balign 0x80
mov x0, #14 // FIQ
b .Lvector_common
.balign 0x80
mov x0, #15 // SError
b .Lvector_common

.Lvector_common:
// Force SP_ELx selection so the Rust handler runs on the boot stack even
// if the exception arrived through the SP_EL0 group.
msr spsel, #1

// The four syndrome registers, in argument order for aarch64_exception:
// x1 = ESR_EL1 — what happened (exception class + syndrome)
// x2 = ELR_EL1 — where it happened (faulting PC)
// x3 = FAR_EL1 — which address, for aborts and alignment faults
// x4 = SPSR_EL1 — the interrupted state
mrs x1, esr_el1
mrs x2, elr_el1
mrs x3, far_el1
mrs x4, spsr_el1

bl aarch64_exception

// aarch64_exception is `-> !`; reaching here means the handler itself is
// broken. Park rather than fall through into whatever follows.
.Lvector_hang:
wfe
b .Lvector_hang
Loading