summaryrefslogtreecommitdiff
path: root/arch/riscv/include
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:31:46 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:31:46 +0800
commite90b72ec3082588b296956730bd2930cf8d5d671 (patch)
tree36ff6916af9b4176ea5a9662219d11e721f0b691 /arch/riscv/include
parent1767d390fbf3d0c65389de46caf0b6d43ef3b6ad (diff)
downloadlume-e90b72ec3082588b296956730bd2930cf8d5d671.tar.gz
lume-e90b72ec3082588b296956730bd2930cf8d5d671.tar.xz
refactor: promote new architecture to repository root
Archive the deprecated implementation under lume-legacy, make the new kernel the default root-level build, and update repository entry points without changing kernel behavior.
Diffstat (limited to 'arch/riscv/include')
-rw-r--r--arch/riscv/include/arch/config.h106
-rw-r--r--arch/riscv/include/arch/context.h33
-rw-r--r--arch/riscv/include/arch/cpu.h95
-rw-r--r--arch/riscv/include/arch/extable.h55
-rw-r--r--arch/riscv/include/arch/fpstate.h94
-rw-r--r--arch/riscv/include/arch/mmu.h78
-rw-r--r--arch/riscv/include/arch/pmap.h49
-rw-r--r--arch/riscv/include/arch/random.h27
-rw-r--r--arch/riscv/include/arch/sbi.h111
-rw-r--r--arch/riscv/include/arch/tlb.h19
-rw-r--r--arch/riscv/include/arch/trap.h94
-rw-r--r--arch/riscv/include/arch/trapframe.h140
-rw-r--r--arch/riscv/include/asm/csr.h37
-rw-r--r--arch/riscv/include/asm/page.h62
-rw-r--r--arch/riscv/include/asm/pgtable.h55
-rw-r--r--arch/riscv/include/asm/sections.h50
16 files changed, 1105 insertions, 0 deletions
diff --git a/arch/riscv/include/arch/config.h b/arch/riscv/include/arch/config.h
new file mode 100644
index 0000000..8ae0502
--- /dev/null
+++ b/arch/riscv/include/arch/config.h
@@ -0,0 +1,106 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * RISC-V Architecture Configuration
+ *
+ * These constants define the architecture-specific memory layout.
+ * Each arch/ implementation must provide this header with the same symbols.
+ *
+ * KASLR: At boot, kaslr_init() randomises g_direct_map_base and
+ * g_vmemmap_base. All MI code must read these runtime variables
+ * (via pa_to_va / vmm helpers) rather than the static defaults.
+ */
+
+#include <lume/types.h>
+
+namespace arch {
+
+/* ================================================================
+ * Default (compile-time) virtual addresses
+ *
+ * SV39 gives a 512GB virtual address space.
+ * Higher-half layout (defaults before KASLR):
+ *
+ * 0xFFFFFFC0_00000000 (256) Direct map (PA+offset)
+ * 0xFFFFFFD0_00000000 (320) Vmemmap (Page array)
+ *
+ * The 64GB gap between them is the KASLR slide range.
+ * ================================================================ */
+inline constexpr uint64 kDirectMapBaseDefault = 0xFFFFFFC000000000ULL;
+inline constexpr uint64 kVmemmapBaseDefault = 0xFFFFFFD000000000ULL;
+
+/* Physical memory base (where RAM starts on RISC-V virt platform) */
+inline constexpr uint64 kPhysBase = 0x80000000ULL;
+
+/* Kernel physical load address (= PHYS_BASE in linker script).
+ * Everything in [kPhysBase, kPhysStart) is firmware territory (PMP-protected).
+ * Analogous to Linux's CONFIG_PHYSICAL_START. */
+inline constexpr uint64 kPhysStart = 0x80200000ULL;
+
+/* ================================================================
+ * Runtime (post-KASLR) virtual addresses
+ *
+ * These start equal to the defaults above. If CONFIG_KASLR is
+ * enabled, kaslr_init() adds a random, 1GB-aligned offset
+ * to each, sliding both regions upward in VA space.
+ * ================================================================ */
+extern uint64 g_direct_map_base;
+extern uint64 g_vmemmap_base;
+
+/* ================================================================
+ * KASLR Layout Structure (Phase A: Unified KASLR State)
+ *
+ * This struct holds all KASLR results: the random seed, derived
+ * hash, selected slot, and the randomised virtual address bases.
+ * It serves as the single source of truth for all KASLR decisions
+ * and is filled once during boot and read-only thereafter.
+ * ================================================================ */
+
+enum class KaslrSeedSource : uint8 {
+ kFdt = 0, /* Loaded from FDT /random node */
+ kHardware = 1, /* From hardware RNG (e.g., RDRAND) */
+ kCycle = 2, /* Fallback: CPU cycle counter */
+};
+
+struct KaslrLayout {
+ /* Seed and provenance */
+ uint64 seed; /* 64-bit random seed value */
+ uint64 seed_hash; /* Non-plaintext derived hash (splitmix64) */
+ KaslrSeedSource source; /* Seed provenance (fdt/hw/cycle) */
+
+ /* Randomisation parameters */
+ uint64 slot; /* Slot index: [0, 63] */
+ uint64 slide_bytes; /* Byte offset: slot * 1GB */
+
+ /* Build-time policy */
+ uint32 policy_id; /* CONFIG_KASLR_POLICY_ID at build time */
+
+ /* Randomised base addresses (post-KASLR) */
+ uint64 direct_map_base; /* Direct map VA (kDirectMapBaseDefault + slide) */
+ uint64 vmemmap_base; /* Vmemmap VA (kVmemmapBaseDefault + slide) */
+
+ /* Validation flags (for debug logging) */
+ bool canonical_ok; /* All VA ranges passed canonical check */
+ bool overlap_ok; /* No overlaps detected between regions */
+};
+
+/* ================================================================
+ * KASLR entry point
+ *
+ * Must be called AFTER random_early_init() and BEFORE vmm_init().
+ * Fills g_kaslr_layout with randomised bases and self-check results.
+ * When CONFIG_KASLR is disabled this is a no-op.
+ * ================================================================ */
+extern KaslrLayout g_kaslr_layout;
+
+void kaslr_init();
+
+/* Helper to query the KASLR layout safely */
+static inline const KaslrLayout& kaslr_layout_get() {
+ return g_kaslr_layout;
+}
+
+} // namespace arch
diff --git a/arch/riscv/include/arch/context.h b/arch/riscv/include/arch/context.h
new file mode 100644
index 0000000..3d935fa
--- /dev/null
+++ b/arch/riscv/include/arch/context.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Context Switch Structure (RISC-V 64)
+ *
+ * Holds only callee-saved registers for voluntary (non-preemptive)
+ * context switches between kernel threads / tasks.
+ *
+ * Used by: arch::context_switch() in swtch.S
+ * Reference: docs/specs/arch_abstraction.md §3.1
+ */
+
+#include <lume/types.h>
+
+namespace arch {
+
+struct Context {
+ uint64 ra; // 0: Return address
+ uint64 sp; // 8: Stack pointer
+ uint64 s[12]; // 16: s0-s11 (callee-saved registers)
+};
+
+static_assert(sizeof(Context) == 112);
+
+// Assembly-implemented context switch.
+// Saves current CPU registers to old_ctx, loads next_ctx into CPU.
+// Defined in arch/riscv/trap/swtch.S
+extern "C" void context_switch(Context* old_ctx, Context* next_ctx);
+
+} // namespace arch
diff --git a/arch/riscv/include/arch/cpu.h b/arch/riscv/include/arch/cpu.h
new file mode 100644
index 0000000..8c573ec
--- /dev/null
+++ b/arch/riscv/include/arch/cpu.h
@@ -0,0 +1,95 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * CPU Control & Status Primitives (RISC-V 64)
+ *
+ * Provides interrupt enable/disable, CPU ID, timer, and halt.
+ * All functions are static inline — zero call overhead.
+ *
+ * Precondition: tp register must be initialized with CPU ID
+ * by entry.S before any of these functions are called.
+ *
+ * Reference: docs/specs/arch_abstraction.md §4.1
+ */
+
+#include <lume/types.h>
+
+/* RISC-V sstatus register bit definitions */
+#define SSTATUS_SIE (1UL << 1) /* Supervisor Interrupt Enable */
+#define SSTATUS_SPIE (1UL << 5) /* Supervisor Previous Interrupt Enable */
+#define SSTATUS_SPP (1UL << 8) /* Supervisor Previous Privilege (0=U, 1=S) */
+
+/* RISC-V sie register bit definitions */
+#define SIE_SSIE (1UL << 1) /* Supervisor Software Interrupt Enable */
+#define SIE_STIE (1UL << 5) /* Supervisor Timer Interrupt Enable */
+#define SIE_SEIE (1UL << 9) /* Supervisor External Interrupt Enable */
+
+namespace arch::cpu {
+
+// Get current CPU ID (from tp register, set by entry.S)
+static inline uint64 id() {
+ uint64 val;
+ __asm__ volatile("mv %0, tp" : "=r"(val));
+ return val;
+}
+
+// Enable interrupts (set sstatus.SIE = 1)
+static inline void intr_on() {
+ __asm__ volatile("csrs sstatus, %0" :: "r"(SSTATUS_SIE));
+}
+
+// Disable interrupts (clear sstatus.SIE = 0)
+static inline void intr_off() {
+ __asm__ volatile("csrc sstatus, %0" :: "r"(SSTATUS_SIE));
+}
+
+// Check if interrupts are currently enabled
+static inline bool intr_enabled() {
+ uint64 sstatus;
+ __asm__ volatile("csrr %0, sstatus" : "=r"(sstatus));
+ return (sstatus & SSTATUS_SIE) != 0;
+}
+
+// Halt CPU until next interrupt (wfi instruction)
+static inline void halt_until_interrupt() {
+ __asm__ volatile("wfi");
+}
+
+// Read hardware monotonic timer (rdtime instruction)
+// Returns ticks since boot; frequency from FDT timebase-frequency.
+static inline uint64 read_time() {
+ uint64 val;
+ __asm__ volatile("rdtime %0" : "=r"(val));
+ return val;
+}
+
+// Enable timer interrupt in sie (sie.STIE = 1)
+static inline void timer_intr_on() {
+ __asm__ volatile("csrs sie, %0" :: "r"(SIE_STIE));
+}
+
+// Disable timer interrupt in sie (sie.STIE = 0)
+static inline void timer_intr_off() {
+ __asm__ volatile("csrc sie, %0" :: "r"(SIE_STIE));
+}
+
+// Enable software interrupt in sie (sie.SSIE = 1) — needed for IPI
+static inline void software_intr_on() {
+ __asm__ volatile("csrs sie, %0" :: "r"(SIE_SSIE));
+}
+
+// Enable external interrupt in sie (sie.SEIE = 1) — needed for PLIC (UART, etc.)
+static inline void external_intr_on() {
+ __asm__ volatile("csrs sie, %0" :: "r"(SIE_SEIE));
+}
+
+// Clear S-mode software interrupt pending bit (sip.SSIP = 0)
+// Must be called in the IPI handler to acknowledge the interrupt.
+static inline void clear_sip_ssip() {
+ __asm__ volatile("csrc sip, %0" :: "r"(1UL << 1));
+}
+
+} // namespace arch::cpu
diff --git a/arch/riscv/include/arch/extable.h b/arch/riscv/include/arch/extable.h
new file mode 100644
index 0000000..eb2daac
--- /dev/null
+++ b/arch/riscv/include/arch/extable.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+#ifndef __ASSEMBLER__
+
+#include <lume/types.h>
+
+/*
+ * Exception table entry.
+ * insn: offset from this struct to the faulting instruction.
+ * fixup: offset from this struct to the fixup code.
+ *
+ * Using relative offsets avoids absolute relocations and saves space.
+ */
+struct ExceptionTableEntry {
+ int32 insn;
+ int32 fixup;
+};
+
+/* Search the exception table for a given PC. Returns fixup address, or 0 if not found. */
+uint64 search_extable(uint64 pc);
+
+/* Sorts the exception table at boot for binary search O(log N). */
+void extable_init();
+
+#endif /* __ASSEMBLER__ */
+
+/*
+ * EXTABLE macro for assembly usage.
+ * Appends an entry to the .extable section.
+ */
+#ifdef __ASSEMBLER__
+
+#define EXTABLE(insn, fixup) \
+ .pushsection .extable, "a"; \
+ .balign 4; \
+ .long insn - .; \
+ .long fixup - .; \
+ .popsection
+
+#else /* !__ASSEMBLER__ */
+
+/* Stringified macro for inline assembly */
+#define _ASM_EXTABLE(insn, fixup) \
+ ".pushsection .extable, \"a\"\n" \
+ ".balign 4\n" \
+ ".long " #insn " - .\n" \
+ ".long " #fixup " - .\n" \
+ ".popsection\n"
+
+#define EXTABLE(insn, fixup) _ASM_EXTABLE(insn, fixup)
+
+#endif /* __ASSEMBLER__ */
diff --git a/arch/riscv/include/arch/fpstate.h b/arch/riscv/include/arch/fpstate.h
new file mode 100644
index 0000000..4b42a22
--- /dev/null
+++ b/arch/riscv/include/arch/fpstate.h
@@ -0,0 +1,94 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * FPU State — Floating-Point Register Context
+ *
+ * Pre-reserved structure for RISC-V F/D extension floating-point state.
+ * MUST be allocated in every TaskControlBlock so that future FPU
+ * save/restore logic has a stable destination without restructuring TCB.
+ *
+ * RISC-V F/D extension state:
+ * - 32 FP registers (f0-f31), each 64 bits for D extension
+ * - fcsr (Floating-point Control and Status Register)
+ *
+ * sstatus.FS field management:
+ * - FS=0 (Off): FPU disabled, any FP instruction traps
+ * - FS=1 (Initial): FPU enabled, registers are initial state
+ * - FS=2 (Clean): FPU enabled, registers not modified since last save
+ * - FS=3 (Dirty): FPU enabled, registers modified (need save)
+ *
+ * Current status: Structure defined, save/restore NOT implemented.
+ * When implemented, trap_dispatch and swtch.S will use lazy
+ * save/restore based on sstatus.FS dirty tracking.
+ *
+ * Reference: RISC-V ISA Manual §3.1.6.5 (FS field)
+ */
+
+#include <lume/types.h>
+
+namespace arch {
+
+struct FpState {
+ uint64 f[32]; // f0-f31 (64-bit each for D extension)
+ uint32 fcsr; // Floating-point CSR (frm + fflags)
+ uint32 _pad; // Alignment padding
+};
+
+static_assert(sizeof(FpState) == 32 * 8 + 8, "FpState must be 264 bytes");
+
+/*
+ * sstatus.FS field constants
+ */
+inline constexpr uint64 kSstatusFS_Off = 0ULL << 13;
+inline constexpr uint64 kSstatusFS_Initial = 1ULL << 13;
+inline constexpr uint64 kSstatusFS_Clean = 2ULL << 13;
+inline constexpr uint64 kSstatusFS_Dirty = 3ULL << 13;
+inline constexpr uint64 kSstatusFS_Mask = 3ULL << 13;
+
+/*
+ * Check if sstatus indicates FPU state is dirty (needs save).
+ */
+inline bool fpu_state_dirty(uint64 sstatus) {
+ return (sstatus & kSstatusFS_Mask) == kSstatusFS_Dirty;
+}
+
+/*
+ * Check if FPU is enabled (FS != Off).
+ */
+inline bool fpu_enabled(uint64 sstatus) {
+ return (sstatus & kSstatusFS_Mask) != kSstatusFS_Off;
+}
+
+} // namespace arch
+
+/* Assembly routines for FPU register save/restore (fpu.S) */
+extern "C" void fpu_save(arch::FpState* state);
+extern "C" void fpu_restore(arch::FpState* state);
+
+namespace arch {
+
+/*
+ * Read sstatus.FS field (current FPU state)
+ */
+inline uint64 fpu_get_fs() {
+ uint64 sstatus;
+ __asm__ volatile("csrr %0, sstatus" : "=r"(sstatus));
+ return sstatus & kSstatusFS_Mask;
+}
+
+/*
+ * Set sstatus.FS field.
+ * @fs: one of kSstatusFS_Off, kSstatusFS_Initial, kSstatusFS_Clean, kSstatusFS_Dirty
+ */
+inline void fpu_set_fs(uint64 fs) {
+ __asm__ volatile(
+ "csrc sstatus, %0\n"
+ "csrs sstatus, %1\n"
+ :: "r"(kSstatusFS_Mask), "r"(fs)
+ );
+}
+
+} // namespace arch
diff --git a/arch/riscv/include/arch/mmu.h b/arch/riscv/include/arch/mmu.h
new file mode 100644
index 0000000..f4c9b26
--- /dev/null
+++ b/arch/riscv/include/arch/mmu.h
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * MMU Hardware Primitives (RISC-V SV39)
+ *
+ * Page table switching, TLB flushing, and fault address reading.
+ * All functions are static inline — zero call overhead.
+ *
+ * Note on SMP: flush_tlb_page() only flushes the LOCAL CPU's TLB.
+ * Cross-CPU TLB shootdown via IPI is not yet implemented.
+ *
+ * Reference: docs/specs/arch_abstraction.md §4.2
+ */
+
+#include <lume/types.h>
+
+/* SATP register: mode field for SV39 */
+#define SATP_SV39 (8UL << 60)
+
+/* SATP PPN mask (bits 43:0) */
+#define SATP_PPN_MASK ((1UL << 44) - 1)
+
+namespace arch::mmu {
+
+// Read the faulting virtual address from stval (set by HW on page fault)
+static inline uint64 get_fault_address() {
+ uint64 val;
+ __asm__ volatile("csrr %0, stval" : "=r"(val));
+ return val;
+}
+
+// Switch the top-level page table.
+// Takes a PHYSICAL ADDRESS (not PPN); internally shifts right by 12
+// and combines with SV39 mode bits before writing to satp.
+static inline void set_page_table(uint64 physical_pgdir_addr) {
+ uint64 ppn = physical_pgdir_addr >> 12;
+ uint64 satp_val = SATP_SV39 | ppn;
+ __asm__ volatile("csrw satp, %0" :: "r"(satp_val));
+ // Note: caller should call flush_tlb_all() after this
+}
+
+// Read the current page table physical address.
+// Extracts PPN from satp and converts back to physical address.
+static inline uint64 get_page_table() {
+ uint64 satp_val;
+ __asm__ volatile("csrr %0, satp" : "=r"(satp_val));
+ return (satp_val & SATP_PPN_MASK) << 12;
+}
+
+// Read the raw satp CSR value (mode + PPN).
+// Used when the caller needs the exact register value (e.g. for TrapFrame metadata).
+static inline uint64 read_satp() {
+ uint64 val;
+ __asm__ volatile("csrr %0, satp" : "=r"(val));
+ return val;
+}
+
+// Flush the entire TLB (all ASIDs, all addresses)
+static inline void flush_tlb_all() {
+ __asm__ volatile("sfence.vma zero, zero" ::: "memory");
+}
+
+// Flush a single TLB entry for the given virtual address
+static inline void flush_tlb_page(uint64 vaddr) {
+ __asm__ volatile("sfence.vma %0, zero" :: "r"(vaddr) : "memory");
+}
+
+// Flush the instruction cache.
+// Required after loading executable code into memory (e.g. ELF loading).
+// sfence.vma only flushes the TLB; fence.i is needed for the I-cache.
+static inline void flush_icache() {
+ __asm__ volatile("fence.i" ::: "memory");
+}
+
+} // namespace arch::mmu
diff --git a/arch/riscv/include/arch/pmap.h b/arch/riscv/include/arch/pmap.h
new file mode 100644
index 0000000..0b43b9e
--- /dev/null
+++ b/arch/riscv/include/arch/pmap.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * pmap — SV39 page table interface (MD layer)
+ *
+ * Namespace functions, zero runtime overhead.
+ * Implementation: arch/riscv/mm/pmap.cc
+ *
+ * Reference: docs/specs/vmm.md §3.2
+ */
+
+#include <lume/types.h>
+
+namespace pmap {
+
+/* PTE permission bits */
+inline constexpr uint64 PTE_V = 1ULL << 0;
+inline constexpr uint64 PTE_R = 1ULL << 1;
+inline constexpr uint64 PTE_W = 1ULL << 2;
+inline constexpr uint64 PTE_X = 1ULL << 3;
+inline constexpr uint64 PTE_U = 1ULL << 4;
+inline constexpr uint64 PTE_G = 1ULL << 5;
+inline constexpr uint64 PTE_A = 1ULL << 6;
+inline constexpr uint64 PTE_D = 1ULL << 7;
+inline constexpr uint64 PTE_COW = 1ULL << 8; /* Custom RSW bit for Copy-On-Write */
+
+uint64 create();
+void destroy(uint64 root_pa);
+int map(uint64 root_pa, uint64 va, uint64 pa, uint64 perm);
+int map_2mb(uint64 root_pa, uint64 va, uint64 pa, uint64 perm);
+void unmap(uint64 root_pa, uint64 va);
+int protect(uint64 root_pa, uint64 va, uint64 perm);
+int protect_range(uint64 root_pa, uint64 start, uint64 end, uint64 perm);
+bool lookup(uint64 root_pa, uint64 va, uint64* pa_out);
+uint64 lookup_pte(uint64 root_pa, uint64 va);
+bool is_cow(uint64 root_pa, uint64 va);
+int make_cow(uint64 root_pa, uint64 va);
+void activate(uint64 root_pa);
+
+/*
+ * Translate MI-layer VM_* permission flags to hardware PTE bits.
+ * Called by vma.cc when faulting in a page.
+ */
+uint64 vm_perm_to_pte(uint64 vm_perm);
+
+} // namespace pmap
diff --git a/arch/riscv/include/arch/random.h b/arch/riscv/include/arch/random.h
new file mode 100644
index 0000000..8c8c051
--- /dev/null
+++ b/arch/riscv/include/arch/random.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+#include <lume/types.h>
+
+namespace arch {
+
+/*
+ * Get a random 64-bit value from the hardware TRNG.
+ * Returns true if supported and successful, false otherwise.
+ */
+bool get_hw_random(uint64& value);
+
+/*
+ * Get the current high-resolution hardware cycle counter.
+ * Used as an entropy fallback (Jitter/Timer).
+ */
+inline uint64 get_cycles()
+{
+ uint64 cycles;
+ __asm__ volatile("rdcycle %0" : "=r"(cycles));
+ return cycles;
+}
+
+} // namespace arch
diff --git a/arch/riscv/include/arch/sbi.h b/arch/riscv/include/arch/sbi.h
new file mode 100644
index 0000000..80a6d7c
--- /dev/null
+++ b/arch/riscv/include/arch/sbi.h
@@ -0,0 +1,111 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * sbi.h — RISC-V Supervisor Binary Interface (SBI) wrappers
+ *
+ * Provides inline ecall wrappers for SBI calls from S-mode.
+ * Uses the SBI v1.0+ extension-based calling convention.
+ *
+ * Reference: RISC-V SBI Specification v2.0
+ */
+
+#include <lume/types.h>
+
+namespace arch::sbi {
+
+struct sbiret {
+ long error;
+ long value;
+};
+
+/* SBI error codes */
+inline constexpr long SBI_SUCCESS = 0;
+inline constexpr long SBI_ERR_NOT_SUPPORTED = -2;
+
+/* Extension IDs */
+inline constexpr int SBI_EXT_TIME = 0x54494D45; /* "TIME" */
+inline constexpr int SBI_EXT_IPI = 0x735049; /* "sPI" */
+inline constexpr int SBI_EXT_RFENCE = 0x52464E43; /* "RFNC" */
+inline constexpr int SBI_EXT_HSM = 0x48534D; /* "HSM" */
+
+static inline sbiret sbi_ecall(int ext, int fid, uint64 arg0 = 0,
+ uint64 arg1 = 0, uint64 arg2 = 0,
+ uint64 arg3 = 0, uint64 arg4 = 0,
+ uint64 arg5 = 0) {
+ sbiret ret;
+ register uint64 a0 asm("a0") = arg0;
+ register uint64 a1 asm("a1") = arg1;
+ register uint64 a2 asm("a2") = arg2;
+ register uint64 a3 asm("a3") = arg3;
+ register uint64 a4 asm("a4") = arg4;
+ register uint64 a5 asm("a5") = arg5;
+ register uint64 a6 asm("a6") = fid;
+ register uint64 a7 asm("a7") = ext;
+ __asm__ volatile("ecall"
+ : "+r"(a0), "+r"(a1)
+ : "r"(a2), "r"(a3), "r"(a4), "r"(a5), "r"(a6), "r"(a7)
+ : "memory");
+ ret.error = static_cast<long>(a0);
+ ret.value = static_cast<long>(a1);
+ return ret;
+}
+
+/* ---- Timer Extension (EID 0x54494D45) ---- */
+
+static inline void set_timer(uint64 stime_value) {
+ sbi_ecall(SBI_EXT_TIME, 0, stime_value);
+}
+
+/* ---- IPI Extension (EID 0x735049) ---- */
+
+/*
+ * sbi_send_ipi — Send S-mode software interrupt to target harts
+ *
+ * @hart_mask: bitmask of target harts (relative to hart_mask_base)
+ * @hart_mask_base: base hart ID for the mask (0 = harts 0-63)
+ */
+static inline sbiret sbi_send_ipi(uint64 hart_mask,
+ uint64 hart_mask_base = 0) {
+ return sbi_ecall(SBI_EXT_IPI, 0, hart_mask, hart_mask_base);
+}
+
+/* ---- Remote Fence Extension (EID 0x52464E43) ---- */
+
+/*
+ * sbi_remote_sfence_vma — Execute SFENCE.VMA on target harts
+ *
+ * @hart_mask/base: target harts
+ * @start_addr: start of VA range to invalidate
+ * @size: size of range (use ~0UL for full flush)
+ */
+static inline sbiret sbi_remote_sfence_vma(uint64 hart_mask,
+ uint64 hart_mask_base,
+ uint64 start_addr,
+ uint64 size) {
+ return sbi_ecall(SBI_EXT_RFENCE, 1,
+ hart_mask, hart_mask_base,
+ start_addr, size);
+}
+
+/* ---- HSM Extension (EID 0x48534D) ---- */
+
+/*
+ * sbi_hart_start — Start (wake) a hart that is in STOPPED state
+ *
+ * @hartid: target hart ID to start
+ * @start_addr: PHYSICAL address where the hart should begin execution
+ * @opaque: passed in a1 register to the hart (e.g. FDT pointer)
+ *
+ * OpenSBI v1.0+ parks non-boot harts; they must be explicitly started.
+ */
+static inline sbiret sbi_hart_start(uint64 hartid,
+ uint64 start_addr,
+ uint64 opaque) {
+ return sbi_ecall(SBI_EXT_HSM, 0, hartid, start_addr, opaque);
+}
+
+} // namespace arch::sbi
+
diff --git a/arch/riscv/include/arch/tlb.h b/arch/riscv/include/arch/tlb.h
new file mode 100644
index 0000000..46c6b97
--- /dev/null
+++ b/arch/riscv/include/arch/tlb.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+#include <lume/types.h>
+
+namespace arch::tlb {
+
+/*
+ * Perform a cross-CPU TLB flush (shootdown).
+ *
+ * @param cpu_mask: Bitmask of CPUs to target.
+ * @param start: Start virtual address, or 0 for full address space.
+ * @param size: Size in bytes, or ~0ULL for full address space.
+ */
+void flush_remote(uint64 cpu_mask, uint64 start, uint64 size);
+
+} // namespace arch::tlb
diff --git a/arch/riscv/include/arch/trap.h b/arch/riscv/include/arch/trap.h
new file mode 100644
index 0000000..8e55250
--- /dev/null
+++ b/arch/riscv/include/arch/trap.h
@@ -0,0 +1,94 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Trap Vector Control Primitives (RISC-V 64)
+ *
+ * Set/read the S-mode trap vector (stvec), trap cause (scause),
+ * and trap value (stval) registers.
+ *
+ * Reference: docs/specs/arch_abstraction.md §4.3
+ */
+
+#include <lume/types.h>
+
+/* scause interrupt bit (bit 63): 1 = interrupt, 0 = exception */
+#define SCAUSE_INTERRUPT (1UL << 63)
+
+/* Exception cause codes (scause with bit 63 = 0) */
+#define EXC_INST_MISALIGNED 0
+#define EXC_INST_ACCESS_FAULT 1
+#define EXC_ILLEGAL_INST 2
+#define EXC_BREAKPOINT 3
+#define EXC_LOAD_MISALIGNED 4
+#define EXC_LOAD_ACCESS_FAULT 5
+#define EXC_STORE_MISALIGNED 6
+#define EXC_STORE_ACCESS_FAULT 7
+#define EXC_ECALL_FROM_U 8
+#define EXC_ECALL_FROM_S 9
+#define EXC_INST_PAGE_FAULT 12
+#define EXC_LOAD_PAGE_FAULT 13
+#define EXC_STORE_PAGE_FAULT 15
+
+/* Interrupt cause codes (scause with bit 63 = 1, lower bits) */
+#define IRQ_S_SOFTWARE 1
+#define IRQ_S_TIMER 5
+#define IRQ_S_EXTERNAL 9
+
+namespace arch::trap {
+
+// Set S-mode trap vector base address (writes stvec).
+// Mode: Direct (all traps go to handler_addr).
+static inline void set_vector(uint64 handler_addr) {
+ // Lowest 2 bits of stvec = mode. 0 = Direct.
+ __asm__ volatile("csrw stvec, %0" :: "r"(handler_addr));
+}
+
+// Read current trap vector base address
+static inline uint64 get_vector() {
+ uint64 val;
+ __asm__ volatile("csrr %0, stvec" : "=r"(val));
+ return val;
+}
+
+// Read trap cause register (scause)
+// Bit 63: 1=interrupt, 0=exception. Lower bits: cause code.
+static inline uint64 get_cause() {
+ uint64 val;
+ __asm__ volatile("csrr %0, scause" : "=r"(val));
+ return val;
+}
+
+// Read trap value register (stval)
+// Contains faulting address (page faults) or faulting instruction.
+static inline uint64 get_value() {
+ uint64 val;
+ __asm__ volatile("csrr %0, stval" : "=r"(val));
+ return val;
+}
+
+// Check if a scause value represents an interrupt (vs exception)
+static inline bool is_interrupt(uint64 cause) {
+ return (cause & SCAUSE_INTERRUPT) != 0;
+}
+
+// Extract the cause code (strip the interrupt bit)
+static inline uint64 cause_code(uint64 cause) {
+ return cause & ~SCAUSE_INTERRUPT;
+}
+
+// Enable Supervisor User Memory access (set SUM bit in sstatus).
+// When SUM=1, S-mode can read/write pages marked PTE_U.
+// This is needed for syscall handlers to access user-space buffers.
+static inline void enable_sum() {
+ __asm__ volatile("csrs sstatus, %0" :: "r"(1UL << 18));
+}
+
+// Disable Supervisor User Memory access (clear SUM bit in sstatus).
+static inline void disable_sum() {
+ __asm__ volatile("csrc sstatus, %0" :: "r"(1UL << 18));
+}
+
+} // namespace arch::trap
diff --git a/arch/riscv/include/arch/trapframe.h b/arch/riscv/include/arch/trapframe.h
new file mode 100644
index 0000000..b198646
--- /dev/null
+++ b/arch/riscv/include/arch/trapframe.h
@@ -0,0 +1,140 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Trap Frame Structure (RISC-V 64)
+ *
+ * Saved/restored by the assembly trap_vector on every interrupt,
+ * exception, or system call. Contains the full CPU snapshot needed
+ * to resume execution after trap handling.
+ *
+ * Layout must match the assembly offsets in trap_vector.S exactly.
+ * Reference: docs/specs/arch_abstraction.md §3.2
+ */
+
+/*
+ * TrapFrame field offsets for use in assembly.
+ * These must be kept in sync with the struct layout below.
+ * Placed before any C++ code so assembly can #include this file.
+ */
+#define TF_KERNEL_SATP 0
+#define TF_KERNEL_SP 8
+#define TF_KERNEL_TRAP 16
+#define TF_KERNEL_CPUID 24
+#define TF_EPC 32
+#define TF_SSTATUS 40
+#define TF_RA 48
+#define TF_SP 56
+#define TF_GP 64
+#define TF_TP 72
+#define TF_T0 80
+#define TF_T1 88
+#define TF_T2 96
+#define TF_T3 104
+#define TF_T4 112
+#define TF_T5 120
+#define TF_T6 128
+#define TF_S0 136
+#define TF_S1 144
+#define TF_S2 152
+#define TF_S3 160
+#define TF_S4 168
+#define TF_S5 176
+#define TF_S6 184
+#define TF_S7 192
+#define TF_S8 200
+#define TF_S9 208
+#define TF_S10 216
+#define TF_S11 224
+#define TF_A0 232
+#define TF_A1 240
+#define TF_A2 248
+#define TF_A3 256
+#define TF_A4 264
+#define TF_A5 272
+#define TF_A6 280
+#define TF_A7 288
+#define TF_SIZE 296
+
+#ifndef __ASSEMBLER__
+
+#include <lume/types.h>
+#include <arch/cpu.h>
+
+namespace arch {
+
+struct TrapFrame {
+ /* ---- Kernel metadata (offset 0-39) ---- */
+ uint64 kernel_satp; // 0: Kernel page table (written to satp on trap entry)
+ uint64 kernel_sp; // 8: Top of kernel stack for this task
+ uint64 kernel_trap; // 16: Address of C++ trap_handler function
+ uint64 kernel_cpu_id; // 24: CPU ID (loaded into tp on trap entry)
+ uint64 epc; // 32: Interrupted instruction address (sepc)
+
+ /* ---- CSR backup (offset 40-47) ---- */
+ uint64 sstatus; // 40: Privilege status (SPP, SPIE bits)
+
+ /* ---- General-purpose registers (offset 48-295) ---- */
+ uint64 ra; // 48: x1 - Return address
+ uint64 sp; // 56: x2 - User stack pointer
+ uint64 gp; // 64: x3 - Global pointer
+ uint64 tp; // 72: x4 - Thread pointer
+ uint64 t[7]; // 80: x5-x7, x28-x31 (t0-t6, caller-saved temporaries)
+ uint64 s[12]; // 136: x8-x9, x18-x27 (s0-s11, callee-saved)
+ uint64 a[8]; // 232: x10-x17 (a0-a7, arguments / syscall params)
+};
+
+static_assert(sizeof(TrapFrame) == 296, "TrapFrame size mismatch with assembly offsets");
+
+} // namespace arch
+
+/* Forward declaration — defined in trap_md.cc.
+ * Must be after TrapFrame definition. */
+extern "C" [[noreturn]] void trap_dispatch(arch::TrapFrame*);
+
+namespace arch {
+
+/*
+ * trapframe_init_user — prepare a TrapFrame for returning to user mode.
+ *
+ * Sets arch-specific sstatus:
+ * SPP = 0 (return to U-mode on sret)
+ * SPIE = 1 (enable interrupts in U-mode)
+ * FS = Initial (allow FP instructions)
+ *
+ * Called from MI exec.cc instead of setting sstatus directly.
+ */
+static inline void trapframe_init_user(TrapFrame* tf, uint64 entry, uint64 sp)
+{
+ /* Clear all registers */
+ for (auto& t : tf->t) { t = 0; }
+ for (auto& s : tf->s) { s = 0; }
+ for (auto& a : tf->a) { a = 0; }
+ tf->ra = 0;
+ tf->gp = 0;
+ tf->tp = 0;
+
+ tf->epc = entry;
+ tf->sp = sp;
+
+ /* sstatus: SPP=0 (U-mode), SPIE=1 (interrupts), FS=Initial (FP ok) */
+ constexpr uint64 kSPIE = (1ULL << 5);
+ constexpr uint64 kFS_Initial = (1ULL << 13);
+ tf->sstatus = kSPIE | kFS_Initial;
+
+ /* Kernel metadata used by trap_vector.S on every trap from U-mode.
+ * These are RISC-V specific — trap_vector reads them from the
+ * TrapFrame to switch to kernel context. */
+ uint64 satp;
+ asm volatile("csrr %0, satp" : "=r"(satp));
+ tf->kernel_satp = satp;
+ tf->kernel_sp = reinterpret_cast<uint64>(tf);
+ tf->kernel_trap = reinterpret_cast<uint64>(&trap_dispatch);
+ tf->kernel_cpu_id = cpu::id();
+}
+
+} // namespace arch
+
+#endif /* __ASSEMBLER__ */
diff --git a/arch/riscv/include/asm/csr.h b/arch/riscv/include/asm/csr.h
new file mode 100644
index 0000000..42f1eb4
--- /dev/null
+++ b/arch/riscv/include/asm/csr.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _ASM_RISCV_CSR_H
+#define _ASM_RISCV_CSR_H
+
+/*
+ * RISC-V CSR (Control & Status Register) definitions used by the early
+ * boot / MMU-switch path.
+ *
+ * LumeOS-native subset of Linux arch/riscv/include/asm/csr.h. Only the
+ * symbols head.S and mm/init.cc actually reference are defined — adding
+ * the rest is straightforward when new code needs them, and skipping the
+ * Linux ALTERNATIVE / vendor-extension machinery keeps the cascade flat.
+ */
+
+/* SATP (Supervisor Address Translation and Protection) layout — RV64 */
+#define SATP_PPN_MASK 0x00000FFFFFFFFFFFUL /* bits [43:0] = root PPN */
+#define SATP_ASID_SHIFT 44
+#define SATP_MODE_SHIFT 60
+
+/* SATP MODE encodings (RV64) */
+#define SATP_MODE_BARE (0UL << SATP_MODE_SHIFT)
+#define SATP_MODE_SV39 (8UL << SATP_MODE_SHIFT)
+#define SATP_MODE_SV48 (9UL << SATP_MODE_SHIFT)
+
+/* Compose a SATP value from a page-table physical address.
+ * Result: MODE | (root_ppn) where root_ppn = pgd_pa >> PAGE_SHIFT. */
+#define MAKE_SATP(mode, pgd_pa) ((mode) | (((pgd_pa) >> 12) & SATP_PPN_MASK))
+
+/* SSTATUS bits we touch at boot — disable FP / vector at entry so that an
+ * accidental FP / V instruction in early code traps cleanly rather than
+ * silently using dirty state from before reset. */
+#define SR_FS_OFF (0UL << 13)
+#define SR_VS_OFF (0UL << 9)
+
+#endif /* _ASM_RISCV_CSR_H */
diff --git a/arch/riscv/include/asm/page.h b/arch/riscv/include/asm/page.h
new file mode 100644
index 0000000..7b9f155
--- /dev/null
+++ b/arch/riscv/include/asm/page.h
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _ASM_RISCV_PAGE_H
+#define _ASM_RISCV_PAGE_H
+
+/*
+ * RISC-V page / virtual-memory layout constants.
+ *
+ * Mirrors the subset of Linux arch/riscv/include/asm/page.h that LumeOS's
+ * early boot path needs:
+ * - PAGE_SIZE / PAGE_SHIFT / PAGE_MASK
+ * - PAGE_OFFSET (start of the higher-half direct map)
+ * - KERNEL_LINK_ADDR (link-time VA of _start)
+ * - SV39 PTE flag bits
+ * - PTRS_PER_PGD (512 for SV39)
+ *
+ * Values match Linux RISC-V SV39 defaults so that struct kernel_mapping
+ * (filled by setup_vm in arch/riscv/mm/init.cc) carries the same numeric
+ * relations Linux's mm/init.c maintains.
+ */
+
+#define PAGE_SHIFT 12
+#define PAGE_SIZE (1UL << PAGE_SHIFT)
+#define PAGE_MASK (~(PAGE_SIZE - 1))
+
+/* Higher-half base of the kernel direct map. Bottom of the SV39 supervisor
+ * address space minus the unmapped guard region. Identical to Linux's
+ * CONFIG_PAGE_OFFSET default on RV64. */
+#define PAGE_OFFSET 0xFFFFFFC000000000UL
+
+/* Where _start was linked. OpenSBI hands control to PHYS_BASE; the kernel
+ * runs at KERNEL_LINK_ADDR after relocate_enable_mmu jumps to higher-half. */
+#define PHYS_BASE 0x80200000UL
+#define KERNEL_LINK_ADDR 0xFFFFFFC080200000UL
+
+/* SATP mode field — SV39 = 8, shifted to bits [63:60]. */
+#define SATP_MODE_SV39 (8UL << 60)
+
+#define PTRS_PER_PGD 512
+
+/* 1 GB superpage size — entry.S used to build the early page table with
+ * these; setup_vm in arch/riscv/mm/init.cc now does the same. */
+#define PGDIR_SHIFT 30
+#define PGDIR_SIZE (1UL << PGDIR_SHIFT)
+#define PGDIR_MASK (~(PGDIR_SIZE - 1))
+
+/* SV39 PTE flag bits (one byte). */
+#define _PAGE_PRESENT (1UL << 0) /* aka VALID — historic Linux name */
+#define _PAGE_READ (1UL << 1)
+#define _PAGE_WRITE (1UL << 2)
+#define _PAGE_EXEC (1UL << 3)
+#define _PAGE_USER (1UL << 4)
+#define _PAGE_GLOBAL (1UL << 5)
+#define _PAGE_ACCESSED (1UL << 6)
+#define _PAGE_DIRTY (1UL << 7)
+
+#define PAGE_KERNEL_EXEC \
+ (_PAGE_PRESENT | _PAGE_READ | _PAGE_WRITE | _PAGE_EXEC | \
+ _PAGE_ACCESSED | _PAGE_DIRTY)
+
+#endif /* _ASM_RISCV_PAGE_H */
diff --git a/arch/riscv/include/asm/pgtable.h b/arch/riscv/include/asm/pgtable.h
new file mode 100644
index 0000000..28fc4c5
--- /dev/null
+++ b/arch/riscv/include/asm/pgtable.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _ASM_RISCV_PGTABLE_H
+#define _ASM_RISCV_PGTABLE_H
+
+/*
+ * Early page-table construction API used by arch/riscv/mm/init.cc::setup_vm.
+ *
+ * Mirrors the create_pgd_mapping interface from Linux's
+ * arch/riscv/mm/init.c — same name, same semantics:
+ *
+ * create_pgd_mapping(pgdir, va, pa, sz, prot)
+ *
+ * pgdir : root of the page table to populate
+ * va : virtual address to map
+ * pa : physical address to map to
+ * sz : mapping size — must be a supported leaf size (PAGE_SIZE,
+ * 2 MiB megapage, or 1 GiB gigapage)
+ * prot : PTE flag bits (PAGE_KERNEL_EXEC etc., from <asm/page.h>)
+ *
+ * The boot-time variant in this file only supports gigapage (1 GiB)
+ * mappings — that is all setup_vm needs for the early_pg_dir / trampoline_pg_dir
+ * built before the slab allocator exists. Finer-grained mappings come
+ * later via vmm_init().
+ */
+
+#include <lume/types.h>
+#include <asm/page.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct { unsigned long pgd; } pgd_t;
+
+/* Linker-provided early page-table storage (defined in lumeos.lds.S
+ * ARCH_BSS_EXTRA, lives in .bss so it's zero-initialized). Allocating
+ * them in the linker script — not in C — mirrors Linux and keeps the
+ * symbols visible from head.S without a forward C decl mismatch. */
+extern pgd_t early_pg_dir[PTRS_PER_PGD];
+extern pgd_t trampoline_pg_dir[PTRS_PER_PGD];
+extern pgd_t fixmap_pg_dir[PTRS_PER_PGD];
+
+/* Install a single PGD entry mapping `va` → `pa` with size `sz` and PTE
+ * flags `prot`. Only sz == PGDIR_SIZE (1 GiB) is supported in this early
+ * variant — vmm_init() takes over with create_pgd_mapping_pmd later. */
+void create_pgd_mapping(pgd_t *pgdir, unsigned long va, unsigned long pa,
+ unsigned long sz, unsigned long prot);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _ASM_RISCV_PGTABLE_H */
diff --git a/arch/riscv/include/asm/sections.h b/arch/riscv/include/asm/sections.h
new file mode 100644
index 0000000..454d9de
--- /dev/null
+++ b/arch/riscv/include/asm/sections.h
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#ifndef _ASM_RISCV_SECTIONS_H
+#define _ASM_RISCV_SECTIONS_H
+
+/*
+ * Linker-emitted symbols that bracket each section of the kernel image.
+ *
+ * The lumeos.lds.S linker script defines these as the start / end
+ * addresses of contiguous regions. Code that walks a section
+ * (relocate_kernel walks .rela.dyn, the C++ ctor loop walks
+ * .init_array, the VMM mapping code walks .text vs .rodata, ...)
+ * imports the pair via this header.
+ *
+ * Mirrors Linux arch/riscv/include/asm/sections.h / asm-generic/sections.h.
+ * Each name and pair is identical to Linux so that direct ports of mm/init.c
+ * relocate_kernel logic compile unchanged against LumeOS.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern char _start[];
+extern char _end[];
+
+extern char _stext[];
+extern char _etext[];
+extern char _srodata[];
+extern char _erodata[];
+extern char _sdata[];
+extern char _edata[];
+extern char _sbss[];
+extern char _ebss[];
+
+extern char __init_array_start[];
+extern char __init_array_end[];
+
+/* .rela.dyn — array of Elf64_Rela entries emitted by the linker under
+ * `-shared -Bsymbolic`. relocate_kernel walks this range and patches
+ * each R_RISCV_RELATIVE slot. */
+extern char __rela_dyn_start[];
+extern char __rela_dyn_end[];
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _ASM_RISCV_SECTIONS_H */