summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-04-17 01:05:31 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-04-17 01:05:31 +0800
commite2564d112ba03b432f9f44f445cbc4a3ed85751a (patch)
tree1541ee010bfd9c0e614fc91c2de5c9aa741418ae
parent81563cb415d8214f1d1e34f73d49a404424f8f2c (diff)
downloadlume-e2564d112ba03b432f9f44f445cbc4a3ed85751a.tar.gz
lume-e2564d112ba03b432f9f44f445cbc4a3ed85751a.tar.xz
feat: add initial RISC-V architecture support and core kernel functionality
- Implement MMU hardware primitives for RISC-V SV39 in mmu.h - Create trap vector control primitives in trap.h - Define trap frame structure for RISC-V in trapframe.h - Add assembly routines for context switching in swtch.S - Implement trap vector entry/exit in trap_vector.S - Introduce basic error number definitions in errno.h - Provide placement new/delete operators in new.h - Define kernel panic handler in panic.h and implement in panic.cc - Establish basic type definitions in types.h - Create the kernel main entry point in main.cc with global initialization - Add synchronization primitives using spinlocks in spinlock.h and spinlock.cc - Define per-CPU state tracking for interrupts in cpu_state.h - Create a linker script for the kernel in linker.ld - Update module makefiles for kernel and library components
-rw-r--r--include/lib/errno.h8
-rw-r--r--include/ulib/errno.h8
-rw-r--r--na/Makefile158
-rw-r--r--na/arch/riscv/boot/entry.S271
-rw-r--r--na/arch/riscv/include/arch/context.h33
-rw-r--r--na/arch/riscv/include/arch/cpu.h64
-rw-r--r--na/arch/riscv/include/arch/mmu.h63
-rw-r--r--na/arch/riscv/include/arch/trap.h82
-rw-r--r--na/arch/riscv/include/arch/trapframe.h92
-rw-r--r--na/arch/riscv/module.mk7
-rw-r--r--na/arch/riscv/trap/swtch.S40
-rw-r--r--na/arch/riscv/trap/trap_vector.S171
-rw-r--r--na/include/lume/errno.h43
-rw-r--r--na/include/lume/new.h18
-rw-r--r--na/include/lume/panic.h16
-rw-r--r--na/include/lume/types.h39
-rw-r--r--na/kernel/main.cc146
-rw-r--r--na/kernel/module.mk3
-rw-r--r--na/kernel/sync/cpu_state.h34
-rw-r--r--na/kernel/sync/spinlock.cc111
-rw-r--r--na/kernel/sync/spinlock.h60
-rw-r--r--na/lib/module.mk2
-rw-r--r--na/lib/panic.cc46
-rw-r--r--na/linker.ld111
24 files changed, 1618 insertions, 8 deletions
diff --git a/include/lib/errno.h b/include/lib/errno.h
index 62fd3e5..f0c1436 100644
--- a/include/lib/errno.h
+++ b/include/lib/errno.h
@@ -1,11 +1,11 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
/*
- * Copyright (C) 2026 Richard Qin
* Lume OS - Linux Compatibility Layer
*
* This file aligns strictly with Linux RISC-V 64 ABI (asm-generic).
- * Reference: linux/include/uapi/asm-generic/errno-base.h
- * Reference: linux/include/uapi/asm-generic/errno.h
+ * The definitions are copied from the Linux kernel's UAPI headers
+ * (include/uapi/asm-generic/errno-base.h and include/uapi/asm-generic/errno.h)
+ * to ensure ABI compatibility. Original copyrights apply.
*/
#ifndef _ERRNO_H
diff --git a/include/ulib/errno.h b/include/ulib/errno.h
index c1691db..dd51598 100644
--- a/include/ulib/errno.h
+++ b/include/ulib/errno.h
@@ -1,11 +1,11 @@
-// SPDX-License-Identifier: GPL-2.0-or-later
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
/*
- * Copyright (C) 2026 Richard Qin
* Lume OS - Linux Compatibility Layer
*
* This file aligns strictly with Linux RISC-V 64 ABI (asm-generic).
- * Reference: linux/include/uapi/asm-generic/errno-base.h
- * Reference: linux/include/uapi/asm-generic/errno.h
+ * The definitions are copied from the Linux kernel's UAPI headers
+ * (include/uapi/asm-generic/errno-base.h and include/uapi/asm-generic/errno.h)
+ * to ensure ABI compatibility. Original copyrights apply.
*/
#ifndef _ERRNO_H
diff --git a/na/Makefile b/na/Makefile
new file mode 100644
index 0000000..3fc72b1
--- /dev/null
+++ b/na/Makefile
@@ -0,0 +1,158 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (C) 2026 Richard Qin
+#
+# LumeOS Kernel Makefile (Non-Recursive)
+#
+# Usage:
+# make - Build kernel ELF
+# make run - Run in QEMU
+# make debug - Run in QEMU with GDB server
+# make clean - Clean build artifacts
+# make fmt - Format all source files
+# make disasm - Generate disassembly listing
+#
+# Reference: docs/specs/00_conventions.md §7
+
+# ============================================================
+# Toolchain Detection
+# ============================================================
+ifndef TOOLCHAIN_PREFIX
+ TOOLCHAIN_PREFIX := $(shell \
+ if command -v riscv64-linux-gnu-gcc >/dev/null 2>&1; then \
+ echo riscv64-linux-gnu-; \
+ elif command -v riscv64-unknown-linux-gnu-gcc >/dev/null 2>&1; then \
+ echo riscv64-unknown-linux-gnu-; \
+ elif command -v riscv64-unknown-elf-gcc >/dev/null 2>&1; then \
+ echo riscv64-unknown-elf-; \
+ else \
+ echo riscv64-linux-gnu-; \
+ fi)
+endif
+
+CC := $(TOOLCHAIN_PREFIX)gcc
+CXX := $(TOOLCHAIN_PREFIX)g++
+LD := $(TOOLCHAIN_PREFIX)ld
+OBJCOPY := $(TOOLCHAIN_PREFIX)objcopy
+OBJDUMP := $(TOOLCHAIN_PREFIX)objdump
+
+# ============================================================
+# Build Configuration
+# ============================================================
+ARCH := riscv
+BUILD := build
+TARGET := $(BUILD)/kernel.elf
+IMAGE := $(BUILD)/kernel.bin
+LINKER_SCRIPT := linker.ld
+
+# Common flags (base, before module includes)
+WARNINGS := -Wall -Wextra -Wno-unused-parameter
+INCLUDES := -Iinclude
+
+# ============================================================
+# Source Collection (Non-Recursive Module Fragments)
+# Module .mk files may append to SRCS_CC, SRCS_S, INCLUDES
+# ============================================================
+SRCS_CC :=
+SRCS_S :=
+
+include arch/$(ARCH)/module.mk
+include kernel/module.mk
+# include mm/module.mk # Phase 1
+# include fs/module.mk # Phase 3
+# include drivers/module.mk # As needed
+include lib/module.mk
+
+# Compiler flags (defined AFTER includes so INCLUDES is complete)
+CFLAGS := $(WARNINGS) -O2 -g \
+ -ffreestanding -nostdlib \
+ -mcmodel=medany \
+ -MMD -MP \
+ $(INCLUDES)
+
+CXXFLAGS := $(CFLAGS) \
+ -std=c++17 \
+ -fno-exceptions -fno-rtti -fno-use-cxa-atexit \
+ -fno-threadsafe-statics
+
+ASFLAGS := $(CFLAGS)
+
+LDFLAGS := -nostdlib -static
+
+# ============================================================
+# Object File Computation
+# ============================================================
+OBJS := $(patsubst %.cc,$(BUILD)/%.o,$(SRCS_CC)) \
+ $(patsubst %.S,$(BUILD)/%.o,$(SRCS_S))
+
+DEPS := $(OBJS:.o=.d)
+
+# ============================================================
+# Build Rules
+# ============================================================
+.PHONY: all clean run debug fmt disasm
+
+all: $(IMAGE)
+
+# Link kernel ELF
+$(TARGET): $(OBJS) $(LINKER_SCRIPT)
+ @echo " LD $@"
+ @mkdir -p $(dir $@)
+ @$(LD) $(LDFLAGS) -T $(LINKER_SCRIPT) -o $@ $(OBJS)
+ @$(TOOLCHAIN_PREFIX)size $@
+
+# Generate flat binary for QEMU loading
+$(IMAGE): $(TARGET)
+ @echo " OBJCOPY $@"
+ @$(OBJCOPY) -O binary $< $@
+
+# Compile C++ sources
+$(BUILD)/%.o: %.cc
+ @echo " CXX $<"
+ @mkdir -p $(dir $@)
+ @$(CXX) $(CXXFLAGS) -c $< -o $@
+
+# Assemble RISC-V assembly
+$(BUILD)/%.o: %.S
+ @echo " AS $<"
+ @mkdir -p $(dir $@)
+ @$(CC) $(ASFLAGS) -c $< -o $@
+
+# Auto-dependency tracking
+-include $(DEPS)
+
+# ============================================================
+# Run & Debug
+# ============================================================
+QEMU := qemu-system-riscv64
+QEMU_MACHINE := -machine virt -m 128M -nographic
+QEMU_BIOS := -bios default
+
+run: $(IMAGE)
+ @echo " QEMU Running..."
+ $(QEMU) $(QEMU_MACHINE) $(QEMU_BIOS) -kernel $(IMAGE)
+
+debug: $(IMAGE)
+ @echo " QEMU Waiting for GDB on :1234..."
+ $(QEMU) $(QEMU_MACHINE) $(QEMU_BIOS) -kernel $(IMAGE) -s -S
+
+# ============================================================
+# Utilities
+# ============================================================
+clean:
+ rm -rf $(BUILD)
+
+disasm: $(TARGET)
+ @echo " OBJDUMP $<"
+ @$(OBJDUMP) -d -S $(TARGET) > $(TARGET).asm
+ @echo " Written to $(TARGET).asm"
+
+fmt:
+ @echo " FMT Formatting source files..."
+ @find . -name '*.cc' -o -name '*.h' | xargs clang-format -i 2>/dev/null || true
+
+# Print build info (for debugging Makefile issues)
+info:
+ @echo "TOOLCHAIN_PREFIX = $(TOOLCHAIN_PREFIX)"
+ @echo "SRCS_CC = $(SRCS_CC)"
+ @echo "SRCS_S = $(SRCS_S)"
+ @echo "OBJS = $(OBJS)"
diff --git a/na/arch/riscv/boot/entry.S b/na/arch/riscv/boot/entry.S
new file mode 100644
index 0000000..895c778
--- /dev/null
+++ b/na/arch/riscv/boot/entry.S
@@ -0,0 +1,271 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * LumeOS Kernel Entry Point (RISC-V 64)
+ *
+ * This file implements the earliest boot sequence:
+ * 1. RISC-V Linux Boot Protocol Image Header (64 bytes)
+ * 2. BSP/AP synchronization
+ * 3. BSS zeroing (BSP only)
+ * 4. Early page table setup (1GB superpages for identity + higher-half)
+ * 5. MMU enable and higher-half trampoline
+ * 6. C++ global constructor invocation
+ * 7. Jump to kernel_main()
+ *
+ * Register contract on entry (from OpenSBI / GRUB / U-Boot):
+ * a0 = CPU ID (Hart ID)
+ * a1 = FDT physical base address
+ *
+ * Reference: docs/specs/linker_and_entry.md
+ */
+
+#define PAGE_OFFSET 0xFFFFFFC000000000
+#define BOOT_STACK_SIZE 8192
+#define NCPU_MAX 8
+
+/* SV39 PTE flags */
+#define PTE_V (1 << 0) /* Valid */
+#define PTE_R (1 << 1) /* Readable */
+#define PTE_W (1 << 2) /* Writable */
+#define PTE_X (1 << 3) /* Executable */
+#define PTE_A (1 << 6) /* Accessed */
+#define PTE_D (1 << 7) /* Dirty */
+
+/* SATP mode field for SV39 */
+#define SATP_SV39 (8UL << 60)
+
+/* ========================================================================
+ * Section: .head.text — RISC-V Linux Boot Protocol Image Header
+ * Must be at byte 0 of the kernel binary.
+ * ======================================================================== */
+ .section .head.text, "ax"
+ .globl _start
+_start:
+ /* code0 (u32, offset 0): executable jump for OpenSBI bare boot */
+ j real_start
+ /* code1 (u32, offset 4): reserved */
+ .word 0
+ /* text_offset (u64, offset 8): load offset from RAM base */
+ .dword 0x200000
+ /* image_size (u64, offset 16): will be filled post-link, 0 = unknown */
+ .dword 0
+ /* flags (u64, offset 24): bit 0: 0=LE, 1=BE */
+ .dword 0
+ /* version (u32, offset 32): header version 0.2 */
+ .word 0x0002
+ /* res1 (u32, offset 36) */
+ .word 0
+ /* res2 (u64, offset 40) */
+ .dword 0
+ /* magic (u64, offset 48): RISCV_IMAGE_MAGIC "RISCV\0\0\0" LE */
+ .dword 0x5643534952
+ /* magic2 (u32, offset 56): RISCV_IMAGE_MAGIC2 "RSC\x05" LE */
+ .word 0x05435352
+ /* res3 (u32, offset 60): reserved for PE/COFF */
+ .word 0
+
+/* ========================================================================
+ * real_start: Actual boot logic begins here (all CPUs land here)
+ * ======================================================================== */
+ .text
+ .globl real_start
+real_start:
+ /* ---- Step 1: Disable interrupts ---- */
+ csrw sie, zero
+ csrw sip, zero
+
+ /* ---- Step 2: Set up gp (global pointer) ---- */
+ /* Critical for medany model: all global variable access goes through gp */
+ .option push
+ .option norelax
+ la gp, __global_pointer$
+ .option pop
+
+ /* ---- Step 3: Set up per-CPU boot stack ---- */
+ /* sp = boot_stack + (cpu_id + 1) * BOOT_STACK_SIZE */
+ la t0, boot_stack
+ li t1, BOOT_STACK_SIZE
+ addi t2, a0, 1 /* t2 = cpu_id + 1 */
+ mul t1, t1, t2 /* t1 = (cpu_id + 1) * BOOT_STACK_SIZE */
+ add sp, t0, t1
+
+ /* ---- Step 4: Store CPU ID in tp for arch::cpu::id() ---- */
+ mv tp, a0
+
+ /* ---- Step 5: BSP / AP divergence ---- */
+ /* CPU 0 is the BSP; all others spin-wait */
+ bnez a0, .Lap_wait
+
+/* ===================================================================
+ * BSP-only path: Clear BSS, set up early page table, release APs
+ * =================================================================== */
+.Lbsp_init:
+ /* ---- 5.1: Clear BSS ---- */
+ la t0, _sbss
+ la t1, _ebss
+.Lbss_loop:
+ bgeu t0, t1, .Lbss_done
+ sd zero, 0(t0)
+ addi t0, t0, 8
+ j .Lbss_loop
+.Lbss_done:
+
+ /* ---- 5.2: Build early page table (1GB superpages) ---- */
+ /*
+ * SV39 root page table (512 entries x 8 bytes = 4KB)
+ * We need 4 entries:
+ * [0] = Identity map: 0x00000000 -> 0x00000000 (MMIO: UART, PLIC, CLINT)
+ * [2] = Identity map: 0x80000000 -> 0x80000000 (RAM)
+ * [256] = Higher-half: 0xFFFFFFC000000000 -> 0x00000000 (MMIO via VA)
+ * [258] = Higher-half: 0xFFFFFFC080000000 -> 0x80000000 (RAM via VA)
+ *
+ * VPN[2] index = (VA >> 30) & 0x1FF
+ * 0x00000000 -> 0 0xFFFFFFC000000000 -> 256
+ * 0x80000000 -> 2 0xFFFFFFC080000000 -> 258
+ */
+ la t0, early_pgdir
+
+ /* PTE flags: V|R|W|X|A|D (1GB leaf superpage) */
+ li t2, (PTE_V | PTE_R | PTE_W | PTE_X | PTE_A | PTE_D)
+
+ /* --- MMIO region: physical 0x00000000 --- */
+ /* PPN = 0x00000000 >> 12 = 0, PTE = flags only */
+ mv t1, t2 /* PTE = 0 | flags */
+
+ /* Entry [0]: Identity map MMIO */
+ sd t1, (0 * 8)(t0)
+
+ /* Entry [256]: Higher-half MMIO (0xFFFFFFC000000000) */
+ li t3, (256 * 8)
+ add t3, t0, t3
+ sd t1, 0(t3)
+
+ /* --- RAM region: physical 0x80000000 --- */
+ li t1, 0x80000 /* PPN = 0x80000000 >> 12 */
+ slli t1, t1, 10 /* PPN field in PTE */
+ or t1, t1, t2 /* t1 = complete PTE */
+
+ /* Entry [2]: Identity map RAM */
+ sd t1, (2 * 8)(t0)
+
+ /* Entry [258]: Higher-half RAM (0xFFFFFFC080000000) */
+ li t3, (258 * 8)
+ add t3, t0, t3
+ sd t1, 0(t3)
+
+ /* ---- 5.3: Memory fence ---- */
+ fence rw, rw
+
+ /* ---- 5.4: Release APs ---- */
+ la t0, boot_sync_flag
+ li t1, 1
+ amoswap.w.rl zero, t1, (t0)
+ j .Lenable_mmu
+
+/* ===================================================================
+ * AP path: spin-wait until BSP signals readiness
+ * =================================================================== */
+.Lap_wait:
+ la t0, boot_sync_flag
+.Lap_spin:
+ lw t1, 0(t0)
+ beqz t1, .Lap_spin
+ fence r, rw /* Acquire: ensure we see BSP's writes */
+
+/* ===================================================================
+ * All CPUs: Enable MMU and jump to higher-half virtual addresses
+ * =================================================================== */
+.Lenable_mmu:
+ /* ---- 6.1: Write satp = SV39 mode | (early_pgdir >> 12) ---- */
+ /* NOTE: `la` at this point is PC-relative. Since we are still executing
+ * at physical addresses, it yields the PHYSICAL address directly.
+ * No VA-to-PA conversion needed. */
+ la t0, early_pgdir
+ srli t0, t0, 12 /* PPN = PA >> 12 */
+ li t1, SATP_SV39
+ or t0, t0, t1 /* satp = mode | PPN */
+ csrw satp, t0
+
+ /* ---- 6.2: Flush TLB ---- */
+ sfence.vma zero, zero
+
+ /* ---- 6.3: Jump to higher-half virtual address ---- */
+ /* Now we are executing via the identity map (PA == VA for 0x8...).
+ * Load the VA of the next instruction and jump there,
+ * so PC switches from physical to virtual address space. */
+ la t0, .Lhigher_half
+ jr t0
+
+.Lhigher_half:
+ /* We are now running at virtual addresses (0xFFFFFFC0...) !
+ * The identity map is still active but will be removed by vmm_init(). */
+
+ /* ---- 6.4: Reload sp with virtual address ---- */
+ /* Boot stack was set up using physical-linked VA already (linker placed
+ * boot_stack in higher-half), so sp is valid. Reload gp just in case. */
+ .option push
+ .option norelax
+ la gp, __global_pointer$
+ .option pop
+
+ /* ---- Step 7: Save fdt_paddr, BSP calls early_cpp_init ---- */
+ /* a1 (fdt_paddr) is caller-saved and may be clobbered by function calls.
+ * Save it in s0 (callee-saved) so it survives early_cpp_init. */
+ mv s0, a1 /* s0 = fdt_paddr (preserved across calls) */
+
+ bnez tp, .Lskip_cpp_init
+
+ /* BSP only: invoke C++ global constructors */
+ call early_cpp_init
+
+.Lskip_cpp_init:
+ /* All CPUs: enter kernel_main(cpu_id, fdt_paddr) */
+ mv a0, tp /* a0 = cpu_id (from tp) */
+ mv a1, s0 /* a1 = fdt_paddr (from s0) */
+ call kernel_main
+
+ /* kernel_main should never return; if it does, halt */
+.Lhalt:
+ csrc sstatus, 2 /* Clear SIE */
+ wfi
+ j .Lhalt
+
+
+/* ========================================================================
+ * early_cpp_init: Traverse .init_array and call global constructors
+ * Called by BSP only, before kernel_main.
+ * ======================================================================== */
+ .globl early_cpp_init
+early_cpp_init:
+ /* Prologue: save ra and callee-saved regs we use as loop state */
+ addi sp, sp, -32
+ sd ra, 0(sp)
+ sd s1, 8(sp)
+ sd s2, 16(sp)
+
+ la s1, __init_array_start
+ la s2, __init_array_end
+.Linit_loop:
+ bgeu s1, s2, .Linit_done
+ ld t0, 0(s1) /* Load function pointer */
+ jalr ra, t0 /* Call constructor (sets ra = return addr) */
+ addi s1, s1, 8 /* Next entry */
+ j .Linit_loop
+.Linit_done:
+ /* Epilogue: restore and return */
+ ld ra, 0(sp)
+ ld s1, 8(sp)
+ ld s2, 16(sp)
+ addi sp, sp, 32
+ ret
+
+
+/* ========================================================================
+ * BSS Data: boot_sync_flag
+ * ======================================================================== */
+ .section .bss
+ .align 2
+ .globl boot_sync_flag
+boot_sync_flag:
+ .word 0
diff --git a/na/arch/riscv/include/arch/context.h b/na/arch/riscv/include/arch/context.h
new file mode 100644
index 0000000..3d935fa
--- /dev/null
+++ b/na/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/na/arch/riscv/include/arch/cpu.h b/na/arch/riscv/include/arch/cpu.h
new file mode 100644
index 0000000..644ae37
--- /dev/null
+++ b/na/arch/riscv/include/arch/cpu.h
@@ -0,0 +1,64 @@
+/* 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) */
+
+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;
+}
+
+} // namespace arch::cpu
diff --git a/na/arch/riscv/include/arch/mmu.h b/na/arch/riscv/include/arch/mmu.h
new file mode 100644
index 0000000..a0ee1ed
--- /dev/null
+++ b/na/arch/riscv/include/arch/mmu.h
@@ -0,0 +1,63 @@
+/* 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;
+}
+
+// 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");
+}
+
+} // namespace arch::mmu
diff --git a/na/arch/riscv/include/arch/trap.h b/na/arch/riscv/include/arch/trap.h
new file mode 100644
index 0000000..c99d8cb
--- /dev/null
+++ b/na/arch/riscv/include/arch/trap.h
@@ -0,0 +1,82 @@
+/* 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;
+}
+
+} // namespace arch::trap
diff --git a/na/arch/riscv/include/arch/trapframe.h b/na/arch/riscv/include/arch/trapframe.h
new file mode 100644
index 0000000..de49e9c
--- /dev/null
+++ b/na/arch/riscv/include/arch/trapframe.h
@@ -0,0 +1,92 @@
+/* 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>
+
+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
+
+#endif /* __ASSEMBLER__ */
diff --git a/na/arch/riscv/module.mk b/na/arch/riscv/module.mk
new file mode 100644
index 0000000..85ed5dd
--- /dev/null
+++ b/na/arch/riscv/module.mk
@@ -0,0 +1,7 @@
+# arch/riscv/module.mk
+SRCS_S += arch/riscv/boot/entry.S
+SRCS_S += arch/riscv/trap/swtch.S
+SRCS_S += arch/riscv/trap/trap_vector.S
+
+# Arch-specific headers (added to include path)
+INCLUDES += -Iarch/riscv/include
diff --git a/na/arch/riscv/trap/swtch.S b/na/arch/riscv/trap/swtch.S
new file mode 100644
index 0000000..4393505
--- /dev/null
+++ b/na/arch/riscv/trap/swtch.S
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+ .section .text
+ .globl context_switch
+ .align 2
+# void context_switch(Context* old_ctx, Context* next_ctx);
+# a0 = old_ctx_ptr, a1 = next_ctx_ptr
+context_switch:
+ sd ra, 0(a0)
+ sd sp, 8(a0)
+ sd s0, 16(a0)
+ sd s1, 24(a0)
+ sd s2, 32(a0)
+ sd s3, 40(a0)
+ sd s4, 48(a0)
+ sd s5, 56(a0)
+ sd s6, 64(a0)
+ sd s7, 72(a0)
+ sd s8, 80(a0)
+ sd s9, 88(a0)
+ sd s10, 96(a0)
+ sd s11, 104(a0)
+
+ ld ra, 0(a1)
+ ld sp, 8(a1)
+ ld s0, 16(a1)
+ ld s1, 24(a1)
+ ld s2, 32(a1)
+ ld s3, 40(a1)
+ ld s4, 48(a1)
+ ld s5, 56(a1)
+ ld s6, 64(a1)
+ ld s7, 72(a1)
+ ld s8, 80(a1)
+ ld s9, 88(a1)
+ ld s10, 96(a1)
+ ld s11, 104(a1)
+
+ ret \ No newline at end of file
diff --git a/na/arch/riscv/trap/trap_vector.S b/na/arch/riscv/trap/trap_vector.S
new file mode 100644
index 0000000..f0b65b5
--- /dev/null
+++ b/na/arch/riscv/trap/trap_vector.S
@@ -0,0 +1,171 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Trap Vector Entry / Exit (RISC-V 64, S-mode)
+ *
+ * On trap entry (interrupt / exception / ecall):
+ * sscratch holds the TrapFrame pointer (set up by kernel before sret).
+ * We swap sscratch <-> tp, save all 31 GPRs + sstatus + sepc into TrapFrame,
+ * then load kernel metadata (sp, satp, tp, trap handler) and jump to C++.
+ *
+ * On trap return (userret):
+ * Restore all GPRs + sstatus + sepc from TrapFrame, swap sscratch <-> tp,
+ * then sret back to user mode.
+ *
+ * Layout must match arch::TrapFrame in arch/riscv/include/arch/trapframe.h
+ * Reference: docs/specs/arch_abstraction.md §7.2
+ */
+
+#include <arch/trapframe.h>
+
+ .section .text
+ .globl trap_vector
+ .globl userret
+ .align 4
+
+/*
+ * trap_vector — unified S-mode trap entry point (Direct mode)
+ *
+ * Precondition: sscratch = pointer to current task's TrapFrame
+ */
+trap_vector:
+ /* Step 1: swap sscratch <-> tp. Now tp = TrapFrame*, sscratch = old tp */
+ csrrw tp, sscratch, tp
+
+ /* Step 2: save all 31 general-purpose registers into TrapFrame */
+ sd ra, TF_RA(tp)
+ sd sp, TF_SP(tp)
+ sd gp, TF_GP(tp)
+ /* tp (x4) is in sscratch — save it later */
+ sd t0, TF_T0(tp)
+ sd t1, TF_T1(tp)
+ sd t2, TF_T2(tp)
+ sd s0, TF_S0(tp)
+ sd s1, TF_S1(tp)
+ sd a0, TF_A0(tp)
+ sd a1, TF_A1(tp)
+ sd a2, TF_A2(tp)
+ sd a3, TF_A3(tp)
+ sd a4, TF_A4(tp)
+ sd a5, TF_A5(tp)
+ sd a6, TF_A6(tp)
+ sd a7, TF_A7(tp)
+ sd s2, TF_S2(tp)
+ sd s3, TF_S3(tp)
+ sd s4, TF_S4(tp)
+ sd s5, TF_S5(tp)
+ sd s6, TF_S6(tp)
+ sd s7, TF_S7(tp)
+ sd s8, TF_S8(tp)
+ sd s9, TF_S9(tp)
+ sd s10, TF_S10(tp)
+ sd s11, TF_S11(tp)
+ sd t3, TF_T3(tp)
+ sd t4, TF_T4(tp)
+ sd t5, TF_T5(tp)
+ sd t6, TF_T6(tp)
+
+ /* Save the real tp (user's tp) which is now in sscratch */
+ csrr t0, sscratch
+ sd t0, TF_TP(tp)
+
+ /* Step 3: save sstatus */
+ csrr t0, sstatus
+ sd t0, TF_SSTATUS(tp)
+
+ /* Step 4: save sepc (interrupted PC) */
+ csrr t0, sepc
+ sd t0, TF_EPC(tp)
+
+ /* Step 5: load kernel context from TrapFrame metadata */
+ ld sp, TF_KERNEL_SP(tp) /* switch to kernel stack */
+ ld t1, TF_KERNEL_SATP(tp) /* kernel page table */
+ ld t2, TF_KERNEL_CPUID(tp) /* CPU ID for tp */
+ ld t3, TF_KERNEL_TRAP(tp) /* C++ trap_handler address */
+
+ /* Switch to kernel page table */
+ csrw satp, t1
+
+ /* Step 6: flush TLB after page table switch */
+ sfence.vma zero, zero
+
+ /* Set tp = CPU ID (kernel convention) */
+ mv tp, t2
+
+ /* Step 7: jump to C++ trap_handler */
+ jalr t3
+
+
+/*
+ * userret — return to user mode
+ *
+ * Called from C++ as: userret(TrapFrame* tf)
+ * a0 = pointer to TrapFrame (with user satp already written by caller)
+ *
+ * Precondition: interrupts must be disabled by caller.
+ */
+userret:
+ /* a0 = TrapFrame pointer, use it as base */
+
+ /* Step 1: write back epc -> sepc */
+ ld t0, TF_EPC(a0)
+ csrw sepc, t0
+
+ /* Step 2: write back sstatus */
+ ld t0, TF_SSTATUS(a0)
+ csrw sstatus, t0
+
+ /* Step 3: caller is responsible for switching satp to user page table
+ * and flushing TLB before calling userret. */
+
+ /* Store TrapFrame address into sscratch for next trap entry */
+ csrw sscratch, a0
+
+ /* Use a0 as base to restore all registers */
+ mv tp, a0
+
+ /* Step 4: restore all 31 general-purpose registers */
+ ld ra, TF_RA(tp)
+ ld sp, TF_SP(tp)
+ ld gp, TF_GP(tp)
+ /* defer tp restore — we're using it as base */
+ ld t0, TF_T0(tp)
+ ld t1, TF_T1(tp)
+ ld t2, TF_T2(tp)
+ ld s0, TF_S0(tp)
+ ld s1, TF_S1(tp)
+ ld a0, TF_A0(tp)
+ ld a1, TF_A1(tp)
+ ld a2, TF_A2(tp)
+ ld a3, TF_A3(tp)
+ ld a4, TF_A4(tp)
+ ld a5, TF_A5(tp)
+ ld a6, TF_A6(tp)
+ ld a7, TF_A7(tp)
+ ld s2, TF_S2(tp)
+ ld s3, TF_S3(tp)
+ ld s4, TF_S4(tp)
+ ld s5, TF_S5(tp)
+ ld s6, TF_S6(tp)
+ ld s7, TF_S7(tp)
+ ld s8, TF_S8(tp)
+ ld s9, TF_S9(tp)
+ ld s10, TF_S10(tp)
+ ld s11, TF_S11(tp)
+ ld t3, TF_T3(tp)
+ ld t4, TF_T4(tp)
+ ld t5, TF_T5(tp)
+ ld t6, TF_T6(tp)
+
+ /* Step 5: restore user tp (swap sscratch <-> tp) */
+ /* sscratch currently = TrapFrame address, tp = TrapFrame address */
+ /* We need: tp = user's tp, sscratch = TrapFrame address */
+ /* Load user tp from TrapFrame first, but we can't use any temp reg now.
+ * Use the sscratch swap trick: */
+ ld tp, TF_TP(tp)
+
+ /* sscratch already holds TrapFrame address (set above with csrw) */
+
+ /* Step 6: sret — return to user mode */
+ sret
diff --git a/na/include/lume/errno.h b/na/include/lume/errno.h
new file mode 100644
index 0000000..e0786ce
--- /dev/null
+++ b/na/include/lume/errno.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (C) 2026 Richard Qin */
+/* from linux kernel linux/include/uapi/asm-generic/errno-base.h*/
+
+#ifndef _ASM_GENERIC_ERRNO_BASE_H
+#define _ASM_GENERIC_ERRNO_BASE_H
+
+#define EPERM 1 /* Operation not permitted */
+#define ENOENT 2 /* No such file or directory */
+#define ESRCH 3 /* No such process */
+#define EINTR 4 /* Interrupted system call */
+#define EIO 5 /* I/O error */
+#define ENXIO 6 /* No such device or address */
+#define E2BIG 7 /* Argument list too long */
+#define ENOEXEC 8 /* Exec format error */
+#define EBADF 9 /* Bad file number */
+#define ECHILD 10 /* No child processes */
+#define EAGAIN 11 /* Try again */
+#define ENOMEM 12 /* Out of memory */
+#define EACCES 13 /* Permission denied */
+#define EFAULT 14 /* Bad address */
+#define ENOTBLK 15 /* Block device required */
+#define EBUSY 16 /* Device or resource busy */
+#define EEXIST 17 /* File exists */
+#define EXDEV 18 /* Cross-device link */
+#define ENODEV 19 /* No such device */
+#define ENOTDIR 20 /* Not a directory */
+#define EISDIR 21 /* Is a directory */
+#define EINVAL 22 /* Invalid argument */
+#define ENFILE 23 /* File table overflow */
+#define EMFILE 24 /* Too many open files */
+#define ENOTTY 25 /* Not a typewriter */
+#define ETXTBSY 26 /* Text file busy */
+#define EFBIG 27 /* File too large */
+#define ENOSPC 28 /* No space left on device */
+#define ESPIPE 29 /* Illegal seek */
+#define EROFS 30 /* Read-only file system */
+#define EMLINK 31 /* Too many links */
+#define EPIPE 32 /* Broken pipe */
+#define EDOM 33 /* Math argument out of domain of func */
+#define ERANGE 34 /* Math result not representable */
+
+#endif \ No newline at end of file
diff --git a/na/include/lume/new.h b/na/include/lume/new.h
new file mode 100644
index 0000000..cdcacb8
--- /dev/null
+++ b/na/include/lume/new.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Placement New for Freestanding Environments
+ *
+ * In -ffreestanding -nostdlib mode, <new> may not be available.
+ * LumeOS provides its own placement new/delete operators.
+ *
+ * Usage: g_obj = new (g_obj_buf) MyClass(args...);
+ *
+ * Reference: docs/specs/global_init.md §3.1
+ */
+
+inline void* operator new(decltype(sizeof(0)), void* ptr) noexcept { return ptr; }
+inline void operator delete(void*, void*) noexcept {}
diff --git a/na/include/lume/panic.h b/na/include/lume/panic.h
new file mode 100644
index 0000000..6316c28
--- /dev/null
+++ b/na/include/lume/panic.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Kernel Panic — fatal error handler.
+ *
+ * Prints the message (if UART is ready), disables interrupts,
+ * and enters an infinite wfi loop. Never returns.
+ *
+ * Implementation will be provided by lib/kprintf.cc or a
+ * dedicated panic module. For now this header declares the interface.
+ */
+
+extern "C" [[noreturn]] void kernel_panic(const char* msg, const char* detail = nullptr);
diff --git a/na/include/lume/types.h b/na/include/lume/types.h
new file mode 100644
index 0000000..a8440f6
--- /dev/null
+++ b/na/include/lume/types.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * LumeOS Base Type Definitions
+ *
+ * All kernel code should use these types instead of
+ * stdint.h (which may not exist in freestanding mode).
+ */
+
+using uint8 = unsigned char;
+using uint16 = unsigned short;
+using uint32 = unsigned int;
+using uint64 = unsigned long long;
+
+using int8 = signed char;
+using int16 = signed short;
+using int32 = signed int;
+using int64 = signed long long;
+
+using size_t = uint64;
+using ssize_t = int64;
+using uintptr = uint64;
+
+/* Null pointer constant */
+#ifndef nullptr
+/* C++11 and later have nullptr built-in */
+#endif
+
+/* Max CPUs supported */
+inline constexpr int kMaxCPUs = 8;
+
+/* Page size */
+inline constexpr uint64 kPageSize = 4096;
+
+/* Kernel virtual address offset */
+inline constexpr uint64 kPageOffset = 0xFFFFFFC000000000ULL;
diff --git a/na/kernel/main.cc b/na/kernel/main.cc
new file mode 100644
index 0000000..b4d792e
--- /dev/null
+++ b/na/kernel/main.cc
@@ -0,0 +1,146 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * LumeOS Kernel Main Entry — Global Initialization Sequence
+ *
+ * All CPUs enter kernel_main() after the assembly bootstrap (entry.S)
+ * has enabled MMU, cleared BSS, and called early_cpp_init().
+ *
+ * BSP (cpu_id == 0) executes the full subsystem initialization DAG.
+ * APs spin on ap_boot_sync, then perform per-CPU init only.
+ *
+ * Reference: docs/specs/global_init.md
+ */
+
+#include <lume/types.h>
+#include <lume/panic.h>
+#include <arch/cpu.h>
+
+// ============================================================
+// Forward declarations for subsystem init functions.
+// Each is implemented by its owning module. On failure they
+// call kernel_panic() directly — no error return.
+// ============================================================
+
+// BSP global init (strict order)
+void console_init(); // 1. UART ready; kprintf/panic can output
+void fdt_init(uint64 fdt_paddr); // 2. Parse FDT, build device node table
+void pmm_init(); // 3. Buddy allocator ready
+void slab_init(); // 4. kmem_cache ready; operator new usable
+void vmm_init(); // 5. Fine-grained kernel page table; retire early_pgdir
+void trap_init(); // 6. Set stvec, register IRQ dispatcher
+void plic_init(); // 7. Configure PLIC (threshold, priority, enable)
+void timer_init(); // 8. Configure timer interrupt (scheduler tick)
+void sched_init(); // 9. Create BSP Idle Task, init ready queues
+
+// AP per-CPU init
+void vmm_init_ap(); // Switch satp to BSP-built kernel page table
+void trap_init_ap(); // Set this CPU's stvec
+void plic_init_ap(); // Enable this CPU's PLIC context
+void timer_init_ap(); // Start this CPU's timer interrupt
+void sched_init_ap(); // Init per-CPU ready queue, create Idle Task
+
+// ============================================================
+// Second-stage synchronization barrier (C++ layer)
+// AP cores spin here until BSP finishes all global init.
+// ============================================================
+static volatile uint32 ap_boot_sync = 0;
+
+// Early UART output functions, defined in lib/panic.cc
+// They are safe to use before console_init().
+extern "C" void early_putc(char c);
+extern "C" void early_puts(const char* s);
+
+// ============================================================
+// kernel_main — C++ entry point for all CPUs
+// ============================================================
+extern "C" [[noreturn]] void kernel_main(uint64 cpu_id, uint64 fdt_paddr) {
+
+ if (cpu_id == 0) {
+ // ========================================================
+ // BSP: Absolute single-core period — no locks needed.
+ // APs are spinning on ap_boot_sync.
+ // ========================================================
+
+ early_puts("\n\n");
+ early_puts("====================================\n");
+ early_puts(" LumeOS Kernel v0.1 (RISC-V 64)\n");
+ early_puts("====================================\n");
+ early_puts("[boot] BSP online, cpu_id=0\n");
+ early_puts("[boot] Higher-half kernel active\n");
+
+ // --- Subsystem init DAG (strict top-down order) ---
+
+ // 1. UART driver — after this, kernel_panic() can output
+ // console_init();
+ early_puts("[init] console_init (stub)\n");
+
+ // 2. FDT parsing
+ // fdt_init(fdt_paddr);
+ (void)fdt_paddr;
+ early_puts("[init] fdt_init (stub)\n");
+
+ // 3. Physical memory manager (Buddy)
+ // pmm_init();
+ early_puts("[init] pmm_init (stub)\n");
+
+ // 4. Slab allocator — operator new usable after this
+ // slab_init();
+ early_puts("[init] slab_init (stub)\n");
+
+ // 5. Virtual memory manager — retire early_pgdir
+ // vmm_init();
+ early_puts("[init] vmm_init (stub)\n");
+
+ // 6. Trap handler — set stvec
+ // trap_init();
+ early_puts("[init] trap_init (stub)\n");
+
+ // 7. PLIC interrupt controller
+ // plic_init();
+ early_puts("[init] plic_init (stub)\n");
+
+ // 8. Timer interrupt (scheduler tick source)
+ // timer_init();
+ early_puts("[init] timer_init (stub)\n");
+
+ // 9. Scheduler — create Idle Task 0, init ready queues
+ // sched_init();
+ early_puts("[init] sched_init (stub)\n");
+
+ early_puts("[boot] BSP init complete, releasing APs\n");
+
+ // Release APs with RELEASE semantics:
+ // guarantees all global data is visible to APs.
+ __atomic_store_n(&ap_boot_sync, 1, __ATOMIC_RELEASE);
+
+ // BSP enables interrupts — first timer tick will preempt
+ // into the scheduler loop.
+ // arch::cpu::intr_on();
+
+ early_puts("[boot] BSP entering idle loop\n");
+
+ } else {
+ // ========================================================
+ // AP: spin until BSP finishes global init
+ // ========================================================
+ while (__atomic_load_n(&ap_boot_sync, __ATOMIC_ACQUIRE) == 0)
+ ;
+
+ // --- Per-CPU init only (no global data mutation) ---
+ // vmm_init_ap();
+ // trap_init_ap();
+ // plic_init_ap();
+ // timer_init_ap();
+ // sched_init_ap();
+
+ // AP enables interrupts — becomes Idle Task on this CPU
+ // arch::cpu::intr_on();
+ }
+
+ // All CPUs: idle loop (wfi until interrupt)
+ while (true) {
+ arch::cpu::halt_until_interrupt();
+ }
+}
diff --git a/na/kernel/module.mk b/na/kernel/module.mk
new file mode 100644
index 0000000..208064f
--- /dev/null
+++ b/na/kernel/module.mk
@@ -0,0 +1,3 @@
+# kernel/module.mk
+SRCS_CC += kernel/main.cc
+SRCS_CC += kernel/sync/spinlock.cc
diff --git a/na/kernel/sync/cpu_state.h b/na/kernel/sync/cpu_state.h
new file mode 100644
index 0000000..d0e3814
--- /dev/null
+++ b/na/kernel/sync/cpu_state.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Per-CPU Interrupt Nesting Tracker
+ *
+ * Tracks spinlock nesting depth on each CPU to ensure interrupts
+ * are only restored when the last spinlock is released.
+ *
+ * BSS-segment global array; zero-initialized by entry.S BSS clear.
+ * Reference: docs/specs/spinlock.md §3.1
+ */
+
+#include <lume/types.h>
+
+namespace sync {
+
+struct CpuSyncState {
+ int lock_depth; // Number of spinlocks held by this CPU
+ bool intr_was_on; // Were interrupts enabled before the first lock?
+};
+
+inline constexpr uint64 kInvalidCpuId = ~0ULL;
+
+// BSS-segment global array, zero-initialized by entry.S
+extern CpuSyncState g_cpu_sync_states[kMaxCPUs];
+
+// Internal API — called only by Spinlock::acquire/release
+void push_intr();
+void pop_intr();
+
+} // namespace sync
diff --git a/na/kernel/sync/spinlock.cc b/na/kernel/sync/spinlock.cc
new file mode 100644
index 0000000..a03130d
--- /dev/null
+++ b/na/kernel/sync/spinlock.cc
@@ -0,0 +1,111 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Spinlock Implementation
+ *
+ * TTAS (Test-and-Test-and-Set) spinlock with:
+ * - Acquire: __ATOMIC_ACQUIRE memory ordering
+ * - Release: __ATOMIC_RELEASE memory ordering
+ * - Automatic interrupt disable/enable via push_intr/pop_intr
+ * - Dead-lock and invalid-release detection via kernel_panic
+ *
+ * Reference: docs/specs/spinlock.md
+ */
+
+#include "spinlock.h"
+#include "cpu_state.h"
+#include <arch/cpu.h>
+#include <lume/panic.h>
+
+// ============================================================
+// Per-CPU sync state (BSS zero-initialized)
+// ============================================================
+namespace sync {
+
+CpuSyncState g_cpu_sync_states[kMaxCPUs];
+
+void push_intr() {
+ bool was_on = arch::cpu::intr_enabled();
+ arch::cpu::intr_off();
+
+ auto& state = g_cpu_sync_states[arch::cpu::id()];
+ if (state.lock_depth == 0) {
+ state.intr_was_on = was_on;
+ }
+ state.lock_depth++;
+}
+
+void pop_intr() {
+ auto& state = g_cpu_sync_states[arch::cpu::id()];
+ state.lock_depth--;
+
+ if (state.lock_depth == 0 && state.intr_was_on) {
+ arch::cpu::intr_on();
+ }
+}
+
+} // namespace sync
+
+// ============================================================
+// Spinlock methods
+// ============================================================
+
+void Spinlock::init(const char* name) {
+ name_ = name;
+ locked_ = 0;
+ cpu_id_ = sync::kInvalidCpuId;
+}
+
+void Spinlock::acquire() {
+#ifdef DEBUG
+ // Optional: detect interrupt-state leakage
+ if (arch::cpu::intr_enabled()) {
+ auto& state = sync::g_cpu_sync_states[arch::cpu::id()];
+ if (state.lock_depth > 0) {
+ kernel_panic("spinlock: interrupts enabled while holding lock");
+ }
+ }
+#endif
+
+ // Step 1: disable interrupts, track nesting depth
+ sync::push_intr();
+
+ // Step 2: dead-lock detection (same CPU re-acquiring)
+ if (is_held_by_current_cpu()) {
+ kernel_panic("spinlock: double lock", name_);
+ }
+
+ // Step 3: TTAS spin loop
+ while (true) {
+ // Outer: spin on local cache copy (no bus traffic)
+ while (__atomic_load_n(&locked_, __ATOMIC_RELAXED))
+ ;
+ // Inner: attempt atomic exchange with acquire semantics
+ if (__atomic_exchange_n(&locked_, 1, __ATOMIC_ACQUIRE) == 0)
+ break;
+ }
+
+ // Step 4: record owner
+ cpu_id_ = arch::cpu::id();
+}
+
+void Spinlock::release() {
+ // Step 1: verify caller is the holder
+ if (!is_held_by_current_cpu()) {
+ kernel_panic("spinlock: invalid release", name_);
+ }
+
+ // Step 2: clear owner
+ cpu_id_ = sync::kInvalidCpuId;
+
+ // Step 3: release the lock with release semantics
+ __atomic_store_n(&locked_, 0, __ATOMIC_RELEASE);
+
+ // Step 4: restore interrupt state if this was the last lock
+ sync::pop_intr();
+}
+
+bool Spinlock::is_held_by_current_cpu() const {
+ return cpu_id_ == arch::cpu::id();
+}
diff --git a/na/kernel/sync/spinlock.h b/na/kernel/sync/spinlock.h
new file mode 100644
index 0000000..b3e3393
--- /dev/null
+++ b/na/kernel/sync/spinlock.h
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Spinlock & LockGuard
+ *
+ * Provides mutual exclusion for SMP kernels using TTAS
+ * (Test-and-Test-and-Set) with acquire/release memory ordering.
+ * Interrupt disable/enable is managed automatically via push_intr/pop_intr.
+ *
+ * Reference: docs/specs/spinlock.md
+ */
+
+#include <lume/types.h>
+#include "cpu_state.h"
+
+class Spinlock {
+public:
+ // Default: BSS zero is safe. Call init() to give the lock a name.
+ Spinlock() = default;
+
+ // Explicit initialization (for global objects or Placement New)
+ void init(const char* name);
+
+ // Non-copyable, non-movable (lock identity is its address)
+ Spinlock(const Spinlock&) = delete;
+ Spinlock& operator=(const Spinlock&) = delete;
+ Spinlock(Spinlock&&) = delete;
+ Spinlock& operator=(Spinlock&&) = delete;
+
+ void acquire();
+ void release();
+ bool is_held_by_current_cpu() const;
+
+private:
+ uint32 locked_ = 0; // 0 = free, 1 = held
+ const char* name_ = "uninit"; // Lock name (for panic messages)
+ uint64 cpu_id_ = sync::kInvalidCpuId; // Holder CPU ID
+};
+
+class LockGuard {
+public:
+ explicit LockGuard(Spinlock& lock) : lock_(lock) {
+ lock_.acquire();
+ }
+
+ ~LockGuard() {
+ lock_.release();
+ }
+
+ LockGuard(const LockGuard&) = delete;
+ LockGuard& operator=(const LockGuard&) = delete;
+ LockGuard(LockGuard&&) = delete;
+ LockGuard& operator=(LockGuard&&) = delete;
+
+private:
+ Spinlock& lock_;
+};
diff --git a/na/lib/module.mk b/na/lib/module.mk
new file mode 100644
index 0000000..46c46d4
--- /dev/null
+++ b/na/lib/module.mk
@@ -0,0 +1,2 @@
+# lib/module.mk
+SRCS_CC += lib/panic.cc
diff --git a/na/lib/panic.cc b/na/lib/panic.cc
new file mode 100644
index 0000000..4745272
--- /dev/null
+++ b/na/lib/panic.cc
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Kernel Panic — minimal fatal error handler.
+ *
+ * Before console_init(): silent death (no UART).
+ * After console_init(): prints message then halts.
+ *
+ * Reference: docs/specs/global_init.md §6
+ */
+
+#include <lume/types.h>
+#include <arch/cpu.h>
+
+// Minimal direct UART output (same as main.cc early boot)
+// This can be used by other early-boot code before console_init()
+
+constexpr uint64 kUart0VA = 0x10000000UL + 0xFFFFFFC000000000ULL;
+
+extern "C" void early_putc(char c) {
+ volatile auto* uart = reinterpret_cast<volatile uint8*>(kUart0VA);
+ *uart = static_cast<uint8>(c);
+}
+
+extern "C" void early_puts(const char* s) {
+ while (*s) early_putc(*s++);
+}
+
+extern "C" [[noreturn]] void kernel_panic(const char* msg, const char* detail) {
+ arch::cpu::intr_off();
+
+ early_puts("\n*** KERNEL PANIC: ");
+ if (msg) {
+ early_puts(msg);
+ }
+ if (detail) {
+ early_puts(": ");
+ early_puts(detail);
+ }
+ early_puts(" ***\n");
+
+ while (true) {
+ arch::cpu::halt_until_interrupt();
+ }
+}
diff --git a/na/linker.ld b/na/linker.ld
new file mode 100644
index 0000000..54f619c
--- /dev/null
+++ b/na/linker.ld
@@ -0,0 +1,111 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * LumeOS Kernel Linker Script
+ *
+ * Memory Layout (SV39 Higher-Half Kernel):
+ * Virtual Base: 0xFFFFFFC080200000
+ * Physical Base: 0x80200000
+ * VA-PA Offset: 0xFFFFFFC000000000 (PAGE_OFFSET)
+ *
+ * The AT() directives produce correct LMA (Load Memory Address) so the
+ * bootloader loads sections at their physical addresses while the kernel
+ * code references their virtual addresses.
+ *
+ * Reference: docs/specs/linker_and_entry.md
+ */
+
+OUTPUT_ARCH(riscv)
+
+PAGE_OFFSET = 0xFFFFFFC000000000;
+BASE_ADDRESS = 0xFFFFFFC080200000;
+PHYS_BASE = 0x80200000;
+
+/* ELF entry point must be the PHYSICAL address because OpenSBI/QEMU
+ * jumps to it before MMU is enabled. _start is at VMA, so we use
+ * the computed physical equivalent. */
+ENTRY(_start)
+
+SECTIONS {
+ . = BASE_ADDRESS;
+
+ /* ===== .head.text: Image Header (must be at byte 0) ===== */
+ .head.text : AT(PHYS_BASE) {
+ _start = .;
+ KEEP(*(.head.text))
+ }
+
+ /* ===== .text: Executable Code ===== */
+ .text : AT(ADDR(.text) - PAGE_OFFSET) {
+ _stext = .;
+ *(.text .text.*)
+ _etext = .;
+ } = 0
+
+ . = ALIGN(4096);
+
+ /* ===== .rodata: Read-Only Data ===== */
+ .rodata : AT(ADDR(.rodata) - PAGE_OFFSET) {
+ _srodata = .;
+ *(.rodata .rodata.*)
+
+ /* C++ global constructor table (.init_array) */
+ . = ALIGN(8);
+ __init_array_start = .;
+ KEEP(*(.init_array .init_array.*))
+ __init_array_end = .;
+
+ _erodata = .;
+ }
+
+ . = ALIGN(4096);
+
+ /* ===== .data: Initialized Read-Write Data ===== */
+ .data : AT(ADDR(.data) - PAGE_OFFSET) {
+ _sdata = .;
+ *(.data .data.*)
+
+ /* GP anchor: RISC-V medany model uses gp-relative addressing
+ * for .sdata/.sbss within +/- 2KB of __global_pointer$ */
+ . = ALIGN(16);
+ PROVIDE(__global_pointer$ = . + 0x800);
+ *(.sdata .sdata.*)
+
+ _edata = .;
+ }
+
+ . = ALIGN(4096);
+
+ /* ===== .bss: Uninitialized Data (zeroed by entry.S) ===== */
+ .bss : AT(ADDR(.bss) - PAGE_OFFSET) {
+ _sbss = .;
+ *(.bss .bss.*)
+ *(.sbss .sbss.*)
+
+ /* Early page table: 4KB aligned, used by entry.S before VMM */
+ . = ALIGN(4096);
+ early_pgdir = .;
+ . += 4096;
+
+ /* Per-CPU boot stacks: 8KB per CPU, max 8 CPUs */
+ . = ALIGN(4096);
+ boot_stack = .;
+ . += 8192 * 8; /* NCPU_MAX = 8, BOOT_STACK_SIZE = 8KB */
+ boot_stack_top = .;
+
+ _ebss = .;
+ }
+
+ . = ALIGN(4096);
+
+ /* ===== End of kernel image ===== */
+ _kernel_end = .;
+
+ /* Discard unwanted sections */
+ /DISCARD/ : {
+ *(.eh_frame .eh_frame_hdr)
+ *(.note .note.*)
+ *(.comment)
+ }
+} \ No newline at end of file