369 lines
11 KiB
C
369 lines
11 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
|
||
*/
|
||
|
||
/**
|
||
* vmm.c - 4-level paging Virtual Memory Manager (x86_64)
|
||
*
|
||
* Minimal bring-up: identity maps the first 16 MiB, supports single-page
|
||
* map/unmap/query, and switches to the new page table root.
|
||
*/
|
||
|
||
#include <stddef.h>
|
||
#include <stdint.h>
|
||
#include "vmm.h"
|
||
#include "pmm.h"
|
||
#include "console.h"
|
||
#include "uefi.h"
|
||
|
||
#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_ACCESSED (1ull << 5)
|
||
#define PTE_DIRTY (1ull << 6)
|
||
#define PTE_PS (1ull << 7)
|
||
#define PTE_GLOBAL (1ull << 8)
|
||
#define PTE_NX (1ull << 63)
|
||
|
||
#define ADDR_MASK 0x000FFFFFFFFFF000ull
|
||
|
||
static uint64_t vmm_root_pml4_phys = 0;
|
||
static const uint64_t lapic_phys_base = 0xFEE00000ull;
|
||
static const uint64_t hpet_phys_base = 0xFED00000ull;
|
||
|
||
static int is_ram_type(uint32_t type) {
|
||
return type == EfiConventionalMemory ||
|
||
type == EfiLoaderCode ||
|
||
type == EfiLoaderData ||
|
||
type == EfiBootServicesCode ||
|
||
type == EfiBootServicesData ||
|
||
type == EfiRuntimeServicesCode ||
|
||
type == EfiRuntimeServicesData ||
|
||
type == EfiACPIReclaimMemory ||
|
||
type == EfiACPIMemoryNVS;
|
||
}
|
||
|
||
static inline uint64_t *paddr_to_virt(uint64_t paddr) {
|
||
return (uint64_t *)(uintptr_t)paddr;
|
||
}
|
||
|
||
static uint64_t alloc_page_zeroed(void) {
|
||
uint64_t paddr = pmm_alloc_page();
|
||
if (paddr == 0) {
|
||
return 0;
|
||
}
|
||
uint64_t *ptr = paddr_to_virt(paddr);
|
||
for (size_t i = 0; i < (VMM_PAGE_SIZE / sizeof(uint64_t)); ++i) {
|
||
ptr[i] = 0;
|
||
}
|
||
return paddr;
|
||
}
|
||
|
||
static inline uint16_t pml4_index(uint64_t vaddr) { return (uint16_t)((vaddr >> 39) & 0x1FF); }
|
||
static inline uint16_t pdpt_index(uint64_t vaddr) { return (uint16_t)((vaddr >> 30) & 0x1FF); }
|
||
static inline uint16_t pd_index(uint64_t vaddr) { return (uint16_t)((vaddr >> 21) & 0x1FF); }
|
||
static inline uint16_t pt_index(uint64_t vaddr) { return (uint16_t)((vaddr >> 12) & 0x1FF); }
|
||
|
||
static uint64_t *get_or_alloc_table(uint64_t *parent, uint16_t idx) {
|
||
uint64_t entry = parent[idx];
|
||
if (entry & PTE_PRESENT) {
|
||
return paddr_to_virt(entry & ADDR_MASK);
|
||
}
|
||
|
||
uint64_t child_phys = alloc_page_zeroed();
|
||
if (child_phys == 0) {
|
||
return NULL;
|
||
}
|
||
|
||
parent[idx] = child_phys | PTE_PRESENT | PTE_WRITABLE;
|
||
return paddr_to_virt(child_phys);
|
||
}
|
||
|
||
static uint64_t *get_table(uint64_t *parent, uint16_t idx) {
|
||
uint64_t entry = parent[idx];
|
||
if (entry & PTE_PRESENT) {
|
||
return paddr_to_virt(entry & ADDR_MASK);
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
static uint64_t make_pte(uint64_t paddr, uint64_t flags) {
|
||
uint64_t pte = paddr & ADDR_MASK;
|
||
if (flags & VMM_FLAG_PRESENT) pte |= PTE_PRESENT;
|
||
if (flags & VMM_FLAG_WRITABLE) pte |= PTE_WRITABLE;
|
||
if (flags & VMM_FLAG_USER) pte |= PTE_USER;
|
||
if (flags & VMM_FLAG_CACHE_DISABLE) pte |= PTE_PCD;
|
||
if (!(flags & VMM_FLAG_NX)) pte &= ~PTE_NX;
|
||
else pte |= PTE_NX;
|
||
return pte;
|
||
}
|
||
|
||
static inline void invlpg(uint64_t vaddr) {
|
||
#if defined(__x86_64__) || defined(__i386__)
|
||
__asm__ volatile ("invlpg (%0)" :: "r"(vaddr) : "memory");
|
||
#else
|
||
(void)vaddr;
|
||
#endif
|
||
}
|
||
|
||
static inline void load_cr3(uint64_t pml4_phys) {
|
||
#if defined(__x86_64__)
|
||
__asm__ volatile("mov %0, %%cr3" :: "r"(pml4_phys) : "memory");
|
||
#else
|
||
(void)pml4_phys;
|
||
#endif
|
||
}
|
||
|
||
static uint64_t *resolve_pt(uint64_t vaddr, uint16_t *idx_out) {
|
||
if (vmm_root_pml4_phys == 0) {
|
||
return NULL;
|
||
}
|
||
|
||
uint64_t *pml4 = paddr_to_virt(vmm_root_pml4_phys);
|
||
uint64_t *pdpt = get_table(pml4, pml4_index(vaddr));
|
||
if (!pdpt) return NULL;
|
||
|
||
uint64_t *pd = get_table(pdpt, pdpt_index(vaddr));
|
||
if (!pd) return NULL;
|
||
|
||
uint64_t *pt = get_table(pd, pd_index(vaddr));
|
||
if (!pt) return NULL;
|
||
|
||
if (idx_out) {
|
||
*idx_out = pt_index(vaddr);
|
||
}
|
||
|
||
return pt;
|
||
}
|
||
|
||
int vmm_map_page(uint64_t vaddr, uint64_t paddr, uint64_t flags) {
|
||
if (vmm_root_pml4_phys == 0) {
|
||
return -1;
|
||
}
|
||
|
||
uint64_t *pml4 = paddr_to_virt(vmm_root_pml4_phys);
|
||
uint64_t *pdpt = get_or_alloc_table(pml4, pml4_index(vaddr));
|
||
if (!pdpt) return -1;
|
||
|
||
uint64_t *pd = get_or_alloc_table(pdpt, pdpt_index(vaddr));
|
||
if (!pd) return -1;
|
||
|
||
uint64_t *pt = get_or_alloc_table(pd, pd_index(vaddr));
|
||
if (!pt) return -1;
|
||
|
||
uint16_t idx = pt_index(vaddr);
|
||
if (pt[idx] & PTE_PRESENT) {
|
||
uint64_t existing_pa = pt[idx] & ADDR_MASK;
|
||
uint64_t new_pte = make_pte(paddr, flags | VMM_FLAG_PRESENT);
|
||
/* Idempotent mapping: same PA + compatible flags => success */
|
||
if (existing_pa == (paddr & ADDR_MASK)) {
|
||
/* Require WRITABLE compatibility (no silent downgrade/upgrade) */
|
||
int was_writable = (pt[idx] & PTE_WRITABLE) != 0;
|
||
int want_writable = (new_pte & PTE_WRITABLE) != 0;
|
||
if (was_writable == want_writable) {
|
||
return 0;
|
||
}
|
||
}
|
||
console_println("VMM map conflict");
|
||
return -1;
|
||
}
|
||
|
||
pt[idx] = make_pte(paddr, flags | VMM_FLAG_PRESENT);
|
||
invlpg(vaddr);
|
||
return 0;
|
||
}
|
||
|
||
int vmm_unmap_page(uint64_t vaddr) {
|
||
if (vmm_root_pml4_phys == 0) {
|
||
return -1;
|
||
}
|
||
|
||
uint64_t *pml4 = paddr_to_virt(vmm_root_pml4_phys);
|
||
uint64_t *pdpt = get_table(pml4, pml4_index(vaddr));
|
||
if (!pdpt) return -1;
|
||
|
||
uint64_t *pd = get_table(pdpt, pdpt_index(vaddr));
|
||
if (!pd) return -1;
|
||
|
||
uint64_t *pt = get_table(pd, pd_index(vaddr));
|
||
if (!pt) return -1;
|
||
|
||
uint16_t idx = pt_index(vaddr);
|
||
if (!(pt[idx] & PTE_PRESENT)) {
|
||
return -1;
|
||
}
|
||
|
||
pt[idx] = 0;
|
||
invlpg(vaddr);
|
||
return 0;
|
||
}
|
||
|
||
uint64_t vmm_get_paddr(uint64_t vaddr) {
|
||
if (vmm_root_pml4_phys == 0) {
|
||
return 0;
|
||
}
|
||
|
||
uint64_t *pml4 = paddr_to_virt(vmm_root_pml4_phys);
|
||
uint64_t *pdpt = get_table(pml4, pml4_index(vaddr));
|
||
if (!pdpt) return 0;
|
||
|
||
uint64_t *pd = get_table(pdpt, pdpt_index(vaddr));
|
||
if (!pd) return 0;
|
||
|
||
uint64_t *pt = get_table(pd, pd_index(vaddr));
|
||
if (!pt) return 0;
|
||
|
||
uint16_t idx = pt_index(vaddr);
|
||
if (!(pt[idx] & PTE_PRESENT)) {
|
||
return 0;
|
||
}
|
||
|
||
return (pt[idx] & ADDR_MASK) | (vaddr & 0xFFF);
|
||
}
|
||
|
||
int vmm_map_range(uint64_t vaddr, uint64_t paddr, uint64_t size, uint64_t flags) {
|
||
uint64_t pages = (size + VMM_PAGE_SIZE - 1) / VMM_PAGE_SIZE;
|
||
for (uint64_t i = 0; i < pages; ++i) {
|
||
if (vmm_map_page(vaddr + i * VMM_PAGE_SIZE, paddr + i * VMM_PAGE_SIZE, flags) != 0) {
|
||
return -1;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
int vmm_query_page(uint64_t vaddr, vmm_page_info_t *info) {
|
||
if (info) {
|
||
info->present = 0;
|
||
info->writable = 0;
|
||
info->executable = 0;
|
||
}
|
||
|
||
uint16_t idx = 0;
|
||
uint64_t *pt = resolve_pt(vaddr, &idx);
|
||
if (!pt) {
|
||
return 0;
|
||
}
|
||
|
||
uint64_t entry = pt[idx];
|
||
if (!(entry & PTE_PRESENT)) {
|
||
return 0;
|
||
}
|
||
|
||
if (info) {
|
||
info->present = 1;
|
||
info->writable = (entry & PTE_WRITABLE) ? 1 : 0;
|
||
info->executable = (entry & PTE_NX) ? 0 : 1;
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
int vmm_init(BootInfo *boot_info) {
|
||
if (!pmm_is_initialized()) {
|
||
console_println("VMM init failed: PMM not ready");
|
||
return -1;
|
||
}
|
||
|
||
if (!boot_info || !boot_info->memory_map || boot_info->memory_map_descriptor_size == 0) {
|
||
console_println("VMM init failed: missing BootInfo");
|
||
return -1;
|
||
}
|
||
|
||
vmm_root_pml4_phys = alloc_page_zeroed();
|
||
if (vmm_root_pml4_phys == 0) {
|
||
console_println("VMM init failed: no memory for PML4");
|
||
return -1;
|
||
}
|
||
|
||
EFI_MEMORY_DESCRIPTOR *map = boot_info->memory_map;
|
||
UINTN map_size = boot_info->memory_map_size;
|
||
UINTN desc_size = boot_info->memory_map_descriptor_size;
|
||
UINTN entry_count = map_size / desc_size;
|
||
|
||
for (UINTN i = 0; i < entry_count; ++i) {
|
||
EFI_MEMORY_DESCRIPTOR *desc = (EFI_MEMORY_DESCRIPTOR *)((uint8_t *)map + i * desc_size);
|
||
if (!is_ram_type(desc->Type)) {
|
||
continue;
|
||
}
|
||
|
||
uint64_t start = desc->PhysicalStart;
|
||
uint64_t size = desc->NumberOfPages * VMM_PAGE_SIZE;
|
||
|
||
if (vmm_map_range(start, start, size, VMM_FLAG_WRITABLE) != 0) {
|
||
console_println("VMM init failed: mapping RAM region");
|
||
return -1;
|
||
}
|
||
}
|
||
|
||
/* Map LAPIC and HPET identity — cache-disabled (PCD) as required for MMIO */
|
||
if (vmm_map_range(lapic_phys_base, lapic_phys_base, VMM_PAGE_SIZE,
|
||
VMM_FLAG_WRITABLE | VMM_FLAG_CACHE_DISABLE) != 0) {
|
||
console_println("VMM init failed: map LAPIC MMIO");
|
||
return -1;
|
||
}
|
||
if (vmm_map_range(hpet_phys_base, hpet_phys_base, VMM_PAGE_SIZE,
|
||
VMM_FLAG_WRITABLE | VMM_FLAG_CACHE_DISABLE) != 0) {
|
||
console_println("VMM init failed: map HPET MMIO");
|
||
return -1;
|
||
}
|
||
|
||
/* Map GOP framebuffer identity — MMIO not covered by the RAM walk above.
|
||
* QEMU's emulated GOP linear framebuffer sits in a reserved memory range
|
||
* (EfiMemoryMappedIO) that is excluded from the RAM walk. Map it WB here
|
||
* so the VT100 renderer can write to it after load_cr3(). CACHE_DISABLE
|
||
* is omitted deliberately: QEMU's VGA emulation is coherent and UC- mapping
|
||
* causes #GP on some PAT/MTRR combinations. */
|
||
if (boot_info->framebuffer.base != NULL && boot_info->framebuffer.size > 0) {
|
||
uint64_t fb_phys = (uint64_t)(uintptr_t)boot_info->framebuffer.base;
|
||
uint64_t fb_size = (uint64_t)boot_info->framebuffer.size;
|
||
if (vmm_map_range(fb_phys, fb_phys, fb_size, VMM_FLAG_WRITABLE) != 0) {
|
||
console_println("VMM init failed: map GOP framebuffer");
|
||
return -1;
|
||
}
|
||
}
|
||
|
||
load_cr3(vmm_root_pml4_phys);
|
||
console_println("VMM initialized (mapped RAM, CR3 switched)");
|
||
return 0;
|
||
}
|