From 919fba989e1af2eee4f571e3a51dc97d4ae642d6 Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Wed, 7 Oct 2026 16:29:34 +0200 Subject: [PATCH] mm: add page-granular kmalloc/kfree heap over the PMM --- Makefile | 7 +- include/kmalloc.h | 55 +++++++++++++ kernel/core/kmain.c | 44 ++++++++++ kernel/mm/kmalloc.c | 192 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 include/kmalloc.h create mode 100644 kernel/mm/kmalloc.c diff --git a/Makefile b/Makefile index c963336..32e8d7d 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ CFLAGS := -std=c17 -ffreestanding -nostdlib -fno-builtin \ -fno-pic -fno-pie -fno-stack-protector \ -Wall -Wextra -Werror -O2 -Iinclude -OBJS := $(BUILD)/entry.o $(BUILD)/isr.o $(BUILD)/kmain.o $(BUILD)/klog.o $(BUILD)/console.o $(BUILD)/idt.o $(BUILD)/pic.o $(BUILD)/pit.o $(BUILD)/serial.o $(BUILD)/keyboard.o $(BUILD)/pmm.o +OBJS := $(BUILD)/entry.o $(BUILD)/isr.o $(BUILD)/kmain.o $(BUILD)/klog.o $(BUILD)/console.o $(BUILD)/idt.o $(BUILD)/pic.o $(BUILD)/pit.o $(BUILD)/serial.o $(BUILD)/keyboard.o $(BUILD)/pmm.o $(BUILD)/kmalloc.o all: $(IMG) @@ -33,7 +33,7 @@ $(BUILD)/entry.o: $(ARCH_DIR)/entry.asm | $(BUILD) $(BUILD)/isr.o: $(ARCH_DIR)/isr.asm | $(BUILD) $(NASM) -f elf64 $< -o $@ -$(BUILD)/kmain.o: kernel/core/kmain.c include/console.h include/idt.h include/pic.h include/pit.h include/serial.h include/keyboard.h include/pmm.h include/e820.h include/klog.h | $(BUILD) +$(BUILD)/kmain.o: kernel/core/kmain.c include/console.h include/idt.h include/pic.h include/pit.h include/serial.h include/keyboard.h include/pmm.h include/kmalloc.h include/e820.h include/klog.h | $(BUILD) $(CC) $(CFLAGS) -c $< -o $@ $(BUILD)/klog.o: kernel/core/klog.c include/klog.h include/console.h include/pit.h | $(BUILD) @@ -42,6 +42,9 @@ $(BUILD)/klog.o: kernel/core/klog.c include/klog.h include/console.h include/pit $(BUILD)/pmm.o: kernel/mm/pmm.c include/pmm.h include/e820.h | $(BUILD) $(CC) $(CFLAGS) -c $< -o $@ +$(BUILD)/kmalloc.o: kernel/mm/kmalloc.c include/kmalloc.h include/pmm.h | $(BUILD) + $(CC) $(CFLAGS) -c $< -o $@ + $(BUILD)/console.o: $(ARCH_DIR)/console.c include/console.h include/io.h | $(BUILD) $(CC) $(CFLAGS) -c $< -o $@ diff --git a/include/kmalloc.h b/include/kmalloc.h new file mode 100644 index 0000000..0447413 --- /dev/null +++ b/include/kmalloc.h @@ -0,0 +1,55 @@ +/* Nadir kernel heap allocator + * + * A page-granular heap on top of the PMM. Runs in 64-bit long + * mode at Ring 0 (CPL 0) under the stage-2 identity map; every address + * handed out is a physical address used as-is. There is no virtual + * address space yet, so kmalloc returns identity-mapped physical + * addresses. + * + * Design: allocations are rounded up to whole 4KB frames. The first + * bytes of the first frame hold a header (magic + frame count) so + * kfree can validate and release the whole run. Free runs are kept on + * a singly-linked free list; kmalloc takes the first run that fits + * (first-fit) and splits the tail. No coalescing yet — adjacent free + * runs stay separate until a later change merges them. + * + * IF/IRQ safety: not interrupt-safe. Callers must not kmalloc/kfree + * from an IRQ handler; the PIT/keyboard handlers never allocate. + * Single-core kernel, so no locking is needed. + * + * Preconditions: pmm_init ran (kmalloc calls pmm_alloc_frame). + * Failure modes: kmalloc returns 0 (NULL) on out-of-memory or when + * the request rounds up to zero frames; kfree silently ignores NULL, + * bad magic, and out-of-window addresses. + */ + +#ifndef NADIR_KMALLOC_H +#define NADIR_KMALLOC_H + +#include +#include + +/* Allocate `size` bytes (size 0 behaves like size 1). Returns an + * identity-mapped physical address, or NULL when the PMM is out of + * frames. The returned block is NOT zeroed. + * Preconditions: pmm_init ran. Failure modes: returns NULL on OOM. */ +void *kmalloc(size_t size); + +/* Release a block from kmalloc. Silently ignores NULL, pointers with + * a bad header magic, and addresses outside the PMM window. + * Preconditions: pmm_init ran. Failure modes: none (no fault, no + * panic on bad input). */ +void kfree(void *ptr); + +/* Live counters for stats printouts and the boot self-test. + * Preconditions: kmalloc_init ran (0 before that). Failure modes: none. */ +uint64_t kmalloc_free_frames(void); +uint64_t kmalloc_used_frames(void); + +/* One-time heap init: takes the first PMM frames as the free pool. + * Called once from kmain after pmm_init, with IF=0. + * Preconditions: pmm_init ran. Failure modes: none — with zero frames + * handed over, kmalloc always returns NULL. */ +void kmalloc_init(void); + +#endif /* NADIR_KMALLOC_H */ diff --git a/kernel/core/kmain.c b/kernel/core/kmain.c index 19f1e9b..f1d910c 100644 --- a/kernel/core/kmain.c +++ b/kernel/core/kmain.c @@ -21,6 +21,7 @@ #include "idt.h" #include "keyboard.h" #include "klog.h" +#include "kmalloc.h" #include "pic.h" #include "pit.h" #include "pmm.h" @@ -113,6 +114,49 @@ void kmain(void) klog_dec(pmm_usable_count() - pmm_free_count()); klog_end(); + /* Heap self-test. init, then alloc/write/free churn to prove the + * free-list split and re-insert paths work before interrupts go + * live. Runs with IF=0, polling console only. */ + kmalloc_init(); + { + uint8_t *a = kmalloc(100); + uint8_t *b = kmalloc(5000); /* spans 2 frames */ + uint8_t *c = kmalloc(1); + int ok = 1; + + if (a == 0 || b == 0 || c == 0) { + ok = 0; + } else { + uint64_t i; + + for (i = 0; i < 100U; i++) { + a[i] = (uint8_t)i; + } + for (i = 0; i < 100U; i++) { + if (a[i] != (uint8_t)i) { + ok = 0; + } + } + b[0] = 0xAAU; + b[4999] = 0x55U; + if (b[0] != 0xAAU || b[4999] != 0x55U) { + ok = 0; + } + } + kfree(a); + kfree(b); + kfree(c); + kfree(0); /* NULL must be a no-op */ + klog_begin("kmalloc"); + klog_str("self-test "); + klog_str(ok != 0 ? "ok" : "FAILED"); + klog_str(", free frames "); + klog_dec(kmalloc_free_frames()); + klog_str(", used frames "); + klog_dec(kmalloc_used_frames()); + klog_end(); + } + /* Explicit opt-in for the two live lines; idempotent even though * keyboard_init already unmasked IRQ1. */ pic_unmask(0); diff --git a/kernel/mm/kmalloc.c b/kernel/mm/kmalloc.c new file mode 100644 index 0000000..e5aa06c --- /dev/null +++ b/kernel/mm/kmalloc.c @@ -0,0 +1,192 @@ +/* Nadir kernel heap allocator (see kmalloc.h) + * + * Page-granular first-fit heap over pmm_alloc_frame. WHY page-granular + * for now: the PMM only hands out 4KB frames, and a slab/buddy layer + * on top is a later step; this keeps the first heap small enough to + * review line by line while giving the kernel a real kmalloc/kfree to + * build on. + * + * Layout of an allocated run: + * + * frame 0: [ header (16 bytes) | payload ... ] + * frames 1..n-1: payload continuation + * + * The header records the run length in frames so kfree can validate + * and release the whole run, plus a magic so a wild pointer is caught + * instead of corrupting the free list. Free runs carry a next pointer + * and a frame count in their first 16 bytes. + * + * Ownership: kmalloc_init pulls KMALLOC_INIT_FRAMES frames from the + * PMM once; from then on the heap owns those frames exclusively and + * the PMM never sees them again. kfree returns runs to the heap free + * list only. WHY: the PMM bitmap is the + * authoritative owner of every frame, and a frame that is both on the + * heap free list and free in the PMM would be handed out twice. + * + * WHY first-fit with split (no coalescing): simplest correct policy. + * Fragmentation is bounded because every allocation is a whole number + * of frames and the boot self-test exercises alloc/free churn. + */ + +#include +#include + +#include "kmalloc.h" +#include "pmm.h" + +/* Magic stamped into allocated headers: catches kfree of a wild or + * double-freed pointer before the free list is corrupted. */ +#define KMALLOC_MAGIC 0x4E414449524D4148ULL /* "NADIRHAM" */ + +/* Frames the heap starts with: 64 frames = 256KB of kernel heap, + * pulled from the PMM at init. Enough for early boot structures. */ +#define KMALLOC_INIT_FRAMES 64U + +struct kmalloc_header { + uint64_t magic; + uint64_t frames; /* total frames in this run, header included */ +}; + +/* A free run: next pointer + frame count in the first 16 bytes. */ +struct kmalloc_free { + struct kmalloc_free *next; + uint64_t frames; +}; + +/* Free list head: runs of whole frames available for allocation. */ +static struct kmalloc_free *g_free_list; + +static uint64_t g_free_frames; +static uint64_t g_used_frames; + +/* Round a byte count up to whole frames, header included. A request + * of `size` bytes needs ceil((size + header) / FRAME) frames. */ +static uint64_t frames_for(size_t size) +{ + uint64_t bytes; + + if (size == 0U) { + size = 1U; + } + bytes = (uint64_t)size + sizeof(struct kmalloc_header); + return (bytes + PMM_FRAME_SIZE - 1U) / PMM_FRAME_SIZE; +} + +void kmalloc_init(void) +{ + uint64_t addr = 0U; + uint64_t i; + struct kmalloc_free *run; + + g_free_list = NULL; + g_free_frames = 0; + g_used_frames = 0; + + /* Pull the init frames from the PMM. pmm_alloc_frame scans from + * frame 0 upward, so consecutive calls return consecutive frames + * while the low memory is free. */ + for (i = 0; i < KMALLOC_INIT_FRAMES; i++) { + uint64_t next = pmm_alloc_frame(); + + if (next == 0U) { + break; /* PMM out of frames */ + } + if (addr != 0U && next != addr + i * PMM_FRAME_SIZE) { + break; /* non-contiguous */ + } + if (addr == 0U) { + addr = next; + } + } + + if (addr != 0U) { + run = (struct kmalloc_free *)(uintptr_t)addr; + run->next = NULL; + run->frames = i; + g_free_list = run; + g_free_frames = i; + } +} + +void *kmalloc(size_t size) +{ + uint64_t need = frames_for(size); + struct kmalloc_free **link = &g_free_list; + struct kmalloc_free *run; + + /* walk the free list for the first run that fits. */ + while (*link != NULL) { + run = *link; + if (run->frames >= need) { + struct kmalloc_header *hdr; + uint64_t run_frames = run->frames; + + if (run->frames > need) { + /* Split: the tail replaces this run in the free list, + * and the head `need` frames become the allocation. + * WHY contiguous: a run is always a contiguous frame + * range, so the split point is exact. WHY replace (not + * keep) the head in the list: the allocation header + * overwrites the first 16 bytes of the head, which + * would clobber the free node's next/frames fields. */ + struct kmalloc_free *tail; + + tail = (struct kmalloc_free *) + ((uintptr_t)run + need * PMM_FRAME_SIZE); + tail->next = run->next; + tail->frames = run->frames - need; + *link = tail; /* tail takes this run's list slot */ + run_frames = need; + } else { + *link = run->next; + } + g_free_frames -= run_frames; + hdr = (struct kmalloc_header *)run; + hdr->magic = KMALLOC_MAGIC; + hdr->frames = run_frames; + g_used_frames += run_frames; + return (void *)((uintptr_t)hdr + sizeof(struct kmalloc_header)); + } + link = &run->next; + } + return NULL; /* no run big enough: OOM */ +} + +void kfree(void *ptr) +{ + struct kmalloc_header *hdr; + struct kmalloc_free *node; + + if (ptr == NULL) { + return; + } + hdr = (struct kmalloc_header *) + ((uintptr_t)ptr - sizeof(struct kmalloc_header)); + if (hdr->magic != KMALLOC_MAGIC) { + return; /* ignore if wild or double-freed pointer */ + } + if (hdr->frames == 0U) { + return; + } + /* Invalidate the magic first so a double free is caught. */ + hdr->magic = 0U; + + /* Return the run to the free list at the head. WHY head insert: + * O(1), and first-fit finds it on the next kmalloc. */ + node = (struct kmalloc_free *)hdr; + node->next = g_free_list; + node->frames = hdr->frames; + g_free_list = node; + g_free_frames += hdr->frames; + g_used_frames -= hdr->frames; +} + +uint64_t kmalloc_free_frames(void) +{ + return g_free_frames; +} + +uint64_t kmalloc_used_frames(void) +{ + return g_used_frames; +}