summaryrefslogtreecommitdiff
path: root/na/arch/riscv/include
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:24:56 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:24:56 +0800
commit1767d390fbf3d0c65389de46caf0b6d43ef3b6ad (patch)
tree0f5e01cd7c7a05abb6b1c7472d7f00f49caf02cc /na/arch/riscv/include
parent63dc025705f6533b91fb1a7d26e6ed864efd9900 (diff)
downloadlume-1767d390fbf3d0c65389de46caf0b6d43ef3b6ad.tar.gz
lume-1767d390fbf3d0c65389de46caf0b6d43ef3b6ad.tar.xz
feat: establish bootable new architecture checkpoint
Replace the compressed boot path with a relocatable higher-half RISC-V entry, expand memory and SMP support, and add the driver, TTY, VFS, exec, signal, syscall, initramfs, and self-test infrastructure needed for a bootable kernel checkpoint.
Diffstat (limited to 'na/arch/riscv/include')
-rw-r--r--na/arch/riscv/include/arch/config.h5
-rw-r--r--na/arch/riscv/include/arch/cpu.h16
-rw-r--r--na/arch/riscv/include/arch/fpstate.h94
-rw-r--r--na/arch/riscv/include/arch/mmu.h15
-rw-r--r--na/arch/riscv/include/arch/pmap.h1
-rw-r--r--na/arch/riscv/include/arch/sbi.h80
-rw-r--r--na/arch/riscv/include/arch/tlb.h19
-rw-r--r--na/arch/riscv/include/arch/trap.h12
-rw-r--r--na/arch/riscv/include/arch/trapframe.h48
-rw-r--r--na/arch/riscv/include/asm/csr.h37
-rw-r--r--na/arch/riscv/include/asm/page.h62
-rw-r--r--na/arch/riscv/include/asm/pgtable.h55
-rw-r--r--na/arch/riscv/include/asm/sections.h50
13 files changed, 490 insertions, 4 deletions
diff --git a/na/arch/riscv/include/arch/config.h b/na/arch/riscv/include/arch/config.h
index 0949c41..8ae0502 100644
--- a/na/arch/riscv/include/arch/config.h
+++ b/na/arch/riscv/include/arch/config.h
@@ -35,6 +35,11 @@ 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
*
diff --git a/na/arch/riscv/include/arch/cpu.h b/na/arch/riscv/include/arch/cpu.h
index d16cf2d..8c573ec 100644
--- a/na/arch/riscv/include/arch/cpu.h
+++ b/na/arch/riscv/include/arch/cpu.h
@@ -76,4 +76,20 @@ 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/na/arch/riscv/include/arch/fpstate.h b/na/arch/riscv/include/arch/fpstate.h
new file mode 100644
index 0000000..4b42a22
--- /dev/null
+++ b/na/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/na/arch/riscv/include/arch/mmu.h b/na/arch/riscv/include/arch/mmu.h
index a0ee1ed..f4c9b26 100644
--- a/na/arch/riscv/include/arch/mmu.h
+++ b/na/arch/riscv/include/arch/mmu.h
@@ -50,6 +50,14 @@ static inline uint64 get_page_table() {
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");
@@ -60,4 +68,11 @@ 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/na/arch/riscv/include/arch/pmap.h b/na/arch/riscv/include/arch/pmap.h
index a99825a..0b43b9e 100644
--- a/na/arch/riscv/include/arch/pmap.h
+++ b/na/arch/riscv/include/arch/pmap.h
@@ -35,6 +35,7 @@ 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);
diff --git a/na/arch/riscv/include/arch/sbi.h b/na/arch/riscv/include/arch/sbi.h
index b65c37e..80a6d7c 100644
--- a/na/arch/riscv/include/arch/sbi.h
+++ b/na/arch/riscv/include/arch/sbi.h
@@ -3,6 +3,15 @@
#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 {
@@ -12,7 +21,20 @@ struct sbiret {
long value;
};
-static inline sbiret sbi_ecall(int ext, int fid, uint64 arg0, uint64 arg1, uint64 arg2, uint64 arg3, uint64 arg4, uint64 arg5) {
+/* 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;
@@ -23,7 +45,7 @@ static inline sbiret sbi_ecall(int ext, int fid, uint64 arg0, uint64 arg1, uint6
register uint64 a6 asm("a6") = fid;
register uint64 a7 asm("a7") = ext;
__asm__ volatile("ecall"
- : "+r"(a0), "=r"(a1)
+ : "+r"(a0), "+r"(a1)
: "r"(a2), "r"(a3), "r"(a4), "r"(a5), "r"(a6), "r"(a7)
: "memory");
ret.error = static_cast<long>(a0);
@@ -31,9 +53,59 @@ static inline sbiret sbi_ecall(int ext, int fid, uint64 arg0, uint64 arg1, uint6
return ret;
}
+/* ---- Timer Extension (EID 0x54494D45) ---- */
+
static inline void set_timer(uint64 stime_value) {
- // Standard SBI TIME Extension (EID 0x54494D45)
- sbi_ecall(0x54494D45, 0, stime_value, 0, 0, 0, 0, 0);
+ 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/na/arch/riscv/include/arch/tlb.h b/na/arch/riscv/include/arch/tlb.h
new file mode 100644
index 0000000..46c6b97
--- /dev/null
+++ b/na/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/na/arch/riscv/include/arch/trap.h b/na/arch/riscv/include/arch/trap.h
index c99d8cb..8e55250 100644
--- a/na/arch/riscv/include/arch/trap.h
+++ b/na/arch/riscv/include/arch/trap.h
@@ -79,4 +79,16 @@ 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/na/arch/riscv/include/arch/trapframe.h b/na/arch/riscv/include/arch/trapframe.h
index de49e9c..b198646 100644
--- a/na/arch/riscv/include/arch/trapframe.h
+++ b/na/arch/riscv/include/arch/trapframe.h
@@ -61,6 +61,7 @@
#ifndef __ASSEMBLER__
#include <lume/types.h>
+#include <arch/cpu.h>
namespace arch {
@@ -89,4 +90,51 @@ static_assert(sizeof(TrapFrame) == 296, "TrapFrame size mismatch with assembly o
} // 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/na/arch/riscv/include/asm/csr.h b/na/arch/riscv/include/asm/csr.h
new file mode 100644
index 0000000..42f1eb4
--- /dev/null
+++ b/na/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/na/arch/riscv/include/asm/page.h b/na/arch/riscv/include/asm/page.h
new file mode 100644
index 0000000..7b9f155
--- /dev/null
+++ b/na/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/na/arch/riscv/include/asm/pgtable.h b/na/arch/riscv/include/asm/pgtable.h
new file mode 100644
index 0000000..28fc4c5
--- /dev/null
+++ b/na/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/na/arch/riscv/include/asm/sections.h b/na/arch/riscv/include/asm/sections.h
new file mode 100644
index 0000000..454d9de
--- /dev/null
+++ b/na/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 */