mm: add bitmap PMM over E820 usable frames

This commit is contained in:
Pierre-Emmanuel Gob. 2026-10-05 18:12:11 +02:00
parent 36fa8d7655
commit d14465458d
2 changed files with 250 additions and 0 deletions

197
kernel/mm/pmm.c Normal file
View file

@ -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 <stdint.h>
#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;
}