From d14465458d2f0d15a5743d4d15641f2a0075640b Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Mon, 5 Oct 2026 18:12:11 +0200 Subject: [PATCH] mm: add bitmap PMM over E820 usable frames --- include/pmm.h | 53 +++++++++++++ kernel/mm/pmm.c | 197 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 include/pmm.h create mode 100644 kernel/mm/pmm.c diff --git a/include/pmm.h b/include/pmm.h new file mode 100644 index 0000000..2adebef --- /dev/null +++ b/include/pmm.h @@ -0,0 +1,53 @@ +/* Nadir physical memory manager + * + * Runs in 64-bit long mode at Ring 0 (CPL 0) under the stage-2 identity + * map; physical addresses are used as-is. Called once from kmain with + * interrupts disabled (IF=0), before sti, so no IRQ handler can allocate + * concurrently. No faults are possible beyond a programming bug; with no + * IDT yet at the call point a wild access would triple-fault. + */ + +#ifndef NADIR_PMM_H +#define NADIR_PMM_H + +#include + +#include "e820.h" + +/* 4KB frames over the identity-mapped first 1GB: hard capacity limit of + * this allocator (256K frames, 32KB bitmap in .bss). Regions above 1GB + * reported by E820 are ignored. */ +#define PMM_FRAME_SIZE 4096U +#define PMM_MAX_BYTES 0x40000000ULL +#define PMM_MAX_FRAMES (PMM_MAX_BYTES / PMM_FRAME_SIZE) + +/* Memory-map globals: filled by the stage-2 entry (copy from the + * stage-1 staging buffer) before kmain runs. pmm_init reads them. */ +extern uint32_t memmap_count; +extern struct e820_entry memmap_entries[E820_MAX]; + +/* Build the bitmap from the E820 map: type-1 (usable) frames start free, + * everything else starts reserved, then the kernel image, page tables, + * staging buffer, low 1MB, stack and VGA regions are reserved. + * Preconditions: count <= E820_MAX, entries points at count valid + * entries, called once with IF=0 before any alloc/free. + * Failure modes: none — an empty/corrupt map just leaves 0 free frames. */ +void pmm_init(uint32_t count, const struct e820_entry *entries); + +/* Hand out one 4KB frame: returns its physical base address, or 0 when + * out of memory (frame 0 is always reserved, so 0 is never valid). + * Preconditions: pmm_init ran. Failure modes: returns 0 on OOM. */ +uint64_t pmm_alloc_frame(void); + +/* Return a frame from pmm_alloc_frame to the free pool. Silently ignores + * unaligned, out-of-range, or already-free addresses. + * Preconditions: pmm_init ran. Failure modes: none (no fault, no panic). */ +void pmm_free_frame(uint64_t addr); + +/* Live counters for stats printouts. + * Preconditions: pmm_init ran (0 before that). Failure modes: none. */ +uint64_t pmm_total_count(void); +uint64_t pmm_free_count(void); +uint64_t pmm_usable_count(void); + +#endif /* NADIR_PMM_H */ diff --git a/kernel/mm/pmm.c b/kernel/mm/pmm.c new file mode 100644 index 0000000..565e3de --- /dev/null +++ b/kernel/mm/pmm.c @@ -0,0 +1,197 @@ +/* Nadir physical memory manager (PMM-only) + * + * very address it touches arrives as a plain + * physical range (E820 entries from the caller, reservation bounds from + * the linker/boot chain). Runs in 64-bit long mode at Ring 0 (CPL 0) + * under the stage-2 identity map, so physical addresses are dereferenced + * as-is. pmm_init runs once with IF=0 before sti; alloc/free never block + * and never touch hardware, so they are safe under any IF state. + * Fault behavior: no page fault is possible inside the first 1GB + * identity window; a caller passing a wild E820 pointer faults before we + * run, not in here. Bitmap: bit set = used/reserved, bit clear = free. + */ + +#include + +#include "e820.h" +#include "pmm.h" + +/* Filled by the stage-2 entry (copy from the stage-1 staging buffer at + * 0x5000/0x5020) after BSS zeroing, before kmain runs. */ +uint32_t memmap_count; +struct e820_entry memmap_entries[E820_MAX]; + +/* Kernel image bounds from the linker script (linked at 0x10000). */ +extern char __kernel_start[]; +extern char __kernel_end[]; + +/* One bit per 4KB frame over the first 1GB: 256K frames = 32KB in .bss. */ +static uint8_t bitmap[PMM_MAX_FRAMES / 8U]; + +static uint64_t g_usable; +static uint64_t g_free; + +/* WHY bit ops instead of a byte-per-frame array: 32KB of .bss keeps the + * kernel image small (sectors are loaded from floppy LBA 1..N, so every + * KB costs boot time); the shift/mask cost is negligible next to a VGA + * print. */ +static void set_bit(uint64_t frame) +{ + bitmap[frame / 8U] |= (uint8_t)(1U << (frame % 8U)); +} + +static void clear_bit(uint64_t frame) +{ + bitmap[frame / 8U] &= (uint8_t)~(1U << (frame % 8U)); +} + +static int test_bit(uint64_t frame) +{ + return (bitmap[frame / 8U] & (uint8_t)(1U << (frame % 8U))) != 0; +} + +/* Mark every frame in [base, base+len) reserved. Out-of-window and + * already-reserved frames are skipped, so overlapping/low regions are + * safe to reserve twice. */ +static void reserve_range(uint64_t base, uint64_t len) +{ + uint64_t end; + uint64_t frame; + + if (len == 0U) { + return; + } + end = base + len; + if (end < base) { + end = UINT64_MAX; + } + if (base >= PMM_MAX_BYTES) { + return; + } + if (end > PMM_MAX_BYTES) { + end = PMM_MAX_BYTES; + } + for (frame = base / PMM_FRAME_SIZE; frame < end / PMM_FRAME_SIZE; frame++) { + if (test_bit(frame) == 0) { + set_bit(frame); + g_free--; + } + } +} + +void pmm_init(uint32_t count, const struct e820_entry *entries) +{ + uint64_t i; + uint64_t frame; + uint64_t start; + uint64_t end; + + /* Start fully reserved; usable E820 frames are cleared below. This + * way unknown memory can never be handed out by accident. */ + for (i = 0; i < sizeof(bitmap); i++) { + bitmap[i] = 0xFFU; + } + g_usable = 0; + g_free = 0; + + if (entries == 0) { + count = 0; + } + if (count > E820_MAX) { + count = E820_MAX; + } + for (i = 0; i < count; i++) { + if (entries[i].type != E820_USABLE) { + continue; + } + if (entries[i].length == 0U) { + continue; + } + start = entries[i].base; + end = start + entries[i].length; + if (end < start) { + end = UINT64_MAX; + } + if (start >= PMM_MAX_BYTES) { + continue; /* above the identity-mapped window: ignored */ + } + if (end > PMM_MAX_BYTES) { + end = PMM_MAX_BYTES; + } + /* Align the range to whole frames */ + start = (start + PMM_FRAME_SIZE - 1U) / PMM_FRAME_SIZE; + end = end / PMM_FRAME_SIZE; + for (frame = start; frame < end; frame++) { + if (test_bit(frame) != 0) { + clear_bit(frame); + g_usable++; + g_free++; + } + } + } + + /* Second pass: carve out every non-usable E820 range. WHY a separate + * pass instead of skipping overlaps up front: BIOS maps routinely + * stack a reserved entry (e.g. ACPI data) inside a larger usable one, + * and reserve_range is idempotent, so usable-then-reserved converges + * on the right bitmap whatever order the firmware lists. */ + for (i = 0; i < count; i++) { + if (entries[i].type != E820_USABLE) { + reserve_range(entries[i].base, entries[i].length); + } + } + + reserve_range(0x00000000ULL, 0x00100000ULL); + reserve_range((uint64_t)(uintptr_t)__kernel_start, + (uint64_t)(uintptr_t)__kernel_end - (uint64_t)(uintptr_t)__kernel_start); + reserve_range(0x00070000ULL, 0x00004000ULL); + reserve_range(0x00005000ULL, 0x00000620ULL); + reserve_range(0x0008C000ULL, 0x00004000ULL); + reserve_range(0x000B8000ULL, 0x00001000ULL); +} + +uint64_t pmm_alloc_frame(void) +{ + uint64_t frame; + + for (frame = 0; frame < PMM_MAX_FRAMES; frame++) { + if (test_bit(frame) == 0) { + set_bit(frame); + g_free--; + return frame * PMM_FRAME_SIZE; + } + } + return 0; /* frame 0 is always reserved: 0 unambiguously means OOM */ +} + +void pmm_free_frame(uint64_t addr) +{ + uint64_t frame; + + if ((addr % PMM_FRAME_SIZE) != 0U) { + return; + } + if (addr >= PMM_MAX_BYTES) { + return; + } + frame = addr / PMM_FRAME_SIZE; + if (test_bit(frame) != 0) { + clear_bit(frame); + g_free++; + } +} + +uint64_t pmm_total_count(void) +{ + return PMM_MAX_FRAMES; +} + +uint64_t pmm_free_count(void) +{ + return g_free; +} + +uint64_t pmm_usable_count(void) +{ + return g_usable; +}