From c5cd954711b54cabd2afd6c98a1a957b5287519d Mon Sep 17 00:00:00 2001 From: Daniel-De-Dev Date: Sat, 19 Sep 2026 17:02:24 +0200 Subject: [PATCH 1/4] refactor: linker & compilation Restructured the linker to have a new boot section and start using virtual memory for the main sections of the kernel, while boot section uses physical memory. Idea is to have a baked in bootstrap page embedded into the kernel binary, which will be used to initially start virtual memory. --- kernel/linker.ld.in | 183 ++++++++++++++++++++++++++++++++----- kernel/src/arch/riscv64.rs | 2 +- nix/boards.nix | 6 +- nix/kernel.nix | 3 + nix/packages.nix | 1 + 5 files changed, 169 insertions(+), 26 deletions(-) diff --git a/kernel/linker.ld.in b/kernel/linker.ld.in index b56557a..c59ab17 100644 --- a/kernel/linker.ld.in +++ b/kernel/linker.ld.in @@ -1,48 +1,153 @@ ENTRY(_start) -BASE_ADDRESS = @kernelAddress@; +PAGE_SIZE = 4K; +GIGAPAGE_SIZE = 0x40000000; /* 1 GiB */ + +KERNEL_HIGH_BASE = 0xffffffff80000000; + +KERNEL_OFFSET = @kernelOffset@; + +KERNEL_PHYSICAL_BASE = @kernelAddress@; +KERNEL_VIRTUAL_BASE = KERNEL_HIGH_BASE + KERNEL_OFFSET; + +KERNEL_REGION_SIZE = @kernelRegionSize@; ASSERT( - (BASE_ADDRESS % 4K) == 0, - "kernel load address must be 4 KiB aligned" + (KERNEL_OFFSET % PAGE_SIZE) == 0, + "kernel offset must be page aligned" ) ASSERT( - _kernel_end > _kernel_start, - "kernel image must be non-empty" + KERNEL_OFFSET < GIGAPAGE_SIZE, + "kernel offset must stay within the bootstrap gigapage" +) + +ASSERT( + (KERNEL_HIGH_BASE % GIGAPAGE_SIZE) == 0, + "kernel high-half base must be 1 GiB aligned" +) + +ASSERT( + (KERNEL_PHYSICAL_BASE % GIGAPAGE_SIZE) == KERNEL_OFFSET, + "kernel physical address must match its offset within the bootstrap gigapage" ) -MEMORY -{ - KERNEL (rwx) : ORIGIN = @kernelAddress@, LENGTH = @kernelRegionSize@ -} SECTIONS { - . = BASE_ADDRESS; + /* + * Physical bootstrap + */ + + . = KERNEL_PHYSICAL_BASE; + + _kernel_physical_start = .; + + /* + * Early bootstrap code and constants. + */ + .boot : { + KEEP(*(.boot.text)) + KEEP(*(.boot.text.*)) + + KEEP(*(.boot.rodata)) + KEEP(*(.boot.rodata.*)) + } + + /* + * Reserve one 4 KiB page for the temporary Sv39 root page table. + */ + . = ALIGN(PAGE_SIZE); + + .boot.page_table (NOLOAD) : { + __boot_page_table = .; + + . += PAGE_SIZE; + + __boot_page_table_end = .; + } + + . = ALIGN(PAGE_SIZE); + + _boot_physical_end = .; + + /* + * Physical space consumed before the normal higher-half kernel begins. + */ + _boot_size = _boot_physical_end - KERNEL_PHYSICAL_BASE; + + + /* + * Higher-half kernel + */ + _kernel_virtual_start = KERNEL_VIRTUAL_BASE; + + . = KERNEL_VIRTUAL_BASE + _boot_size; + + . = ALIGN(PAGE_SIZE); - _kernel_start = .; - .text : { + /* + * Executable kernel code. + * + * VMA: + * high-half virtual address + * + * LMA: + * corresponding physical address inside the loaded kernel image + */ + .text : AT( + KERNEL_PHYSICAL_BASE + + (ADDR(.text) - KERNEL_VIRTUAL_BASE) + ) { + _text_start = .; + KEEP(*(.text.init)) *(.text .text.*) - } > KERNEL - .rodata : { + _text_end = .; + } + + . = ALIGN(PAGE_SIZE); + + .rodata : AT( + KERNEL_PHYSICAL_BASE + + (ADDR(.rodata) - KERNEL_VIRTUAL_BASE) + ) { + _rodata_start = .; + *(.srodata .srodata.*) *(.rodata .rodata.*) - } > KERNEL - .data : { + _rodata_end = .; + } + + . = ALIGN(PAGE_SIZE); + + + .data : AT( + KERNEL_PHYSICAL_BASE + + (ADDR(.data) - KERNEL_VIRTUAL_BASE) + ) { + _data_start = .; + . = ALIGN(8); __global_pointer$ = . + 0x800; *(.sdata .sdata.*) *(.data .data.*) - } > KERNEL - .bss (NOLOAD) : { + _data_end = .; + } + + . = ALIGN(PAGE_SIZE); + + + .bss (NOLOAD) : AT( + KERNEL_PHYSICAL_BASE + + (ADDR(.bss) - KERNEL_VIRTUAL_BASE) + ) { . = ALIGN(8); _bss_start = .; @@ -52,16 +157,50 @@ SECTIONS *(COMMON) . = ALIGN(8); + _bss_end = .; - } > KERNEL + } + + . = ALIGN(PAGE_SIZE); - .stack (NOLOAD) : { + .stack (NOLOAD) : AT( + KERNEL_PHYSICAL_BASE + + (ADDR(.stack) - KERNEL_VIRTUAL_BASE) + ) { . = ALIGN(16); _stack_start = .; + . += 16K; + _stack_end = .; - } > KERNEL + } + + . = ALIGN(PAGE_SIZE); + + _kernel_virtual_end = .; + - _kernel_end = .; + /* + * Translate the final virtual offset back into its physical equivalent. + */ + _kernel_physical_end = + KERNEL_PHYSICAL_BASE + + (_kernel_virtual_end - KERNEL_VIRTUAL_BASE); } + +ASSERT( + (__boot_page_table_end - __boot_page_table) == PAGE_SIZE, + "bootstrap page table must occupy exactly one page" +) + +ASSERT( + _kernel_physical_end > _kernel_physical_start, + "kernel image must be non-empty" +) + +ASSERT( + _kernel_physical_end + <= (KERNEL_PHYSICAL_BASE + KERNEL_REGION_SIZE), + "kernel runtime footprint exceeds reserved physical region" +) diff --git a/kernel/src/arch/riscv64.rs b/kernel/src/arch/riscv64.rs index be2cdc1..3d828d3 100644 --- a/kernel/src/arch/riscv64.rs +++ b/kernel/src/arch/riscv64.rs @@ -37,7 +37,7 @@ use core::arch::{asm, global_asm}; // arguments passed to `main`. global_asm!( r#" - .section .text.init + .section .boot.text, "ax" .global _start _start: diff --git a/nix/boards.nix b/nix/boards.nix index 84b693f..274d049 100644 --- a/nix/boards.nix +++ b/nix/boards.nix @@ -31,7 +31,7 @@ dramBase = 2 * GiB; # 0x80000000 in { - inherit kernelRegionSize; + inherit kernelOffset kernelRegionSize; opensbiAddress = dramBase; kernelAddress = dramBase + kernelOffset; @@ -48,7 +48,7 @@ dramBase = 1 * GiB; # 0x40000000 in { - inherit kernelRegionSize; + inherit kernelOffset kernelRegionSize; opensbiAddress = dramBase; kernelAddress = dramBase + kernelOffset; @@ -87,7 +87,7 @@ dramSize = 512 * MiB; # 0x20000000 in { - inherit kernelRegionSize; + inherit kernelOffset kernelRegionSize; inherit dramBase dramSize; opensbiAddress = dramBase; diff --git a/nix/kernel.nix b/nix/kernel.nix index 44d3c2d..6de3f79 100644 --- a/nix/kernel.nix +++ b/nix/kernel.nix @@ -15,6 +15,7 @@ naersk', name, loadAddress, + kernelOffset, regionSize, release ? true, }: @@ -24,10 +25,12 @@ let rustTarget = "riscv64gc-unknown-none-elf"; loadAddressHex = "0x${lib.toHexString loadAddress}"; + kernelOffsetHex = "0x${lib.toHexString kernelOffset}"; regionSizeHex = "0x${lib.toHexString regionSize}"; linkerScript = pkgs.replaceVars ../kernel/linker.ld.in { kernelAddress = loadAddressHex; + kernelOffset = kernelOffsetHex; kernelRegionSize = regionSizeHex; }; in diff --git a/nix/packages.nix b/nix/packages.nix index b75037c..a7284e6 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -33,6 +33,7 @@ ; loadAddress = board.kernelAddress; + inherit (board) kernelOffset; regionSize = board.kernelRegionSize; }; From e453754bf9be9df7875c373e60364e09d05177be Mon Sep 17 00:00:00 2001 From: Daniel-De-Dev Date: Sat, 19 Sep 2026 21:32:29 +0200 Subject: [PATCH 2/4] refactor(_start): kernels entry changed to enable mmu The kernels entry has been refactored to now switch the kernel to high half virtual address, and sets up a single gigapage leaf that adds the mapping. And the identity mapping and the dtb (if it happens to lie in a different gigapage) --- .harper-dictionary.txt | 2 + kernel/linker.ld.in | 12 ++ kernel/src/arch/riscv64.rs | 64 +--------- kernel/src/arch/riscv64/boot.rs | 201 ++++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 62 deletions(-) create mode 100644 kernel/src/arch/riscv64/boot.rs diff --git a/.harper-dictionary.txt b/.harper-dictionary.txt index 5d0f9f4..02d1458 100644 --- a/.harper-dictionary.txt +++ b/.harper-dictionary.txt @@ -18,10 +18,12 @@ RV64 SBI SPL StarFive +Sv39 VisionFive devicetree devicetrees representable +stackless u32 usize v0 diff --git a/kernel/linker.ld.in b/kernel/linker.ld.in index c59ab17..c68d809 100644 --- a/kernel/linker.ld.in +++ b/kernel/linker.ld.in @@ -204,3 +204,15 @@ ASSERT( <= (KERNEL_PHYSICAL_BASE + KERNEL_REGION_SIZE), "kernel runtime footprint exceeds reserved physical region" ) + +ASSERT( + _kernel_physical_end + <= ((KERNEL_PHYSICAL_BASE - KERNEL_OFFSET) + GIGAPAGE_SIZE), + "kernel runtime footprint crosses the bootstrap physical gigapage" +) + +ASSERT( + _kernel_virtual_end + <= (KERNEL_HIGH_BASE + GIGAPAGE_SIZE), + "kernel runtime footprint crosses the bootstrap virtual gigapage" +) diff --git a/kernel/src/arch/riscv64.rs b/kernel/src/arch/riscv64.rs index 3d828d3..0a99b47 100644 --- a/kernel/src/arch/riscv64.rs +++ b/kernel/src/arch/riscv64.rs @@ -2,72 +2,12 @@ //! //! This module provides the architecture-specific boundary between the //! kernel and an RV64 execution environment. -//! -//! # Kernel entry -//! -//! The `_start` entry point establishes the minimum execution environment -//! required before entering Rust: -//! -//! 1. Initialize the global pointer (`gp`). -//! 2. Initialize the stack pointer (`sp`). -//! 3. Clear the `.bss` section. -//! 4. Transfer control to [`crate::main`]. -//! -//! The firmware-provided `a0` and `a1` registers are deliberately preserved -//! so they are passed to `main` as the hart ID and device-tree address, -//! respectively. -//! -//! The linker script provides the symbols used during initialization, -//! including `__global_pointer$`, `_stack_end`, `_bss_start`, and `_bss_end`. -//! -//! `_bss_start` and `_bss_end` are 8-byte aligned because startup clears -//! `.bss` using 8-byte stores. +mod boot; mod trap; pub(crate) use trap::init as init_trap; -use core::arch::{asm, global_asm}; - -// Kernel entry point. -// -// Establish the minimum execution environment required by Rust before -// transferring control to `main`. -// -// `a0` and `a1` are deliberately preserved so they remain the first two -// arguments passed to `main`. -global_asm!( - r#" - .section .boot.text, "ax" - .global _start - -_start: - .option push - .option norelax - - /* Initialize gp */ - la gp, __global_pointer$ - - .option pop - - /* Initialize stack */ - la sp, _stack_end - - /* Clear .bss */ - la t0, _bss_start - la t1, _bss_end - -.bss_loop: - bgeu t0, t1, .bss_done - - sd zero, 0(t0) - addi t0, t0, 8 - - j .bss_loop - -.bss_done: - tail main - "# -); +use core::arch::asm; /// Parks the current hart indefinitely. /// diff --git a/kernel/src/arch/riscv64/boot.rs b/kernel/src/arch/riscv64/boot.rs new file mode 100644 index 0000000..22b8377 --- /dev/null +++ b/kernel/src/arch/riscv64/boot.rs @@ -0,0 +1,201 @@ +//! RISC-V kernel bootstrap. +//! +//! Startup is split into two stages: +//! +//! - `_start` executes at the physical load address with translation disabled. +//! It creates a minimal Sv39 root table containing temporary 1 GiB identity +//! and higher-half mappings, enables paging, and jumps to the higher-half +//! kernel entry. +//! - `__high_half_start` establishes the Rust execution environment by +//! initializing `gp` and `sp`, clearing `.bss`, and entering [`crate::main`]. +//! +//! The bootstrap intentionally uses only a single root page table and level-2 +//! Sv39 leaves. The linker guarantees that the complete kernel boot footprint +//! fits within one physical 1 GiB region and the corresponding higher-half +//! virtual region. +//! +//! OpenSBI supplies the hart ID in `a0` and the physical device-tree address in +//! `a1`. Both registers are preserved until `main` is entered. +//! +//! The DTB's containing 1 GiB region is identity-mapped so the existing +//! physical pointer remains usable during early initialization. A DTB crossing +//! a 1 GiB boundary would require mapping the following region as well. + +use core::arch::global_asm; + +// Physical entry point and higher-half transition. +// +// This code runs before a Rust execution environment exists and must therefore +// remain stackless until `__high_half_start`. +global_asm!( + r#" + /* constants. */ + .equ PAGE_SIZE, 4096 + .equ PAGE_SHIFT, 12 + .equ GIGAPAGE_SHIFT, 30 + .equ PTE_BYTE_SHIFT, 3 + .equ VPN2_MASK, 0x1ff + .equ PTE_VRWXAD, 0xcf + .equ SATP_MODE_SV39, 8 + .equ SATP_MODE_SHIFT, 60 + + /* + * Physical bootstrap. + * + * Entry: + * a0 = hart ID + * a1 = physical DTB address + * + * Persistent register state: + * t0 = physical root page-table address + * t3 = physical kernel gigapage base + * t4 = kernel gigapage leaf PTE + * t6 = virtual address of __high_half_start + */ + + .section .boot.text, "ax" + .global _start + +_start: + lla t0, __boot_page_table + mv t1, t0 + + li t2, PAGE_SIZE + add t2, t0, t2 + +.Lclear_boot_page_table: + bgeu t1, t2, .Lboot_page_table_cleared + + sd zero, 0(t1) + addi t1, t1, 8 + j .Lclear_boot_page_table + +.Lboot_page_table_cleared: + /* + * The high entry is outside the range of a low PC-relative address. + * Load its full virtual address through a nearby bootstrap literal. + */ + lla t5, .Lhigh_half_start_address + ld t6, 0(t5) + + /* Round _start down to the containing physical 1 GiB region. */ + lla t3, _start + srli t3, t3, GIGAPAGE_SHIFT + slli t3, t3, GIGAPAGE_SHIFT + + /* + * Construct an RWXAD level-2 leaf for that region. + * + * PPN occupies PTE bits 10+, so: + * (physical_address >> 12) << 10 == physical_address >> 2 + */ + srli t4, t3, 2 + ori t4, t4, PTE_VRWXAD + + /* + * Identity-map the kernel gigapage so the current PC survives satp + * activation. + */ + srli t5, t3, GIGAPAGE_SHIFT + andi t5, t5, VPN2_MASK + slli t5, t5, PTE_BYTE_SHIFT + + add t5, t0, t5 + sd t4, 0(t5) + + /* Map the same physical gigapage into the kernel's higher-half VPN[2]. */ + srli t5, t6, GIGAPAGE_SHIFT + andi t5, t5, VPN2_MASK + slli t5, t5, PTE_BYTE_SHIFT + + add t5, t0, t5 + sd t4, 0(t5) + + /* + * Keep the firmware DTB directly accessible. If it shares the kernel gigapage + * this simply rewrites the same identity entry. + */ + srli t1, a1, GIGAPAGE_SHIFT + slli t1, t1, GIGAPAGE_SHIFT + + srli t2, t1, 2 + ori t2, t2, PTE_VRWXAD + + srli t5, t1, GIGAPAGE_SHIFT + andi t5, t5, VPN2_MASK + slli t5, t5, PTE_BYTE_SHIFT + + add t5, t0, t5 + sd t2, 0(t5) + + /* Order the PTE stores before the MMU begins walking the new table. */ + sfence.vma zero, zero + + /* + * satp = Sv39 | root PPN. + * + * ASID remains zero during bootstrap. + */ + srli t1, t0, PAGE_SHIFT + + li t2, SATP_MODE_SV39 + slli t2, t2, SATP_MODE_SHIFT + + or t1, t1, t2 + csrw satp, t1 + + /* Discard stale translation state after installing the new address space. */ + sfence.vma zero, zero + + /* Continue through the higher-half alias of the kernel image. */ + jr t6 + + + /* + * Nearby storage for the otherwise unreachable high virtual entry address. + */ + + .section .boot.rodata, "a" + .balign 8 + +.Lhigh_half_start_address: + .dword __high_half_start + + + /* + * Higher-half Rust environment. + * + * Normal linker symbols are virtual addresses from this point onward. + */ + + .section .text.init, "ax" + .global __high_half_start + +__high_half_start: + /* + * Prevent relaxation from assuming gp is already initialized while loading + * __global_pointer$ itself. + */ + .option push + .option norelax + la gp, __global_pointer$ + .option pop + + la sp, _stack_end + + /* The bootstrap page table is outside .bss and must remain intact. */ + la t0, _bss_start + la t1, _bss_end + +.Lbss_loop: + bgeu t0, t1, .Lbss_done + + sd zero, 0(t0) + addi t0, t0, 8 + j .Lbss_loop + +.Lbss_done: + /* a0 and a1 still carry OpenSBI's hart ID and physical DTB address. */ + tail main + "# +); From 1930b33c8ec895332425b44928dfb1920753cf1f Mon Sep 17 00:00:00 2001 From: Daniel-De-Dev Date: Sun, 20 Sep 2026 00:42:59 +0200 Subject: [PATCH 3/4] refactor: pass kernel start to main and use it for kernel range The reason for adding the extra argument for main is that high-half compiled code cannot conveniently form the address of a linker symbol whose value lives extremely far away in the low physical address range. Updated and rewrote relevant function and docs for getting the kernels physical memory range. --- kernel/src/arch/riscv64/boot.rs | 8 ++++-- kernel/src/main.rs | 26 ++++++++++++------- kernel/src/memory.rs | 44 ++++++++++++++++++++------------- 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/kernel/src/arch/riscv64/boot.rs b/kernel/src/arch/riscv64/boot.rs index 22b8377..73d3441 100644 --- a/kernel/src/arch/riscv64/boot.rs +++ b/kernel/src/arch/riscv64/boot.rs @@ -14,8 +14,9 @@ //! fits within one physical 1 GiB region and the corresponding higher-half //! virtual region. //! -//! OpenSBI supplies the hart ID in `a0` and the physical device-tree address in -//! `a1`. Both registers are preserved until `main` is entered. +//! `OpenSBI` supplies the hart ID in `a0` and the physical device-tree address in +//! `a1`. Both registers are preserved until `main` is entered. `a2` contains +//! the kernels physical start address. //! //! The DTB's containing 1 GiB region is identity-mapped so the existing //! physical pointer remains usable during early initialization. A DTB crossing @@ -57,6 +58,9 @@ global_asm!( .global _start _start: + /* Preserve the physical kernel base, as it will become innaccessible later */ + lla a2, _kernel_physical_start + lla t0, __boot_page_table mv t1, t0 diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 22d2e9d..b1f94c9 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -26,10 +26,16 @@ use memory::{BootFrameAllocator, PhysAddr, PhysRange}; /// /// This function does not return. #[unsafe(no_mangle)] -extern "C" fn main(hart_id: usize, dtb: usize) -> ! { +extern "C" fn main(hart_id: usize, dtb: usize, kernel_phys_start: usize) -> ! { let dtb_phys = PhysAddr::new(dtb); + let kernel_phys_start = PhysAddr::new(kernel_phys_start); - logging::info!("kernel entered (hart={}, dtb={:#x})", hart_id, dtb_phys); + logging::info!( + "kernel entered (hart={}, dtb={:#x}, kernel_start={:#x})", + hart_id, + dtb_phys, + kernel_phys_start + ); logging::info!("initializing trap handling"); arch::init_trap(); @@ -37,10 +43,9 @@ extern "C" fn main(hart_id: usize, dtb: usize) -> ! { let dtb_ptr = core::ptr::with_exposed_provenance::(dtb_phys.as_usize()); // SAFETY: - // Address translation is not enabled, so the firmware-provided physical DTB - // address is directly addressable by the kernel. The boot environment - // guarantees that it points to a readable, contiguous DTB memory that remains - // valid while it is being parsed. + // The bootstrap page table identity-maps the physical 1 GiB region containing + // the firmware-provided DTB, so its physical address is temporarily also a + // valid virtual address. The DTB remains mapped while it is parsed here. let fdt = match unsafe { Fdt::from_ptr(dtb_ptr) } { Ok(fdt) => fdt, Err(error) => { @@ -64,14 +69,17 @@ extern "C" fn main(hart_id: usize, dtb: usize) -> ! { logging::debug!("{:?}", memory_reservation); } - let kernel_range = memory::kernel_range(); + let Some(kernel_range) = memory::kernel_range(kernel_phys_start) else { + logging::error!("Invalid physical kernel range; kernel startup is unrecoverable, halting"); + + arch::halt(); + }; logging::debug!("Kernel Range: {:?}", kernel_range); let Some(dtb_range) = PhysRange::from_start_size(dtb_phys, fdt.total_size()) else { logging::error!( - "Failed to establish DTB physical range; \ - kernel startup is unrecoverable, halting" + "Failed to establish DTB physical range; kernel startup is unrecoverable, halting" ); arch::halt(); diff --git a/kernel/src/memory.rs b/kernel/src/memory.rs index 25e88ee..e1f5cd8 100644 --- a/kernel/src/memory.rs +++ b/kernel/src/memory.rs @@ -12,7 +12,7 @@ mod address; mod frame; pub(crate) use address::PhysAddr; -pub(crate) use frame::{BootFrameAllocator, BootFrameAllocatorError, PhysFrame}; +pub(crate) use frame::BootFrameAllocator; /// A non-empty half-open physical address range `[start, end)`. /// @@ -28,34 +28,44 @@ pub(crate) struct PhysRange { } unsafe extern "C" { - /// Linker-defined symbol marking the start of the kernel's boot-time - /// physical memory range. - static _kernel_start: u8; + /// Linker-defined symbol marking the start of the kernel's complete virtual + /// boot-time footprint. + static _kernel_virtual_start: u8; - /// Linker-defined symbol marking the end of the kernel's boot-time - /// physical memory range. - static _kernel_end: u8; + /// Linker-defined symbol marking the end of the kernel's complete virtual + /// boot-time footprint. + static _kernel_virtual_end: u8; } -/// Returns the physical memory range reserved for the kernel at boot. +/// Returns the kernel's complete boot-time memory footprint in bytes. /// -/// The range is derived from the linker-defined [`_kernel_start`] and -/// [`_kernel_end`] boundaries and includes the linked kernel sections and -/// statically reserved boot stack. +/// The size includes the bootstrap region and all higher-half kernel sections. /// /// # Panics /// -/// Panics if the linker-provided boundaries do not describe a non-empty range. +/// Panics if the linker-provided virtual kernel boundaries are reversed. #[must_use] #[expect( clippy::expect_used, - reason = "the linker script places _kernel_end strictly after _kernel_start" + reason = "the linker guarantees _kernel_virtual_end is after _kernel_virtual_start" )] -pub(crate) fn kernel_range() -> PhysRange { - let start = PhysAddr::new(core::ptr::addr_of!(_kernel_start).addr()); - let end = PhysAddr::new(core::ptr::addr_of!(_kernel_end).addr()); +fn kernel_size() -> usize { + let start = core::ptr::addr_of!(_kernel_virtual_start).addr(); + let end = core::ptr::addr_of!(_kernel_virtual_end).addr(); + + end + .checked_sub(start) + .expect("linker must produce a non-empty kernel virtual range") +} - PhysRange::new(start, end).expect("linker must produce a non-empty kernel image") +/// Returns the physical memory range reserved for the kernel at boot. +/// +/// `physical_start` is supplied by the architecture bootstrap because the +/// higher-half kernel cannot infer its board-specific physical load address +/// from its virtual location alone. +#[must_use] +pub(crate) fn kernel_range(physical_start: PhysAddr) -> Option { + PhysRange::from_start_size(physical_start, kernel_size()) } impl PhysRange { From abb0f47fde49b35643030f761621f15ecad2cc25 Mon Sep 17 00:00:00 2001 From: Daniel-De-Dev Date: Sun, 20 Sep 2026 01:47:51 +0200 Subject: [PATCH 4/4] refactor: fixed up docs & added assert for linker Made documentation more consistent and reflect the new changes added which i missed earlier. --- kernel/linker.ld.in | 5 +++++ kernel/src/arch/riscv64/boot.rs | 12 ++++++------ kernel/src/main.rs | 10 ++++++---- kernel/src/memory.rs | 7 ++++--- nix/kernel.nix | 6 +++--- nix/qemu.nix | 4 ++-- 6 files changed, 26 insertions(+), 18 deletions(-) diff --git a/kernel/linker.ld.in b/kernel/linker.ld.in index c68d809..c09a15b 100644 --- a/kernel/linker.ld.in +++ b/kernel/linker.ld.in @@ -189,6 +189,11 @@ SECTIONS + (_kernel_virtual_end - KERNEL_VIRTUAL_BASE); } +ASSERT( + _start == KERNEL_PHYSICAL_BASE, + "bootstrap entry must begin at the physical kernel load address" +) + ASSERT( (__boot_page_table_end - __boot_page_table) == PAGE_SIZE, "bootstrap page table must occupy exactly one page" diff --git a/kernel/src/arch/riscv64/boot.rs b/kernel/src/arch/riscv64/boot.rs index 73d3441..f01ac96 100644 --- a/kernel/src/arch/riscv64/boot.rs +++ b/kernel/src/arch/riscv64/boot.rs @@ -14,9 +14,9 @@ //! fits within one physical 1 GiB region and the corresponding higher-half //! virtual region. //! -//! `OpenSBI` supplies the hart ID in `a0` and the physical device-tree address in -//! `a1`. Both registers are preserved until `main` is entered. `a2` contains -//! the kernels physical start address. +//! `OpenSBI` supplies the hart ID in `a0` and the physical device-tree address +//! in `a1`. The physical bootstrap places the kernel's physical start address +//! in `a2`. All three values are preserved until `main` is entered. //! //! The DTB's containing 1 GiB region is identity-mapped so the existing //! physical pointer remains usable during early initialization. A DTB crossing @@ -36,7 +36,7 @@ global_asm!( .equ GIGAPAGE_SHIFT, 30 .equ PTE_BYTE_SHIFT, 3 .equ VPN2_MASK, 0x1ff - .equ PTE_VRWXAD, 0xcf + .equ PTE_VRWXAD, 0xcf .equ SATP_MODE_SV39, 8 .equ SATP_MODE_SHIFT, 60 @@ -58,7 +58,7 @@ global_asm!( .global _start _start: - /* Preserve the physical kernel base, as it will become innaccessible later */ + /* Carry the physical kernel base into higher-half Rust through a2. */ lla a2, _kernel_physical_start lla t0, __boot_page_table @@ -199,7 +199,7 @@ __high_half_start: j .Lbss_loop .Lbss_done: - /* a0 and a1 still carry OpenSBI's hart ID and physical DTB address. */ + /* a0-a2 carry the hart ID, physical DTB address, and physical kernel base. */ tail main "# ); diff --git a/kernel/src/main.rs b/kernel/src/main.rs index b1f94c9..596110f 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -20,9 +20,10 @@ use memory::{BootFrameAllocator, PhysAddr, PhysRange}; /// Runs the kernel after architecture-specific initialization. /// -/// `hart_id` identifies the RISC-V hart on which the kernel was entered, -/// while `dtb` is the physical address of the device tree supplied by the -/// previous firmware stage. +/// `hart_id` identifies the RISC-V hart on which the kernel was entered. +/// `dtb` is the physical address of the device tree supplied by the previous +/// firmware stage. `kernel_phys_start` is the physical start of the kernel +/// image preserved by the bootstrap before entering the higher half. /// /// This function does not return. #[unsafe(no_mangle)] @@ -45,7 +46,8 @@ extern "C" fn main(hart_id: usize, dtb: usize, kernel_phys_start: usize) -> ! { // SAFETY: // The bootstrap page table identity-maps the physical 1 GiB region containing // the firmware-provided DTB, so its physical address is temporarily also a - // valid virtual address. The DTB remains mapped while it is parsed here. + // valid virtual address. Identity mapping must outlive all accesses through + // `fdt` let fdt = match unsafe { Fdt::from_ptr(dtb_ptr) } { Ok(fdt) => fdt, Err(error) => { diff --git a/kernel/src/memory.rs b/kernel/src/memory.rs index e1f5cd8..3076c12 100644 --- a/kernel/src/memory.rs +++ b/kernel/src/memory.rs @@ -39,15 +39,16 @@ unsafe extern "C" { /// Returns the kernel's complete boot-time memory footprint in bytes. /// -/// The size includes the bootstrap region and all higher-half kernel sections. +/// The span includes the physical bootstrap reservation, higher-half kernel +/// sections, `.bss`, boot stack, and linker-introduced alignment. /// /// # Panics /// -/// Panics if the linker-provided virtual kernel boundaries are reversed. +/// Panics if the linker does not provide a non-empty virtual kernel range. #[must_use] #[expect( clippy::expect_used, - reason = "the linker guarantees _kernel_virtual_end is after _kernel_virtual_start" + reason = "the linker guarantees a non-empty virtual kernel range" )] fn kernel_size() -> usize { let start = core::ptr::addr_of!(_kernel_virtual_start).addr(); diff --git a/nix/kernel.nix b/nix/kernel.nix index 6de3f79..1a8eff2 100644 --- a/nix/kernel.nix +++ b/nix/kernel.nix @@ -1,9 +1,9 @@ /* Build the RISC-V kernel that runs as OpenSBI's S-mode next stage. - The kernel is linked for a board-specific load address and memory - region. The linker script ensures that the kernel's runtime memory - footprint fits within that region. + The kernel is physically loaded at a board-specific address while its normal + kernel sections are linked into the higher-half virtual address space. The + linker preserves corresponding physical load addresses for the flat image. The linked ELF is retained for debugging, and llvm-objcopy also produces a flat binary for loading into memory. Platform-specific boot code places diff --git a/nix/qemu.nix b/nix/qemu.nix index e645941..e9fc51b 100644 --- a/nix/qemu.nix +++ b/nix/qemu.nix @@ -30,8 +30,8 @@ Run the kernel on QEMU's RISC-V `virt` machine. OpenSBI FW_JUMP is installed as the machine firmware with `-bios`. - QEMU's generic loader places the raw kernel image at the address - for which it was linked. The loader does not change the CPU entry + QEMU's generic loader places the raw kernel image at its configured + physical load address. The loader does not change the CPU entry point; execution begins in OpenSBI, which later jumps to the kernel.