Paging: mapped kernel, fb, early-mem, HHDM

This commit is contained in:
2026-01-02 13:40:44 +01:00
parent bb5fb9db33
commit c065df6ff3
4 changed files with 188 additions and 15 deletions

View File

@@ -1,13 +1,156 @@
#include "paging.h"
#include "pmm.h"
#include <kernel.h>
#include <stddef.h>
#include <limine.h>
void paging_init()
/*
Paging on x86 uses four different page table levels:
cr3 register contains the phys address for the PML4 (root directory)
Each directory/table is made of 512 entries, each one uint64_t
Each of these entries have special bits (PRESENT/WRITEABLE/USER/etc.)
that dictates their attributes. Also these bits fall back on children tables.
If we use 1GB huge pages: PML4 -> PDPT -> 1gb pages
2MB huge pages: PML4 -> PDPT -> PD -> 2mb pages
4KB (regular size): PML4 -> PDPT -> PD -> PT -> 4kb pages
*/
static inline void load_cr3(uint64_t value) {
asm volatile ("mov %0, %%cr3" :: "r"(value) : "memory");
}
// To flush TLB
static inline void invlpg(void *addr)
{
uint64_t cr3;
asm volatile ("mov %%cr3, %0" : "=r"(cr3));
asm volatile("invlpg (%0)" :: "r"(addr) : "memory");
}
// Root directory address (cr3 without 12 less significant bits)
uint64_t* pml4 = PHYS_TO_VIRT(cr3 & ~0xFFF);
// Allocates a 512-entry 64bit page table/directory/whatever (zeroed)
static uint64_t* alloc_page_table()
{
uint64_t* virt = (uint64_t*)PHYS_TO_VIRT(pmm_alloc());
DEBUG("pml4=0x%p", pml4);
for (size_t i=0; i<512; i++)
{
virt[i] = 0;
}
return virt;
}
__attribute__((aligned(4096)))
static uint64_t *kernel_pml4;
void map_page(uint64_t virt, uint64_t phys, uint64_t flags)
{
virt = PAGE_ALIGN_DOWN(virt);
phys = PAGE_ALIGN_DOWN(phys);
// Translate the virt address into page table indexes
uint64_t pml4_i = PML4_INDEX(virt);
uint64_t pdpt_i = PDPT_INDEX(virt);
uint64_t pd_i = PD_INDEX(virt);
uint64_t pt_i = PT_INDEX(virt);
uint64_t *pdpt, *pd, *pt;
// PML4
// If the entry at index is not present, allocate enough space for it
// then populate the entry with correct addr + flags
if (!(kernel_pml4[pml4_i] & PTE_PRESENT))
{
pdpt = alloc_page_table();
kernel_pml4[pml4_i] = VIRT_TO_PHYS(pdpt) | PTE_PRESENT | PTE_WRITABLE;
}
else {
pdpt = (uint64_t *)PHYS_TO_VIRT(kernel_pml4[pml4_i] & ~0xFFFULL);
}
// PDPT: same here
if (!(pdpt[pdpt_i] & PTE_PRESENT))
{
pd = alloc_page_table();
pdpt[pdpt_i] = VIRT_TO_PHYS(pd) | PTE_PRESENT | PTE_WRITABLE;
}
else {
pd = (uint64_t *)PHYS_TO_VIRT(pdpt[pdpt_i] & ~0xFFFULL);
}
// PD: and here
if (!(pd[pd_i] & PTE_PRESENT))
{
pt = alloc_page_table();
pd[pd_i] = VIRT_TO_PHYS(pt) | PTE_PRESENT | PTE_WRITABLE;
}
else {
pt = (uint64_t *)PHYS_TO_VIRT(pd[pd_i] & ~0xFFFULL);
}
// PT: finally, populate the page table entry
pt[pt_i] = phys | flags | PTE_PRESENT;
// Flush TLB (apply changes)
invlpg((void *)virt);
}
void paging_init(struct limine_kernel_address_response* kaddr, struct limine_framebuffer* fb)
{
// We should map the kernel, GDT, IDT, stack, framebuffer.
// Optionally we could map ACPI tables (we can find them in the Limine memmap)
uint64_t kernel_phys_base = kaddr->physical_base;
uint64_t kernel_virt_base = kaddr->virtual_base;
kernel_pml4 = alloc_page_table();
// for debug
uint64_t page_count = 0;
// First 16 MB identity-mapped (phys = virt)
// This is because there might be some leftover stuff in the lower phys addresses
// from boot/bios/acpi/...
for (uint64_t i=0; i<0x1000000; i += PAGE_SIZE)
{
map_page(i, i, PTE_WRITABLE);
page_count++;
}
DEBUG("Mapped %u pages for the identity-mapping of the first 16 MB", page_count); page_count = 0;
// HHDM map first 1 GB using given offset
for (uint64_t i=0; i<0x40000000; i += PAGE_SIZE)
{
map_page(i+hhdm_off, i, PTE_WRITABLE);
page_count++;
}
DEBUG("Mapped %u pages for first 1GB (HHDM)", page_count); page_count = 0;
// Map the kernel (according to virt/phys_base given by Limine)
// SOME DAY when we want a safer kernel we should map .text as Read/Exec
// .rodata as Read and .data as Read/Write
// For now who gives a shit, let's RWX all kernel
for (uint64_t i = 0; i < KERNEL_SIZE; i += PAGE_SIZE)
{
map_page(kernel_virt_base+i, kernel_phys_base+i, PTE_WRITABLE);
page_count++;
}
DEBUG("Mapped %u pages for kernel", page_count); page_count = 0;
// Get the framebuffer phys/virt address, and size
uint64_t fb_virt = (uint64_t)fb->address;
uint64_t fb_phys = VIRT_TO_PHYS(fb_virt);
uint64_t fb_size = fb->pitch * fb->height;
uint64_t fb_pages = (fb_size + PAGE_SIZE-1)/PAGE_SIZE;
// Map the framebuffer (with cache-disable & write-through)
for (uint64_t i=0; i<fb_pages; i++)
{
map_page(fb_virt+i*PAGE_SIZE, fb_phys+i*PAGE_SIZE, PTE_WRITABLE | PTE_PCD | PTE_PWT);
page_count++;
}
DEBUG("Mapped %u pages for framebuffer", page_count);
// Finally, we load the physical address of our PML4 (root table) into cr3
load_cr3(VIRT_TO_PHYS(kernel_pml4));
DEBUG("cr3 loaded, we're still alive");
}

View File

@@ -5,8 +5,9 @@
#define BITS_PER_ROW 64
#include <stdint.h>
#include <limine.h>
void paging_init();
void paging_init(struct limine_kernel_address_response* kaddr, struct limine_framebuffer* fb);
extern uint64_t hhdm_off;
@@ -16,14 +17,27 @@ extern uint64_t hhdm_off;
// Stole it
#define ALIGN_UP(x, align) (((x) + ((align) - 1)) & ~((align) - 1))
#define ALIGN_DOWN(x, align) ((x) & ~((align) - 1))
#define PAGE_ALIGN_DOWN(x) ((x) & ~0xFFFULL)
#define PML4_INDEX(x) (((x) >> 39) & 0x1FF)
#define PDPT_INDEX(x) (((x) >> 30) & 0x1FF)
#define PD_INDEX(x) (((x) >> 21) & 0x1FF)
#define PT_INDEX(x) (((x) >> 12) & 0x1FF)
// Page entry special bits
// Bits set on a parent (directory, table) fall back to their children
#define PTE_PRESENT (1ULL << 0)
#define PTE_WRITE (1ULL << 1)
#define PTE_USER (1ULL << 2)
#define PTE_HUGE (1ULL << 7)
#define PTE_NOEXEC (1ULL << 63)
#define PTE_PRESENT (1ULL << 0)
#define PTE_WRITABLE (1ULL << 1)
#define PTE_USER (1ULL << 2)
#define PTE_PWT (1ULL << 3)
#define PTE_PCD (1ULL << 4)
#define PTE_HUGE (1ULL << 7)
#define PTE_NOEXEC (1ULL << 63)
// Specified in linker.ld
#define KERNEL_BASE 0xFFFFFFFF80000000ULL
// 2 MB should be enough (as of now, the whole kernel ELF is around 75kb)
#define KERNEL_SIZE 0x200000
#endif

View File

@@ -4,6 +4,8 @@
#include <limine.h>
void pmm_init(struct limine_memmap_response* memmap, struct limine_hhdm_response* hhdm);
void pmm_free(uintptr_t addr);
uintptr_t pmm_alloc();
// Might be upgraded to a freelist later.
// For now, we can take the biggest usable region and we will be fine.