Skip to content

Memory Overview

Physical Memory Model

The kernel runs at ring 0 with a largely identity-mapped address space (virtual == physical for most addresses). The boot-time page tables are set up by the assembly stage in boot.asm; Rust code then adjusts them as needed during init.

The Multiboot2 memory map tag (type 6) is parsed at boot and reports usable RAM regions. The sum of all usable (type 1) regions is kept as init::boot::total_ram_bytes() and printed by the shell's meminfo command. The kernel does not maintain a physical frame allocator — allocations are handled entirely through pre-reserved static regions described in the linker script and in the page-table pool.


Virtual Address Space Layout

All addresses are 64-bit (x86-64). The boot identity map covers the lower 4 GiB; xHCI can add supervisor-only MMIO mappings above 4 GiB for a 64-bit PCI BAR. The page table is a 4-level (PML4) structure; P2 entries use 2 MiB huge pages except the VGA VRAM window, which uses 4 KiB P1 entries.

Start / symbol End / symbol Size Description
0x000_000 0x0FF_FFF 1 MiB Firmware/legacy ranges, physical VGA memory and the floppy write-DMA bounce buffer at 0x1000–0x11FF
0x100_000 ~ varies Kernel image: .text .rodata .data .bss; placed by linker at 0x100_000
__stack_bottom __stack_top 64 KiB Boot stack (inside kernel image)
__heap_start __heap_end 64 KiB Kernel linked-list heap (legacy)
p4_table / p3_fb_table 8 KiB Static page tables in .boot_reserved (NOLOAD)
0x400_000 0x5FF_FFF 2 MiB User stacks of slots 16–31 (128 KiB each). Mapped user-accessible in every process (boot.asm P2[2]), so the kernel image has to end below 0x400_000: anything of the kernel's here could be read and written by any program. linker.ld asserts it; the exact image end depends on the build
0x600_000 0x7FF_FFF 2 MiB ELF userland load region. Each slot's private 2 MiB physical frame is mapped here by create_user_page_table
0x800_000 0x9FF_FFF 2 MiB User stacks of slots 0–15 (128 KiB each)
0xA00_000 0xA0F_FFF 64 KiB VGA graphics RAM window (mapped on demand by syscall 0x14 / map_vram).
0xA10_000 0xBFF_FFF 1984 KiB Supervisor-only identity mapping initially; not present in a process after its VGA window replaces P2[5]
0xC00_000 0xFFF_FFF 4 MiB Userland heap (shared, uheap)
0x1000_000 0x4FFF_FFF 64 MiB Per-process ELF physical frames: slot 0 → 0x1000_000, slot 1 → 0x1200_000, ... slot 31 → 0x4E00_000. Checked to be usable RAM at boot
Runtime-selected within 0x500_0000–0x53FF_FFF size_of::<xhci::Dma>() xHCI rings, contexts, scratchpads and up to four additional MMIO page tables; 4 KiB-aligned, supervisor-only RAM outside kernel .bss
Runtime-selected within 0x500_0000–0x53FF_FFF size_of::<bcm57765::hw::Dma>() Broadcom descriptor rings and packet buffers; 4 KiB-aligned, avoids the xHCI reservation and boot archive
0xA00_0000 + RAM/8 varies Userland heap extension, mapped USER+WRITE only once the 4 MiB are full (past the tar archive if that is here; see allocators.md)
KSTACK_POOL (.bss) 1 MiB One 32 KiB kernel stack per slot
PAGE_TABLE_POOL (.bss) 512 KiB Static pool for dynamically allocated P4/P3/P2/P1 tables, 4 pages per slot
HD Audio ring, CORB, RIRB (.bss) 131 KiB The PCM ring the controller plays (128 KiB) and the codec command rings (audio/hda.rs)
top of usable RAM below 4 GiB RAM/16 The /mnt/tmp RAM disk: a FAT16 volume formatted at boot, gone at power-off (fs/memdisk). Placed at boot, clear of everything below 0x5000_000 and of the boot archive; 126 MiB at 0x7800_0000 under QEMU's 2 GiB. Kernel-only, like all of the identity map outside the user windows. See RAM Disk

The RAM disk also avoids xHCI and Broadcom DMA reservations. Their placement checks the usable-memory map and excludes the boot archive and boot information. xHCI's BAR can be above 4 GiB; its register window uses supervisor-only, uncached 2 MiB identity mappings installed before process page tables are cloned.

Download memory-map.pdf for four diagrams: the address space at three zoom levels (down to one row per 2 MiB page), the linked kernel image to scale, boot-selected placement with a worked 2 GiB QEMU example, and kernel versus process page tables. The linked addresses identify the graphics-kernel ELF snapshot recorded in the PDF; runtime addresses depend on the machine and boot medium. Each figure says whether it is to scale, and all PDF ranges use an exclusive end address.

Regenerate after rebuilding the graphics kernel, using ReportLab and GNU binutils (readelf and nm):

python3 utils/memmap.py docs/memory/memory-map.pdf \
  --elf target/kernel_graphics/x86_64-r2/release/kernel.elf --as-of 2026-10-12

Use the source-review date for --as-of. The renderer reads fixed pool sizes from Rust source and section/symbol addresses from the chosen ELF; it checks that the selected kernel pools match the source sizes. Boot page-table addresses come from the .bss layout in boot.asm, checked against nm: nm's p4_table is the unused linker-script copy in .boot_reserved, not the table loaded into CR3.

Fixed Pool Capacities

Reserved capacities on a linear MiB scale: ELF frames 64 MiB, user stacks 4 MiB, base user heap 4 MiB, kernel stacks 1 MiB and page tables 0.5 MiB.

Capacity of the fixed pools in the current tree, including all 32 slots. This is not total RAM usage: the kernel image, boot archive, RAM disk and heap extension are excluded. Pools inside kernel .bss also belong to the kernel image.


Paging Model

P4 / P3 / P2 structure

P4[0] points to a P3 with four populated entries covering 0–4 GiB. P3[0] points to the low-memory P2; P3[1..3] point to the high-memory identity-map tables. P2 entries use 2 MiB huge pages (bit 7 set), except the per-process VGA window after map_vram:

P4[0] → P3       (cloned for each user process)
  P3[0] → P2     (low-memory P2, cloned per process)
    P2[0]  → 0x000_000  (2 MiB, kernel image + legacy)
    P2[1]  → 0x200_000  (2 MiB, kernel image, ending below 0x400_000)
    P2[2]  → 0x400_000  (2 MiB, user stacks of slots 16-31)
    P2[3]  → per-slot phys frame  (overridden in user tables)
    P2[4]  → 0x800_000  (2 MiB, user stacks of slots 0-15)
    P2[5]  → VGA P1 table  (64 KiB fine-grained, mapped on demand)
    P2[6]  → 0xC00_000  (2 MiB, userland heap, USER+WRITE)
    P2[7]  → 0xE00_000  (2 MiB, userland heap, USER+WRITE)
    ...

Kernel page table vs. user page table

Slots 4 and 5 see the same virtual ELF image window backed by separate physical frames; stack pages and the user heap are shared user mappings, while kernel mappings are supervisor-only.

Private image mappings versus shared user memory. Each process receives its own stack slice, but the stack pages remain accessible to every user process.

At init_processes, save_kernel_cr3() snapshots the current CR3 as KERNEL_CR3. This is the reference from which all per-process tables are cloned.

create_user_page_table(slot) allocates new P4/P3/P2 tables from PAGE_TABLE_POOL, copies all 512 entries from the kernel tables, then overrides P2[3] to point at the slot's private 2 MiB physical frame (0x1000_000 + slot * 0x200_000). The result is a process that sees:

  • its own ELF code/data at virtual 0x600_000 (private frame)
  • supervisor-only kernel mappings elsewhere (inaccessible from ring 3)
  • the shared userland heap at 0xC00_000–0xFFF_FFF (U/S + R/W, inherited from kernel P2[6/7])

TLB management

CR3 is written on every context switch in the scheduler. Writing CR3 always flushes the entire TLB (Translation Lookaside Buffer), so no explicit invlpg is needed. The flush_tlb() helper reloads CR3 with its own current value for cases where only the active process's mappings changed (e.g. after map_vram).


Page Table Pool

PAGE_TABLE_POOL is a zero-initialised static array in kernel .bss of 4 pages per process slot (512 KiB, 128 × 4 KiB pages, for 32 slots): enough for every slot to hold a user process with its P4, P3, P2 and a map_vram P1 at once. Running out is a panic. alloc_page() serves requests from two sources in order:

  1. Free list (FREE_LIST / FREE_COUNT) — a fixed-size stack of pointers to pages that were previously returned by free_page(). Recycled pages are zeroed before reuse so stale page table entries from the previous owner cannot be followed.
  2. Bump allocator — if the free list is empty, NEXT_FREE_PAGE is advanced and a fresh page is carved from the pool. This pointer only ever increases.

free_page(p) pushes a pointer back onto FREE_LIST. The list is sized to PAGE_TABLE_MEMORY_SIZE / 4096 (128 entries) so it can never overflow as long as only genuinely allocated pages are freed.

free_user_page_table(cr3) is the public reclamation entry point. It walks the P4 → P3 → P2 chain that create_user_page_table built and calls free_page for each of the three tables. It also checks P2[5] for a fine-grained P1 table that map_vram may have installed; if found (indicated by PAGE_PS being clear on a present entry), that page is freed too.

Scheduler::kill() calls free_user_page_table(proc.cr3) before marking the process Dead, then zeros proc.cr3 to prevent a second call from walking freed memory. Kernel processes (cr3 == 0) are skipped automatically.

Each call to create_user_page_table consumes 3 pages (P4 + P3 + P2); those pages are returned to the free list when the process exits. map_vram consumes 1 additional page per process (but only once — repeated calls reuse the existing P1).


ELF Process Memory Layout (per slot)

Physical frame  0x1000_000 + slot*0x200_000   (2 MiB, private)
  ├── ELF PT_LOAD segments written here by load_elf64
  └── zeroed BSS

Virtual 0x600_000 – 0x7FF_FFF   maps to the above private frame
  (P2[3] in the per-process table)

Virtual 0x420_000 – 0xA00_000   user stack top (slot-indexed, see below)
  ├── argv strings and pointers (SysV layout, written by push_user_args)
  └── stack grows downward

User stack tops (by slot)

Each slot has 128 KiB of user stack, sixteen to a 2 MiB page (pages::user_stack_top):

Slots Stack top Page
0–15 0xA00_000 - slot × 0x20_000 (0xA00_000 … 0x820_000) P2[4], 0x800_000–0x9FF_FFF
16–31 0x600_000 - (slot - 16) × 0x20_000 (0x600_000 … 0x420_000) P2[2], 0x400_000–0x5FF_FFF

Both pages are identity-mapped USER+WRITE by boot.asm and are not per-process windows, so the stacks are shared memory like the user heap: the kernel can write a child's argv from any page table, and syscalls accept pointers into either page. Slot 31's stack ends at 0x400_000, where the supervisor-only kernel image begins, so overflowing it faults rather than corrupting anything.

The stack, like the 2 MiB frame, belongs to the scheduler slot the program is about to occupy (scheduler::next_free_slot()). It used to come from a counter that wrapped modulo 10, so the eleventh program started took the first one's frame and stack while it was still running.

With ten slots, slots 8 and 9 had their initial stacks at 0x7F0_000 and 0x7D0_000, inside the 0x600_000–0x7FF_FFF image window. Memento's image reaches 0x7E5_720, so in slot 9 its argv was written over its own .bss. No stack is in the window any more.


C Library Intrinsics (c.rs)

memcpy, memset, memmove, and memcmp are provided as #[no_mangle] extern "C" functions. They are required by the compiler for struct copies, zero-inits, and copy_nonoverlapping fallbacks in no_std + no_libc builds. All are byte-loop implementations with no SIMD.


Related documentation: Kernel Internals, Allocators, Processes.