Phase 02
Memory Basics
Move from a static early kernel into a kernel that can allocate memory safely and reason about physical and virtual address space.
Learning goal
Understand the memory map provided by the bootloader.
Category
foundations
Components touched
Builds on
Unlocked by this phase
Why this phase matters
Move from a static early kernel into a kernel that can allocate memory safely and reason about physical and virtual address space.
How it works in m3OS
After Phase 1, the kernel could boot and print diagnostics, but it still had no way to manage RAM dynamically. Phase 2 adds the minimum memory machinery needed to build future subsystems:
How it builds on earlier phases
This phase builds on Boot Foundation and turns that foundation into Memory Basics.
Implementation outline
- Store a
'staticreference to the bootloader memory map (the regions slice is valid for the kernel’s lifetime) and parse it into typed kernel structures. - Implement a simple frame allocator with conservative rules.
- Expose page-table operations through safe wrappers around
x86_64. - Map a small heap and initialize the allocator.
- Add simple allocation tests and logging for confidence.
Task progress snapshot
16 of 16 tracked tasks are complete.
Documentation targets
- Documentation deliverables have not been listed yet.
Deferred follow-on work
- reclaiming freed physical frames
- demand paging
- sophisticated virtual memory policies
Source materials
Important code
Code spotlight cards
Code spotlight
Copy the bootloader memory map into kernel-owned structures
Copy the bootloader memory map into kernel-owned structures. Why it matters: The bootloader-provided memory map must be copied into stable kernel storage before BootInfo references become invalid. Focus on `init` and `BootInfo` in `kernel/src/mm/memory_map.rs` while you trace the snippet.
pub fn init(regions: &'static [MemoryRegion]) {
assert!(
!MEMORY_MAP_INIT.swap(true, Ordering::AcqRel),
"memory_map::init called more than once"
);
MEMORY_REGIONS.call_once(|| regions);
let total = regions.len();
let mut usable = 0usize;
for region in regions {
let size_kb = (region.end - region.start) / 1024;
log::debug!(
"[mm] region: {:?} start={:#x} end={:#x} size={}KB",
region.kind,
region.start,
region.end,
size_kb
);
if region.kind == MemoryRegionKind::Usable {
usable += 1;
}
} Code spotlight
Implement a simple frame allocator
Implement a simple frame allocator. Why it matters: Physical frame allocation is the foundation for all virtual memory operations — page tables, heap, and later userspace mappings all require frames. Focus on `BumpAllocator` and `init` in `kernel/src/mm/frame_allocator.rs` while you trace the snippet.
struct BumpAllocator {
regions: Option<&'static [MemoryRegion]>,
region_index: usize,
next_addr: u64,
frames_allocated: usize,
} Code spotlight
Add safe wrappers around page-table manipulation
Add safe wrappers around page-table manipulation. Why it matters: The kernel needs a safe interface to map virtual addresses to physical frames without writing raw page-table entries everywhere. Focus on `init` and `GlobalFrameAlloc` in `kernel/src/mm/paging.rs` while you trace the snippet.
/// Initialise the kernel's page-table mapper.
///
/// # Safety
/// - `physical_memory_offset` must be the value from `BootInfo` — it must be
/// the virtual base address of the bootloader's physical-memory offset mapping
/// (i.e. a physical address `P` is accessible at `physical_memory_offset + P`).
/// - **Must be called exactly once.** A second call panics to prevent aliased
/// `&'static mut` references to the active L4 table (which would be UB).
pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> {
assert!(
INIT_GUARD.get().is_none(),
"paging::init called more than once"
);
INIT_GUARD.call_once(|| ());
let level_4_table = active_level_4_table(physical_memory_offset);
let mapper = OffsetPageTable::new(level_4_table, physical_memory_offset);
log::info!("[mm] page tables initialized");
mapper
} Key files
- kernel/src/mm/memory_map.rs — The memory-map logic lives in kernel/src/mm/memory_map.rs.
- kernel/src/mm/frame_allocator.rs — The frame allocator lives in kernel/src/mm/frame_allocator.rs.
- kernel/src/mm/paging.rs — The page-table helpers live in kernel/src/mm/paging.rs.
- kernel/src/mm/heap.rs — The heap setup lives in kernel/src/mm/heap.rs.
- kernel/src/mm/debug.rs — The debug helpers live in kernel/src/mm/debug.rs, and the smoke-test allocations
- kernel/src/main.rs — live in kernel/src/main.rs.
How real OSes differ
- Real kernels support far more memory states, NUMA, large pages, reclaim strategies, copy-on-write, and demand paging. For a toy OS, a fixed heap and a simple allocator are better because they teach the mechanics without hiding them behind complex policy.
Success criteria
- `Box`, `Vec`, and small dynamic data structures work after heap init.
- Frame allocation does not reuse already claimed memory.
- Known virtual addresses can be translated for debugging.
- Memory setup is documented clearly enough to revisit later.
Related phases
Keep exploring the roadmap
Phase 01
CompleteBoot Foundation
Boot the kernel through UEFI, print useful output over serial, and halt cleanly. This is the first end-to-end proof that the workspace, toolchain, boot path, and panic path all work together.
Phase 03
CompleteInterrupts
Handle exceptions and hardware interrupts reliably enough to support timing, keyboard input, and basic fault diagnosis.