Initial commit: XOmB exokernel foundation

Core kernel infrastructure:
- Multiboot2 boot with GRUB, long mode setup, higher-half kernel
- Serial port output for debugging
- Unified boot info abstraction for future UEFI support

Memory management:
- Physical frame allocator with bitmap tracking
- Page table manipulation via recursive mapping (PML4[510])
- Support for 4KB, 2MB, and 1GB page mappings
- TLB invalidation and proper NXE support

Build system:
- Cargo-based build with custom x86_64 target
- Makefile for QEMU and Bochs testing
- GRUB ISO generation for multiboot2 boot
This commit is contained in:
wilkie
2025-12-26 14:20:39 -05:00
commit c6fa2895e2
28 changed files with 4146 additions and 0 deletions

21
.cargo/config.toml Normal file
View File

@@ -0,0 +1,21 @@
# XOmB Cargo Configuration
#
# Note: build-std is passed via command line in Makefile to avoid conflicts
# with host-side unit tests.
[build]
target = "x86_64-unknown-uefi"
[target.x86_64-unknown-uefi]
runner = "cargo run --manifest-path runner/Cargo.toml --"
# Linker settings - UEFI uses PE/COFF format, handled automatically
rustflags = [
"-C", "link-arg=/subsystem:efi_application",
]
[alias]
# Convenient aliases
kbuild = "build --release"
ktest = "test --lib --target x86_64-unknown-linux-gnu"
kclippy = "clippy --lib --target x86_64-unknown-linux-gnu"

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# Build artifacts
/target/
# IDE/Editor
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store
# UEFI/Emulator files
/esp/
/esp.img
/OVMF_VARS.fd
# Bochs artifacts
bochs.log
debugger.log
serial.log
bx_enh_dbg.ini
# Lock files
esp.img.lock
# Build artifacts
/iso_staging/
/boot_staging/
*.iso
# Backup files
*.bak
*.orig
# Cargo lock (optional for binaries, usually committed)
# Cargo.lock

180
Cargo.lock generated Normal file
View File

@@ -0,0 +1,180 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bit_field"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "ptr_meta"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcada80daa06c42ed5f48c9a043865edea5dc44cbf9ac009fda3b89526e28607"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bca9224df2e20e7c5548aeb5f110a0f3b77ef05f8585139b7148b59056168ed2"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "ucs2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79298e11f316400c57ec268f3c2c29ac3c4d4777687955cd3d4f3a35ce7eba"
dependencies = [
"bit_field",
]
[[package]]
name = "uefi"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6679b7fc2f6d6d2ea2f67555ef3ed9d71d30c5021faf9193091a5192db7dc468"
dependencies = [
"bitflags",
"cfg-if",
"log",
"ptr_meta",
"ucs2",
"uefi-macros",
"uefi-raw",
"uguid",
]
[[package]]
name = "uefi-macros"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b24e77d3fc1e617051e630f99da24bcae6328abab37b8f9216bb68d06804f9a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "uefi-raw"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f6d465de2c918779dafb769a5a4fe8d6e4fb7cc4cc6cb1a735f2f6ec68beea4"
dependencies = [
"bitflags",
"ptr_meta",
"uguid",
]
[[package]]
name = "uguid"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c8352f8c05e47892e7eaf13b34abd76a7f4aeaf817b716e88789381927f199c"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "xomb"
version = "0.1.0"
dependencies = [
"log",
"spin",
"uefi",
"uefi-raw",
]

53
Cargo.toml Normal file
View File

@@ -0,0 +1,53 @@
[package]
name = "xomb"
version = "0.1.0"
edition = "2024"
authors = ["Your Name <you@example.com>"]
description = "A Rust-based exokernel for x86_64"
[dependencies]
log = "0.4"
spin = "0.9" # Spinlock for no_std
# UEFI dependencies (only for UEFI build)
uefi = { version = "0.33", features = ["alloc", "logger", "global_allocator"], optional = true }
uefi-raw = { version = "0.9", optional = true }
[dev-dependencies]
# For host-side unit testing
[features]
default = []
uefi = ["dep:uefi", "dep:uefi-raw"]
multiboot2 = []
integration-test = [] # Enable for integration tests in emulator
# Kernel library (unit-testable on host)
[lib]
name = "xomb"
path = "src/lib.rs"
test = true
# UEFI binary
[[bin]]
name = "xomb-uefi"
path = "src/main.rs"
required-features = ["uefi"]
test = false
# Multiboot2 binary (for Bochs/GRUB)
[[bin]]
name = "xomb-multiboot2"
path = "src/multiboot2_main.rs"
required-features = ["multiboot2"]
test = false
[profile.dev]
panic = "abort"
opt-level = 1 # Some optimization even in dev (helps with code size)
[profile.release]
panic = "abort"
lto = true
codegen-units = 1
opt-level = "z" # Optimize for size

248
Makefile Normal file
View File

@@ -0,0 +1,248 @@
# XOmB Exokernel Makefile
#
# Targets:
# make build - Build both UEFI and multiboot2 kernels
# make build-uefi - Build UEFI kernel only
# make build-mb2 - Build multiboot2 kernel only (for Bochs)
# make qemu - Run UEFI kernel in QEMU
# make bochs - Run multiboot2 kernel in Bochs
# make qemu-gdb - Run in QEMU with GDB server
# make test - Run unit tests on host
# make clippy - Run clippy lints
# make fmt - Format code
# make clean - Clean build artifacts
# Targets
TARGET_UEFI := x86_64-unknown-uefi
TARGET_MB2 := x86_64-xomb.json
# Build directories
BUILD_DIR_UEFI := target/$(TARGET_UEFI)
BUILD_DIR_MB2 := target/x86_64-xomb
# Output files
DEBUG_EFI := $(BUILD_DIR_UEFI)/debug/xomb-uefi.efi
RELEASE_EFI := $(BUILD_DIR_UEFI)/release/xomb-uefi.efi
DEBUG_ELF := $(BUILD_DIR_MB2)/debug/xomb-multiboot2
RELEASE_ELF := $(BUILD_DIR_MB2)/release/xomb-multiboot2
# OVMF firmware paths (adjust for your system)
# Ubuntu/Debian: /usr/share/OVMF/
# Fedora: /usr/share/edk2/ovmf/
# Arch: /usr/share/ovmf/x64/
OVMF_DIR ?= /usr/share/OVMF
OVMF_CODE ?= $(OVMF_DIR)/OVMF_CODE.fd
OVMF_VARS ?= $(OVMF_DIR)/OVMF_VARS.fd
# ESP (EFI System Partition) image
ESP_DIR := esp
ESP_IMG := esp.img
# QEMU settings
QEMU := qemu-system-x86_64
QEMU_MEMORY := 512M
QEMU_UEFI := \
-machine q35 \
-m $(QEMU_MEMORY) \
-drive if=pflash,format=raw,readonly=on,file=$(OVMF_CODE) \
-drive if=pflash,format=raw,file=OVMF_VARS.fd \
-drive format=raw,file=$(ESP_IMG) \
-serial stdio \
-no-reboot
QEMU_MB2 := \
-machine q35 \
-m $(QEMU_MEMORY) \
-kernel $(DEBUG_ELF) \
-serial stdio \
-no-reboot
# Bochs settings
BOCHS := /home/wilkie/Bochs/usr/bin/bochs
BOCHSRC := bochsrc.txt
# Build-std flags
BUILD_STD := -Z build-std=core,compiler_builtins,alloc -Z build-std-features=compiler-builtins-mem
BUILD_STD_NO_ALLOC := -Z build-std=core,compiler_builtins -Z build-std-features=compiler-builtins-mem
.PHONY: all build build-uefi build-mb2 release qemu bochs qemu-gdb qemu-mb2 test clippy fmt clean setup help
all: build
# Build both kernels
build: build-uefi build-mb2
# Build UEFI kernel
build-uefi:
cargo build --bin xomb-uefi --features uefi --target $(TARGET_UEFI) $(BUILD_STD)
# Build multiboot2 kernel
build-mb2:
cargo build --bin xomb-multiboot2 --features multiboot2 --target $(TARGET_MB2) $(BUILD_STD_NO_ALLOC)
# Release builds
release: release-uefi release-mb2
release-uefi:
cargo build --release --bin xomb-uefi --features uefi --target $(TARGET_UEFI) $(BUILD_STD)
release-mb2:
cargo build --release --bin xomb-multiboot2 --features multiboot2 --target $(TARGET_MB2) $(BUILD_STD_NO_ALLOC)
# Create ESP filesystem image
esp: build-uefi
@echo "Creating EFI System Partition image..."
@mkdir -p $(ESP_DIR)/EFI/BOOT
@cp $(DEBUG_EFI) $(ESP_DIR)/EFI/BOOT/BOOTX64.EFI
@dd if=/dev/zero of=$(ESP_IMG) bs=1M count=64 2>/dev/null
@mkfs.fat -F 32 $(ESP_IMG) >/dev/null
@mcopy -i $(ESP_IMG) -s $(ESP_DIR)/EFI ::
esp-release: release-uefi
@echo "Creating EFI System Partition image (release)..."
@mkdir -p $(ESP_DIR)/EFI/BOOT
@cp $(RELEASE_EFI) $(ESP_DIR)/EFI/BOOT/BOOTX64.EFI
@dd if=/dev/zero of=$(ESP_IMG) bs=1M count=64 2>/dev/null
@mkfs.fat -F 32 $(ESP_IMG) >/dev/null
@mcopy -i $(ESP_IMG) -s $(ESP_DIR)/EFI ::
# Copy OVMF_VARS to local directory (needed for writable vars)
OVMF_VARS.fd:
@if [ -f "$(OVMF_VARS)" ]; then \
cp "$(OVMF_VARS)" OVMF_VARS.fd; \
else \
echo "Error: OVMF_VARS not found at $(OVMF_VARS)"; \
echo "Please install OVMF or set OVMF_DIR"; \
exit 1; \
fi
# Run targets
run: qemu
# QEMU with UEFI
qemu: esp OVMF_VARS.fd
@echo "Starting QEMU (UEFI)..."
$(QEMU) $(QEMU_UEFI)
qemu-release: esp-release OVMF_VARS.fd
@echo "Starting QEMU (UEFI, release build)..."
$(QEMU) $(QEMU_UEFI)
qemu-gdb: esp OVMF_VARS.fd
@echo "Starting QEMU with GDB server on :1234..."
@echo "Connect with: rust-gdb -ex 'target remote :1234'"
$(QEMU) $(QEMU_UEFI) -s -S
# QEMU with multiboot2 (boots from GRUB ISO)
qemu-mb2: $(BOOT_ISO)
@echo "Starting QEMU (multiboot2 via GRUB ISO)..."
$(QEMU) -machine q35 -m $(QEMU_MEMORY) -cdrom $(BOOT_ISO) -serial stdio -no-reboot
# Bootable ISO for Bochs (GRUB + multiboot2 kernel)
BOOT_ISO := xomb.iso
ISO_DIR := iso_staging
$(BOOT_ISO): build-mb2
@echo "Creating GRUB bootable ISO..."
@mkdir -p $(ISO_DIR)/boot/grub
@cp $(DEBUG_ELF) $(ISO_DIR)/boot/xomb-multiboot2
@cp boot/grub/grub.cfg $(ISO_DIR)/boot/grub/
@grub-mkrescue -o $(BOOT_ISO) $(ISO_DIR) 2>/dev/null || \
(echo "Error: grub-mkrescue failed. Install grub-pc-bin and xorriso." && exit 1)
@rm -rf $(ISO_DIR)
@echo "Bootable ISO created: $(BOOT_ISO)"
# Bochs with multiboot2 (boots from ISO)
bochs: $(BOOT_ISO)
@echo "Starting Bochs (multiboot2 via GRUB ISO)..."
@if [ ! -f "$(BOCHSRC)" ]; then \
echo "Error: $(BOCHSRC) not found"; \
exit 1; \
fi
$(BOCHS) -f $(BOCHSRC) -q
# Alternative: QEMU with ISO (useful for testing GRUB boot)
qemu-iso: $(BOOT_ISO)
@echo "Starting QEMU with GRUB ISO..."
$(QEMU) -machine q35 -m $(QEMU_MEMORY) -cdrom $(BOOT_ISO) -serial stdio -no-reboot
# Testing
test:
cargo test --lib --target x86_64-unknown-linux-gnu
test-verbose:
cargo test --lib --target x86_64-unknown-linux-gnu -- --nocapture
# Linting and formatting
clippy:
cargo clippy --lib --target x86_64-unknown-linux-gnu -- -D warnings
cargo clippy --bin xomb-uefi --features uefi --target $(TARGET_UEFI) $(BUILD_STD) -- -D warnings
fmt:
cargo fmt
fmt-check:
cargo fmt -- --check
# Clean
clean:
cargo clean
rm -rf $(ESP_DIR) $(ESP_IMG) $(BOOT_ISO) $(ISO_DIR) OVMF_VARS.fd bochs.log debugger.log serial.log
# Setup helper - checks dependencies
setup:
@echo "Checking dependencies..."
@echo ""
@echo "Rust toolchain:"
@rustc --version || echo " ERROR: rustc not found"
@cargo --version || echo " ERROR: cargo not found"
@echo ""
@echo "Required components:"
@rustup component list --installed | grep -E "(rust-src|llvm-tools)" || echo " Run: rustup component add rust-src llvm-tools-preview"
@echo ""
@echo "Targets:"
@rustup target list --installed | grep -E "($(TARGET_UEFI)|$(TARGET_MB2))" || echo " Run: rustup target add $(TARGET_UEFI) $(TARGET_MB2)"
@echo ""
@echo "OVMF firmware:"
@if [ -f "$(OVMF_CODE)" ]; then echo " Found: $(OVMF_CODE)"; else echo " NOT FOUND: $(OVMF_CODE)"; fi
@if [ -f "$(OVMF_VARS)" ]; then echo " Found: $(OVMF_VARS)"; else echo " NOT FOUND: $(OVMF_VARS)"; fi
@echo ""
@echo "Emulators:"
@which qemu-system-x86_64 >/dev/null 2>&1 && echo " QEMU: $$(which qemu-system-x86_64)" || echo " QEMU: NOT FOUND"
@which bochs >/dev/null 2>&1 && echo " Bochs: $$(which bochs)" || echo " Bochs: NOT FOUND"
@echo ""
@echo "Disk tools:"
@which mkfs.fat >/dev/null 2>&1 && echo " mkfs.fat: OK" || echo " mkfs.fat: NOT FOUND (install dosfstools)"
@which mcopy >/dev/null 2>&1 && echo " mcopy: OK" || echo " mcopy: NOT FOUND (install mtools)"
@echo ""
@echo "Assembler:"
@which as >/dev/null 2>&1 && echo " as (GNU assembler): OK" || echo " as: NOT FOUND (install binutils)"
@echo ""
@echo "ISO creation:"
@which grub-mkrescue >/dev/null 2>&1 && echo " grub-mkrescue: OK" || echo " grub-mkrescue: NOT FOUND (install grub-common)"
@which xorriso >/dev/null 2>&1 && echo " xorriso: OK" || echo " xorriso: NOT FOUND (install xorriso)"
# Help
help:
@echo "XOmB Exokernel Build System"
@echo ""
@echo "Build commands:"
@echo " make build Build both UEFI and multiboot2 kernels"
@echo " make build-uefi Build UEFI kernel only"
@echo " make build-mb2 Build multiboot2 kernel only"
@echo " make release Build both in release mode"
@echo ""
@echo "Run commands:"
@echo " make qemu Run UEFI kernel in QEMU"
@echo " make qemu-mb2 Run multiboot2 kernel in QEMU"
@echo " make bochs Run multiboot2 kernel in Bochs"
@echo " make qemu-gdb Run UEFI kernel with GDB server"
@echo ""
@echo "Test commands:"
@echo " make test Run unit tests on host"
@echo " make clippy Run clippy lints"
@echo " make fmt Format code"
@echo ""
@echo "Other:"
@echo " make setup Check development dependencies"
@echo " make clean Remove build artifacts"

56
bochsrc.txt Normal file
View File

@@ -0,0 +1,56 @@
# Bochs configuration for XOmB exokernel (Multiboot2)
#
# Usage: make bochs
#
# This configuration boots the multiboot2 kernel using a GRUB-based disk image.
# CPU configuration - 64-bit mode
cpu: model=core2_penryn_t9600, count=1, ips=50000000, reset_on_triple_fault=1
# Memory: 512 MB
memory: guest=512, host=512
# BIOS (no fastboot for better CD-ROM compatibility)
romimage: file=$BXSHARE/BIOS-bochs-latest
vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest.bin
# Boot from CD-ROM (ISO image)
boot: cdrom
# CD-ROM with GRUB bootable ISO
ata0: enabled=true, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=cdrom, path="xomb.iso", status=inserted
# Display - use X11 with debug GUI
display_library: x, options="gui_debug"
vga: extension=vbe, update_freq=15, realtime=1
# Serial port - output to file for debugging
com1: enabled=true, mode=file, dev=serial.log
# Enable magic breakpoint (xchg bx, bx)
magic_break: enabled=1
# Mouse and keyboard
mouse: enabled=false
keyboard: type=mf, serial_delay=250, paste_delay=100000
# Clock
clock: sync=realtime, time0=local
# Logging
log: bochs.log
debugger_log: debugger.log
# Port E9 hack - output to console (useful for early debugging)
port_e9_hack: enabled=1
# Panic behavior
panic: action=ask
error: action=report
info: action=report
debug: action=ignore
# Debug controls (if using Bochs debugger)
# Press Ctrl+C in Bochs to enter debugger
# Commands: c (continue), s (step), b (breakpoint), r (registers)

11
boot/grub/grub.cfg Normal file
View File

@@ -0,0 +1,11 @@
# GRUB configuration for XOmB exokernel
#
# This file is embedded in the bootable disk image for Bochs.
set timeout=0
set default=0
menuentry "XOmB Exokernel" {
multiboot2 /boot/xomb-multiboot2
boot
}

56
build.rs Normal file
View File

@@ -0,0 +1,56 @@
//! Build script for XOmB
//!
//! Assembles the multiboot2 boot stub when building for bare metal target.
use std::env;
use std::path::PathBuf;
use std::process::Command;
fn main() {
let target = env::var("TARGET").unwrap_or_default();
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
// Only build assembly for multiboot2 target
if (target.contains("none") || target.contains("xomb")) && env::var("CARGO_FEATURE_MULTIBOOT2").is_ok() {
println!("cargo:rerun-if-changed=src/boot/multiboot2_header.asm");
println!("cargo:rerun-if-changed=linker-multiboot2.ld");
// Assemble with NASM (handles 32/64-bit mixing correctly)
let obj_path = out_dir.join("multiboot2_header.o");
let status = Command::new("nasm")
.args([
"-f", "elf64",
"-o", obj_path.to_str().unwrap(),
"src/boot/multiboot2_header.asm",
])
.status()
.expect("Failed to run NASM. Install nasm package.");
if !status.success() {
panic!("Failed to assemble multiboot2_header.asm");
}
// Create static library
let lib_path = out_dir.join("libboot_asm.a");
let status = Command::new("ar")
.args([
"crus",
lib_path.to_str().unwrap(),
obj_path.to_str().unwrap(),
])
.status()
.expect("Failed to run ar");
if !status.success() {
panic!("Failed to create libboot_asm.a");
}
// Link the assembly library
println!("cargo:rustc-link-search=native={}", out_dir.display());
println!("cargo:rustc-link-lib=static=boot_asm");
// Use custom linker script
println!("cargo:rustc-link-arg=-Tlinker-multiboot2.ld");
println!("cargo:rustc-link-arg=--gc-sections");
}
}

27
docs/MAIN.md Normal file
View File

@@ -0,0 +1,27 @@
XOmB is an exokernel.
An exokernel is a type of kernel that provides a very minimal abstraction. It multiplexes hardware resources in userspace in the form of 'Library OSes'. These library OSes provide the implementations necessary to drive devices for the needs of applications that are linked to them.
The role of the privileged kernel is just what is necessary to secure access to resources. All responsibility for resource management and decisions based around how to effectively use those resources is relegated to the library OSes and the needs of each application.
In XOmB, the main mechanism to secure access to resources is the paging system. Resources are memory mapped whenever possible and then access to resources are given to applications, through their library OSes, via the paging system. For instance, access to the network card might be given by mapping the register space onto a region of memory and then placing that into the memory space of a library OS. If an application might only want to read that memory space, it could have those pages mapped into their virtual address space with a read-only flag set.
The exokernel will make extensive use of any optimization or technique available to it in order to most efficiently use virtual memory to provide access and access control of such resources. For example, in x86-64, the kernel would certainly make use of 'superpages', which are large virtual memory allocations made by shallow page table entries marked as terminating at higher levels. These pages, then, represent multiples of the normal page size in terms of their allocations. It might even be beneficial to use gigabyte-large virtual address spaces to very efficently map out large linear address spaces for application use.
Updating access controls (or revoking access) will be as simple as updating the page table entries themselves and flushing any cache or TLB that might still be referencing it. Multiplexing a scarce resource might entail atomically updating access in one page table entry with another entry within a different process virtual address space to, thus, atomically swap such access. Though, the atomicity is still affected by our ability to flush relevant caches in time. Therefore, it is still rather important to consider the scheduling of these actions when requested by each application.
The main kernel maintains the root page table. The root page table is effectively set at the start and not changed. Within the page table structure, the entries within maintain the state of the system and the current process or processes and the current resources. Each Process is effectively represented as a page table entry and a slightly lower level. So, if we assume a five-level paging system, like on modern x86-64, the root page table is created and maintained by the kernel and it maps into that as a page table entry a pointer to the root page table of a process. The process, then, in turn owns its page table and maps in resources via kernel primitive functions and system calls. Once resources are securely provisioned to the process, the process can effectively do anything the page tables permit. Resources are, like processes, represented by slightly lower level page table structures that are able to be mapped into the process page table (which in turn can be placed into the system address space via the kernel's root page table.)
To summarize the kernel actions we need to make this kernel operate:
- Create a process which is basically a virtual address space (which is represented as a page table entry and is the main data structure that represents a process or process group)
- Allocate a resource (which is represented as a page table entry)
- Attach a resource to a virtual address space (link a page table entry in a 'process' to the page table entry serving as the root of a resource)
- Update the access of a resource (update a page table entry in a 'process')
- Atomically swap access to a resource (update two 'process' structures by nulling a resource entry while adding it to another)
These operations need to be verifiable in our kernel. Applications rely on these operations being secure.
The unanswered questions so far have to do with scheduling and preemption.
For stage 1, we will have a non-preemptive single process kernel to sidestep solving these problems.

5
docs/MEMORY.md Normal file
View File

@@ -0,0 +1,5 @@
The XOmB exokernel multiplexes hardware resources mostly through the use of the virtual memory system. So, a hardware resource might be memory mapped into a certain memory range and then that memory range is, when access is granted, mapped into the virtual address space of a process.
This means that the kernel needs to allocate memory pages in order to allocate the page table entries. So, it maintains the data structure responsible for keeping track of free pages of physical memory. Applications may request specific physical pages to be allocated. Applications may create resources which are represented by page table entries... in our case, the kernel is a PML4, a process is a PML3, and thus a resource is either a PML2 (PD) or a first-level page table (PT). An application or library OS (libOS) can do this by asking the kernel to allocate a resource with a particular physical page to be used as its relative page table root.
The kernel should be somewhat aware of hardware memory mapped ranges and allocate resources for those that can be securely provisioned when asked by a user application via a library OS. The kernel does not create those resources itself, however, or have any logic that is specific to any hardware except what it needs for debugging itself. An 'init' process will eventually exist that will allocate some of those initial resources that it can pass off to driving libOSes and applications that run later.

180
docs/tasks/STAGE_1.md Normal file
View File

@@ -0,0 +1,180 @@
# Stage 1: Non-Preemptive Single Process Kernel
This document outlines the work required to build the initial XOmB exokernel as described in docs/MAIN.md. Stage 1 focuses on a non-preemptive, single-process kernel to establish the core mechanisms without solving scheduling and preemption problems.
## Goals
- Establish the kernel's page table as the root of the system
- Implement the five core kernel actions (for a single process)
- Demonstrate resource allocation and access control via paging
- Provide a foundation for Library OS development
## Core Kernel Actions to Implement
### 1. Create a Process (Virtual Address Space)
A process is represented as a page table entry at a level below the kernel's root page table. On x86-64 with 5-level paging, this means:
- Kernel owns PML5 (or PML4 on 4-level systems)
- A process is a PML4 (or PML3) entry that the kernel maps into its root
**Tasks:**
- [ ] Define the process data structure (essentially a page table root + metadata)
- [ ] Implement process creation (allocate page table, initialize entries)
- [ ] Map the process into the kernel's address space
### 2. Allocate a Resource
Resources are memory-mapped regions represented as page table structures that can be attached to processes.
**Tasks:**
- [ ] Define resource types (physical memory regions, device MMIO, etc.)
- [ ] Implement resource allocation (create page table entries representing the resource)
- [ ] Track allocated resources (ownership, reference counting?)
### 3. Attach a Resource to a Virtual Address Space
Link a resource's page table entry into a process's page table at a specified virtual address.
**Tasks:**
- [ ] Implement resource attachment (map resource page table into process page table)
- [ ] Handle alignment requirements (superpages: 2MB, 1GB)
- [ ] Set appropriate access flags (read, write, execute, user/supervisor)
### 4. Update Resource Access
Modify the access permissions of an already-attached resource.
**Tasks:**
- [ ] Implement permission updates (modify page table entry flags)
- [ ] Handle TLB invalidation (invlpg, or full flush)
- [ ] Consider cache coherency implications
### 5. Atomically Swap Resource Access
Transfer a resource from one process to another atomically.
**Tasks:**
- [ ] Implement atomic swap (null one entry while setting another)
- [ ] Handle TLB/cache synchronization
- [ ] Note: In single-process Stage 1, this may be simplified
## Infrastructure Required
### Physical Memory Management
**Tasks:**
- [ ] Parse memory map from bootloader (multiboot2/UEFI)
- [ ] Implement physical frame allocator
- [ ] Track free/used physical pages
### Page Table Management
**Tasks:**
- [ ] Implement page table creation and manipulation
- [ ] Support for 4KB, 2MB, and 1GB pages (superpages)
- [x] Kernel mapping strategy: higher-half at 0xFFFFFFFF80000000 with recursive mapping at PML4[510]
### System Call Interface
**Tasks:**
- [ ] Define syscall mechanism (syscall/sysret instruction)
- [ ] Implement syscall handler
- [ ] Define initial syscall ABI for the five core actions
### Initial Process Loading
**Tasks:**
- [ ] Define executable format (ELF?)
- [ ] Load initial process from boot module or embedded binary
- [ ] Transfer control to user mode
## Design Decisions
### Memory Layout
- **Higher-half kernel**: Kernel mapped at 0xFFFFFFFF80000000 (top 2GB, required for kernel code model)
- **4-level paging**: Using standard x86-64 4-level paging (PML4 → PDPT → PD → PT). 5-level paging (LA57) is a future consideration.
- **Self-referencing page table**: PML4[510] points to the PML4 itself, enabling recursive page table access
- **Kernel mapping**: PML4[511] maps the kernel's higher-half address space
#### PML4 Layout
| Index | Purpose | Virtual Address Range |
|-------|---------|----------------------|
| 0 | Identity map (boot only) | 0x0000_0000_0000_0000 - 0x0000_007F_FFFF_FFFF |
| 510 | Recursive mapping | 0xFFFF_FF00_0000_0000 - 0xFFFF_FF7F_FFFF_FFFF |
| 511 | Kernel higher-half | 0xFFFF_FF80_0000_0000 - 0xFFFF_FFFF_FFFF_FFFF |
#### Recursive Mapping Implications
With PML4[510] as the self-reference entry:
- **Recursive region base**: 0xFFFF_FF00_0000_0000
- **PML4 accessible at**: 0xFFFF_FF7F_BFDF_E000
- **Any page table** can be accessed by constructing the appropriate virtual address
The recursive mapping formula for accessing page table entries:
```
PML4: 0xFFFFFF7FBFDFE000 + (pml4_idx * 8)
PDPT: 0xFFFFFF7FBFC00000 + (pml4_idx * 0x1000) + (pdpt_idx * 8)
PD: 0xFFFFFF7F80000000 + (pml4_idx * 0x200000) + (pdpt_idx * 0x1000) + (pd_idx * 8)
PT: 0xFFFFFF0000000000 + (pml4_idx * 0x40000000) + (pdpt_idx * 0x200000) + (pd_idx * 0x1000) + (pt_idx * 8)
```
## Open Questions
The following questions need to be answered before or during implementation:
### Resource Model
1. **Resource granularity**: What's the minimum resource size? A single 4KB page, or always aligned to superpages?
2. **Device resources in Stage 1**: Do we need device MMIO support, or just physical memory? For a minimal kernel, memory-only may suffice.
3. **Resource metadata**: Where do we store resource metadata (size, type, owner)? Separate structures, or encoded in page table entries?
### Process Model
4. **Process metadata location**: Where does process state live? In kernel memory, or in a reserved area of the process's own address space?
5. **Initial process origin**: Is the first process loaded from a multiboot module, embedded in the kernel, or loaded from a filesystem?
### Stage 1 Scope
6. **User mode in Stage 1?**: Does Stage 1 require actual user-mode execution, or can we demonstrate the mechanisms with kernel-mode "processes" first?
7. **Serial/console output from processes**: How do processes output debug information? Direct serial access? Kernel-provided syscall?
## Boot Code Status
The boot assembly (`src/boot/multiboot2_header.asm`) and linker script (`linker-multiboot2.ld`) have been updated:
1. **[DONE] Identity map first 1GB** - PML4[0] → PDPT_LOW → PD (512 x 2MB pages)
2. **[DONE] Map kernel at higher-half** - PML4[511] → PDPT_HIGH → PD at 0xFFFFFFFF80000000
3. **[DONE] Set up recursive mapping** - PML4[510] = physical address of PML4 | flags
4. **[DONE] Jump to higher-half** - Boot code transitions to higher-half stack and calls Rust entry point
5. **[TODO] Unmap identity mapping** - Remove PML4[0] once running in higher-half (can be done in Rust)
## Suggested Implementation Order
1. ~~Update boot code for higher-half + recursive mapping~~ **[DONE]**
2. ~~Update linker script for higher-half kernel~~ **[DONE]**
3. Physical memory allocator (using boot memory map)
4. Page table manipulation primitives (using recursive mapping)
5. Process creation (allocate PML4, map into kernel space)
6. Resource allocation (physical memory regions as page table structures)
7. Resource attachment (map into process address space)
8. Permission updates and TLB management
9. System call interface
10. Initial process loading and user-mode transition
11. Atomic resource swapping
## Success Criteria
Stage 1 is complete when:
- A single process can be created with its own virtual address space
- Physical memory resources can be allocated and mapped into the process
- Access permissions can be set and modified
- The process can execute code in user mode
- Basic syscalls allow the process to request resources from the kernel

95
linker-multiboot2.ld Normal file
View File

@@ -0,0 +1,95 @@
/* Linker script for XOmB multiboot2 kernel (higher-half) */
ENTRY(_start)
/* Physical load address (standard location for multiboot) */
KERNEL_PHYS = 0x100000;
/* Virtual address base (top 2GB for kernel code model compatibility) */
KERNEL_VIRT = 0xFFFFFFFF80000000;
SECTIONS
{
/* ===== Boot sections at physical addresses ===== */
/* These run before paging maps the higher-half */
. = KERNEL_PHYS;
_kernel_phys_start = .;
/* Multiboot2 header must be in the first 32KB of the file */
.multiboot2_header : ALIGN(8)
{
KEEP(*(.multiboot2_header))
}
/* Boot code (32-bit startup and 64-bit trampoline) */
/* Must be at physical address for initial execution */
.text.boot : ALIGN(4096)
{
*(.text.boot)
}
/* Record end of low-memory boot sections */
. = ALIGN(4096);
_boot_end_phys = .;
/* ===== Kernel sections at higher-half virtual addresses ===== */
/* Calculate physical address where higher-half sections will be loaded */
_higher_half_phys = .;
/* Switch to higher-half virtual addresses */
. = KERNEL_VIRT + _higher_half_phys;
/* Main kernel code */
.text : AT(_higher_half_phys)
{
_text_start = .;
*(.text .text.*)
_text_end = .;
}
/* Read-only data - use ALIGN to ensure proper separation */
. = ALIGN(4096);
.rodata : AT(_higher_half_phys + (. - (KERNEL_VIRT + _higher_half_phys)))
{
_rodata_start = .;
*(.rodata .rodata.*)
_rodata_end = .;
}
/* Initialized data */
. = ALIGN(4096);
.data : AT(_higher_half_phys + (. - (KERNEL_VIRT + _higher_half_phys)))
{
_data_start = .;
*(.data .data.*)
_data_end = .;
}
/* BSS (uninitialized data) */
. = ALIGN(4096);
.bss (NOLOAD) : AT(_higher_half_phys + (. - (KERNEL_VIRT + _higher_half_phys)))
{
_bss_start = .;
__bss_start = .;
*(.bss .bss.*)
*(.bss.stack)
*(COMMON)
__bss_end = .;
_bss_end = .;
}
/* Kernel end markers */
. = ALIGN(4096);
_kernel_virt_end = .;
_kernel_phys_end = _higher_half_phys + (. - (KERNEL_VIRT + _higher_half_phys));
/* Discard unnecessary sections */
/DISCARD/ :
{
*(.comment)
*(.note.*)
*(.eh_frame*)
}
}

13
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,13 @@
[toolchain]
channel = "nightly"
components = [
"rust-src",
"rustfmt",
"clippy",
"llvm-tools-preview",
]
targets = [
"x86_64-unknown-uefi",
"x86_64-unknown-linux-gnu", # For host-side unit tests
]
profile = "minimal"

9
src/arch/mod.rs Normal file
View File

@@ -0,0 +1,9 @@
//! Architecture-specific code
//!
//! Currently only x86_64 is supported.
#[cfg(target_arch = "x86_64")]
pub mod x86_64;
#[cfg(target_arch = "x86_64")]
pub use x86_64::*;

91
src/arch/x86_64/mod.rs Normal file
View File

@@ -0,0 +1,91 @@
//! x86_64 architecture support
/// Halt the CPU until the next interrupt
#[inline]
pub fn hlt() {
unsafe {
core::arch::asm!("hlt", options(nostack, nomem, preserves_flags));
}
}
/// Disable interrupts
#[inline]
pub fn cli() {
unsafe {
core::arch::asm!("cli", options(nostack, nomem, preserves_flags));
}
}
/// Enable interrupts
#[inline]
pub fn sti() {
unsafe {
core::arch::asm!("sti", options(nostack, nomem, preserves_flags));
}
}
/// Halt the CPU with interrupts disabled (hang forever)
#[inline]
pub fn halt() -> ! {
loop {
unsafe {
core::arch::asm!("cli; hlt", options(nostack, nomem, preserves_flags));
}
}
}
/// Read the current value of the RFLAGS register
#[inline]
pub fn read_rflags() -> u64 {
let rflags: u64;
unsafe {
core::arch::asm!("pushfq; pop {}", out(reg) rflags, options(nostack, preserves_flags));
}
rflags
}
/// Check if interrupts are enabled
#[inline]
pub fn interrupts_enabled() -> bool {
read_rflags() & (1 << 9) != 0
}
/// Output a byte to an I/O port
///
/// # Safety
/// Writing to arbitrary I/O ports can cause undefined behavior.
#[inline]
pub unsafe fn outb(port: u16, val: u8) {
unsafe {
core::arch::asm!("out dx, al", in("dx") port, in("al") val, options(nostack, preserves_flags));
}
}
/// Input a byte from an I/O port
///
/// # Safety
/// Reading from arbitrary I/O ports can cause undefined behavior.
#[inline]
pub unsafe fn inb(port: u16) -> u8 {
let ret: u8;
unsafe {
core::arch::asm!("in al, dx", out("al") ret, in("dx") port, options(nostack, preserves_flags));
}
ret
}
#[cfg(test)]
mod tests {
// Most of these functions require actual hardware to test
// They're better suited for integration tests
#[test]
fn test_rflags_bit_check() {
// Test that our flag checking logic works
let flags_with_if = 1u64 << 9;
assert_ne!(flags_with_if & (1 << 9), 0);
let flags_without_if = 0u64;
assert_eq!(flags_without_if & (1 << 9), 0);
}
}

7
src/boot/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
//! Boot protocol implementations
//!
//! This module contains entry points for different boot protocols:
//! - UEFI: See src/main.rs
//! - Multiboot2: See multiboot2.rs
pub mod multiboot2;

309
src/boot/multiboot2.rs Normal file
View File

@@ -0,0 +1,309 @@
//! Multiboot2 Boot Entry Point for XOmB
//!
//! This module handles the kernel entry when booted via a multiboot2-compliant
//! bootloader (e.g., GRUB). It parses the multiboot2 information structure and
//! initializes the kernel.
use core::fmt::Write;
use crate::serial::SerialPort;
use crate::{VERSION, NAME};
use crate::boot_info::{BootInfo, BootMethod, MemoryRegionType, FramebufferInfo, FramebufferType};
/// Multiboot2 magic number (passed in eax by bootloader)
pub const MULTIBOOT2_BOOTLOADER_MAGIC: u32 = 0x36d76289;
/// Multiboot2 tag types
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TagType {
End = 0,
Cmdline = 1,
BootLoaderName = 2,
Module = 3,
BasicMeminfo = 4,
Bootdev = 5,
Mmap = 6,
Vbe = 7,
Framebuffer = 8,
ElfSections = 9,
Apm = 10,
Efi32 = 11,
Efi64 = 12,
Smbios = 13,
AcpiOld = 14,
AcpiNew = 15,
Network = 16,
EfiMmap = 17,
EfiBs = 18,
Efi32Ih = 19,
Efi64Ih = 20,
LoadBaseAddr = 21,
}
/// Multiboot2 information header
#[repr(C)]
pub struct Mb2BootInfo {
pub total_size: u32,
pub reserved: u32,
// Tags follow...
}
/// Multiboot2 tag header
#[repr(C)]
pub struct Tag {
pub typ: u32,
pub size: u32,
// Tag-specific data follows...
}
/// Basic memory information tag
#[repr(C)]
pub struct BasicMeminfoTag {
pub typ: u32,
pub size: u32,
pub mem_lower: u32, // KB of lower memory (starting at 0)
pub mem_upper: u32, // KB of upper memory (starting at 1MB)
}
/// Memory map tag
#[repr(C)]
pub struct MmapTag {
pub typ: u32,
pub size: u32,
pub entry_size: u32,
pub entry_version: u32,
// Entries follow...
}
/// Memory map entry
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct MmapEntry {
pub base_addr: u64,
pub length: u64,
pub typ: u32,
pub reserved: u32,
}
impl MmapEntry {
pub fn is_usable(&self) -> bool {
self.typ == 1 // Type 1 = available RAM
}
/// Convert multiboot2 memory type to our unified MemoryRegionType
pub fn to_region_type(&self) -> MemoryRegionType {
match self.typ {
1 => MemoryRegionType::Usable,
2 => MemoryRegionType::Reserved,
3 => MemoryRegionType::AcpiReclaimable,
4 => MemoryRegionType::AcpiNvs,
5 => MemoryRegionType::BadMemory,
other => MemoryRegionType::Unknown(other),
}
}
}
/// Framebuffer tag
#[repr(C)]
pub struct FramebufferTag {
pub typ: u32,
pub size: u32,
pub addr: u64,
pub pitch: u32,
pub width: u32,
pub height: u32,
pub bpp: u8,
pub fb_type: u8,
pub reserved: u16,
}
/// ACPI old RSDP tag (version 1.0)
#[repr(C)]
pub struct AcpiOldTag {
pub typ: u32,
pub size: u32,
// RSDP structure follows (20 bytes for v1)
}
/// ACPI new RSDP tag (version 2.0+)
#[repr(C)]
pub struct AcpiNewTag {
pub typ: u32,
pub size: u32,
// RSDP structure follows (36 bytes for v2)
}
/// Multiboot2 entry point - called from assembly
///
/// # Safety
/// This function is called directly from assembly with raw pointers.
#[unsafe(no_mangle)]
pub extern "C" fn multiboot2_entry(info_ptr: *const Mb2BootInfo, magic: u32) -> ! {
// Initialize serial port for debug output
let mut serial = unsafe { SerialPort::new(0x3F8) };
serial.init();
writeln!(serial, "").ok();
writeln!(serial, "================================").ok();
writeln!(serial, " {} v{}", NAME, VERSION).ok();
writeln!(serial, " Multiboot2 Boot").ok();
writeln!(serial, "================================").ok();
writeln!(serial, "").ok();
// Verify multiboot2 magic
if magic != MULTIBOOT2_BOOTLOADER_MAGIC {
writeln!(serial, "ERROR: Invalid multiboot2 magic: {:#x}", magic).ok();
writeln!(serial, " Expected: {:#x}", MULTIBOOT2_BOOTLOADER_MAGIC).ok();
halt();
}
writeln!(serial, "Multiboot2 magic verified: {:#x}", magic).ok();
writeln!(serial, "Boot info at: {:p}", info_ptr).ok();
// Create unified boot info structure
let mut boot_info = BootInfo::new(BootMethod::Multiboot2);
// Set kernel addresses (from linker script)
boot_info.kernel_physical_base = 0x100000; // Standard multiboot load address
boot_info.kernel_virtual_base = 0xFFFFFFFF80000000; // Higher-half mapping
// Parse boot information
if !info_ptr.is_null() {
let info = unsafe { &*info_ptr };
writeln!(serial, "Boot info size: {} bytes", info.total_size).ok();
// Iterate through tags
let mut tag_ptr = unsafe { (info_ptr as *const u8).add(8) } as *const Tag;
let end_ptr = unsafe { (info_ptr as *const u8).add(info.total_size as usize) };
while (tag_ptr as *const u8) < end_ptr {
let tag = unsafe { &*tag_ptr };
if tag.typ == TagType::End as u32 {
break;
}
match tag.typ {
typ if typ == TagType::BasicMeminfo as u32 => {
let meminfo = unsafe { &*(tag_ptr as *const BasicMeminfoTag) };
writeln!(serial, "Basic memory: lower={}KB, upper={}KB",
meminfo.mem_lower, meminfo.mem_upper).ok();
}
typ if typ == TagType::Mmap as u32 => {
let mmap = unsafe { &*(tag_ptr as *const MmapTag) };
writeln!(serial, "Memory map (entry_size={}):", mmap.entry_size).ok();
let entries_start = unsafe { (tag_ptr as *const u8).add(16) };
let entries_end = unsafe { (tag_ptr as *const u8).add(mmap.size as usize) };
let mut entry_ptr = entries_start;
while entry_ptr < entries_end {
let entry = unsafe { &*(entry_ptr as *const MmapEntry) };
let type_str = match entry.typ {
1 => "Available",
2 => "Reserved",
3 => "ACPI Reclaimable",
4 => "ACPI NVS",
5 => "Bad Memory",
_ => "Unknown",
};
writeln!(serial, " {:#016x} - {:#016x} ({} bytes) {}",
entry.base_addr,
entry.base_addr + entry.length,
entry.length,
type_str).ok();
// Add to unified memory map
boot_info.memory_map.add(
entry.base_addr,
entry.length,
entry.to_region_type(),
);
entry_ptr = unsafe { entry_ptr.add(mmap.entry_size as usize) };
}
}
typ if typ == TagType::BootLoaderName as u32 => {
let name_ptr = unsafe { (tag_ptr as *const u8).add(8) };
let mut len = 0;
while unsafe { *name_ptr.add(len) } != 0 && len < 256 {
len += 1;
}
let name = unsafe {
core::str::from_utf8_unchecked(core::slice::from_raw_parts(name_ptr, len))
};
writeln!(serial, "Bootloader: {}", name).ok();
}
typ if typ == TagType::Cmdline as u32 => {
let cmdline_ptr = unsafe { (tag_ptr as *const u8).add(8) };
let mut len = 0;
while unsafe { *cmdline_ptr.add(len) } != 0 && len < 256 {
len += 1;
}
let cmdline = unsafe {
core::slice::from_raw_parts(cmdline_ptr, len)
};
boot_info.set_cmdline(cmdline);
writeln!(serial, "Command line: {}", boot_info.cmdline_str()).ok();
}
typ if typ == TagType::Framebuffer as u32 => {
let fb = unsafe { &*(tag_ptr as *const FramebufferTag) };
boot_info.framebuffer = FramebufferInfo {
address: fb.addr,
width: fb.width,
height: fb.height,
pitch: fb.pitch,
bpp: fb.bpp,
fb_type: match fb.fb_type {
0 => FramebufferType::Indexed,
1 => FramebufferType::Rgb,
2 => FramebufferType::EgaText,
_ => FramebufferType::Unknown,
},
};
writeln!(serial, "Framebuffer: {}x{} @ {:#x} ({}bpp)",
fb.width, fb.height, fb.addr, fb.bpp).ok();
}
typ if typ == TagType::AcpiOld as u32 => {
// RSDP v1 starts at offset 8
let rsdp_addr = unsafe { (tag_ptr as *const u8).add(8) } as u64;
boot_info.acpi.rsdp = rsdp_addr;
writeln!(serial, "ACPI RSDP v1 at: {:#x}", rsdp_addr).ok();
}
typ if typ == TagType::AcpiNew as u32 => {
// RSDP v2 starts at offset 8
let rsdp_addr = unsafe { (tag_ptr as *const u8).add(8) } as u64;
boot_info.acpi.rsdp_v2 = rsdp_addr;
if boot_info.acpi.rsdp == 0 {
boot_info.acpi.rsdp = rsdp_addr;
}
writeln!(serial, "ACPI RSDP v2 at: {:#x}", rsdp_addr).ok();
}
_ => {
// Skip unknown tags
}
}
// Move to next tag (8-byte aligned)
let next_offset = ((tag.size + 7) & !7) as usize;
tag_ptr = unsafe { (tag_ptr as *const u8).add(next_offset) } as *const Tag;
}
}
let total_memory = boot_info.memory_map.total_usable_memory();
writeln!(serial, "").ok();
writeln!(serial, "Total usable memory: {} MB", total_memory / (1024 * 1024)).ok();
writeln!(serial, "").ok();
// Transition to common kernel entry point
crate::kernel_init(&boot_info)
}
/// Halt the CPU
fn halt() -> ! {
loop {
unsafe {
core::arch::asm!("cli; hlt", options(nostack, nomem));
}
}
}

View File

@@ -0,0 +1,241 @@
; Multiboot2 Header and Boot Stub for XOmB
;
; Sets up higher-half kernel with recursive page table mapping.
; Uses 4-level paging with:
; - PML4[0] -> Identity map first 1GB (for boot transition)
; - PML4[510] -> Recursive mapping (points to PML4 itself)
; - PML4[511] -> Higher-half kernel at 0xFFFFFFFF80000000
MULTIBOOT2_MAGIC equ 0xe85250d6
MULTIBOOT2_ARCH_I386 equ 0
; Virtual address layout
; Kernel at top 2GB for kernel code model compatibility
KERNEL_VMA equ 0xFFFFFFFF80000000 ; Higher-half base (top 2GB)
KERNEL_PML4_IDX equ 511 ; PML4 index for kernel
KERNEL_PDPT_IDX equ 510 ; PDPT index for kernel (within PML4[511])
RECURSIVE_PML4_IDX equ 510 ; PML4 index for recursive mapping
; Physical memory layout for page tables
; Place page tables at 2MB to avoid GRUB's boot info which is usually below 1MB
PML4_TABLE equ 0x200000
PDPT_LOW equ 0x201000 ; PDPT for identity mapping (PML4[0])
PDPT_HIGH equ 0x202000 ; PDPT for kernel mapping (PML4[511])
PD_TABLE equ 0x203000 ; PD shared by both mappings
; Stack must be ABOVE page tables (0x204000+) to avoid being overwritten
PHYS_STACK_TOP equ 0x280000 ; Physical stack during boot (512KB above page tables)
; Higher-half addresses (used after paging enabled)
KERNEL_STACK_TOP equ KERNEL_VMA + PHYS_STACK_TOP
extern __bss_start
extern __bss_end
extern multiboot2_entry
; Multiboot2 header
section .multiboot2_header
align 8
multiboot2_header:
dd MULTIBOOT2_MAGIC
dd MULTIBOOT2_ARCH_I386
dd multiboot2_header_end - multiboot2_header ; Total header size
dd -(MULTIBOOT2_MAGIC + MULTIBOOT2_ARCH_I386 + (multiboot2_header_end - multiboot2_header))
; Framebuffer request tag
align 8
dw 5, 0 ; type=5 (framebuffer), flags=0
dd 20 ; size=20 bytes
dd 1024 ; width
dd 768 ; height
dd 32 ; depth
; End tag
align 8
dw 0, 0 ; type=0 (end), flags=0
dd 8 ; size=8 bytes
multiboot2_header_end:
; 32-bit boot code
section .text.boot
bits 32
global _start
_start:
cli
; Verify multiboot magic first (before clobbering eax)
cmp eax, 0x36d76289
jne .hang32
; Set up stack (physical address during boot)
mov esp, PHYS_STACK_TOP
; Save multiboot values on stack (esp is now valid)
push dword 0 ; Padding for 8-byte alignment
push eax ; magic (will be at [esp+4])
push dword 0 ; Padding
push ebx ; info ptr (will be at [esp])
; Zero page tables (PML4 + PDPT_LOW + PDPT_HIGH + PD = 4 pages = 4096 dwords)
mov edi, PML4_TABLE
xor eax, eax
mov ecx, 4096
.zero:
mov [edi], eax
add edi, 4
loop .zero
; === Set up PML4 entries ===
; PML4[0] -> PDPT_LOW (identity mapping for boot transition)
mov edi, PML4_TABLE
mov eax, PDPT_LOW | 3 ; Present + Writable
mov [edi], eax
mov dword [edi+4], 0
; PML4[510] -> PML4 (recursive mapping)
mov edi, PML4_TABLE + (RECURSIVE_PML4_IDX * 8)
mov eax, PML4_TABLE | 3 ; Points to itself
mov [edi], eax
mov dword [edi+4], 0
; PML4[511] -> PDPT_HIGH (kernel mapping at 0xFFFFFFFF80000000)
mov edi, PML4_TABLE + (KERNEL_PML4_IDX * 8)
mov eax, PDPT_HIGH | 3 ; Present + Writable
mov [edi], eax
mov dword [edi+4], 0
; === Set up PDPT entries ===
; PDPT_LOW[0] -> PD (identity maps first 1GB at virtual 0x0)
mov edi, PDPT_LOW
mov eax, PD_TABLE | 3 ; Present + Writable
mov [edi], eax
mov dword [edi+4], 0
; PDPT_HIGH[510] -> PD (maps first 1GB at virtual 0xFFFFFFFF80000000)
; Index 510 = offset 510 * 8 = 4080 = 0xFF0
mov edi, PDPT_HIGH + (KERNEL_PDPT_IDX * 8)
mov eax, PD_TABLE | 3 ; Present + Writable (same PD as identity map)
mov [edi], eax
mov dword [edi+4], 0
; === Set up PD entries (512 x 2MB pages = 1GB) ===
mov edi, PD_TABLE
mov eax, 0x83 ; Present + Writable + PageSize (2MB page)
mov ecx, 512
.map:
mov [edi], eax
mov dword [edi+4], 0
add eax, 0x200000 ; Next 2MB
add edi, 8
loop .map
; CR3 = PML4
mov eax, PML4_TABLE
mov cr3, eax
; Enable PAE
mov eax, cr4
or eax, 0x20
mov cr4, eax
; Enable Long Mode (LME) and No-Execute (NXE)
; EFER bits: 8=LME, 11=NXE
mov ecx, 0xC0000080
rdmsr
or eax, 0x100 | 0x800 ; LME | NXE
wrmsr
; Enable paging
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
; Load GDT - use dword to force 32-bit absolute addressing
lgdt [dword gdt_ptr]
; Far jump to 64-bit mode
jmp dword 0x08:long_mode
.hang32:
hlt
jmp .hang32
; GDT pointer
align 4
gdt_ptr:
dw gdt_end - gdt - 1
dd gdt
; GDT
align 8
gdt:
dq 0 ; Null descriptor
gdt_code:
dw 0xFFFF ; Limit
dw 0 ; Base low
db 0 ; Base mid
db 0x9A ; Access: code, exec/read
db 0xAF ; Flags: 64-bit, limit high
db 0 ; Base high
gdt_data:
dw 0xFFFF
dw 0
db 0
db 0x92 ; Access: data, read/write
db 0xCF ; Flags: 32-bit
db 0
gdt_end:
; 64-bit entry point (still running at identity-mapped address)
bits 64
long_mode:
; Set up data segments
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; Enable SSE (required by x86-64 ABI for floating-point)
; 1. Clear CR0.EM (bit 2) and set CR0.MP (bit 1)
mov rax, cr0
and ax, 0xFFFB ; Clear EM (bit 2)
or ax, 0x2 ; Set MP (bit 1)
mov cr0, rax
; 2. Set CR4.OSFXSR (bit 9) and CR4.OSXMMEXCPT (bit 10)
mov rax, cr4
or ax, (1 << 9) | (1 << 10)
mov cr4, rax
; Set up higher-half stack (using high address now that paging is on)
mov rsp, KERNEL_STACK_TOP
; Zero BSS (linker provides higher-half addresses)
mov rdi, __bss_start
mov rcx, __bss_end
sub rcx, rdi
jz .skip_bss ; Skip if BSS is empty
shr rcx, 3
xor rax, rax
rep stosq
.skip_bss:
; Restore multiboot values from stack (still at physical address)
; Stack layout at PHYS_STACK_TOP: [info_ptr, 0, magic, 0]
mov edi, [PHYS_STACK_TOP - 16] ; info ptr
mov esi, [PHYS_STACK_TOP - 8] ; magic
; Jump to higher-half kernel entry point
mov rax, multiboot2_entry
call rax
.hang64:
cli
hlt
jmp .hang64
; Stack
section .bss.stack nobits alloc write
align 16
resb 65536

246
src/boot_info.rs Normal file
View File

@@ -0,0 +1,246 @@
//! Unified Boot Information
//!
//! This module provides a boot-method-agnostic representation of the
//! information passed from the bootloader to the kernel. Both UEFI and
//! Multiboot2 paths populate this structure before calling kernel_init().
/// Maximum number of memory map entries we support
pub const MAX_MEMORY_REGIONS: usize = 64;
/// Maximum command line length
pub const MAX_CMDLINE_LEN: usize = 256;
/// Boot method used to start the kernel
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BootMethod {
/// Booted via UEFI firmware
Uefi,
/// Booted via Multiboot2-compliant bootloader (e.g., GRUB)
Multiboot2,
}
/// Memory region type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemoryRegionType {
/// Usable RAM - available for kernel use
Usable,
/// Reserved by firmware or hardware
Reserved,
/// ACPI tables - can be reclaimed after parsing
AcpiReclaimable,
/// ACPI Non-Volatile Storage
AcpiNvs,
/// Bad/defective memory
BadMemory,
/// Bootloader code/data - can be reclaimed
BootloaderReclaimable,
/// Kernel code and data
KernelAndModules,
/// Framebuffer memory
Framebuffer,
/// Unknown/other type
Unknown(u32),
}
/// A single memory region
#[derive(Debug, Clone, Copy)]
pub struct MemoryRegion {
/// Physical base address
pub base: u64,
/// Length in bytes
pub length: u64,
/// Region type
pub region_type: MemoryRegionType,
}
impl MemoryRegion {
pub const fn empty() -> Self {
Self {
base: 0,
length: 0,
region_type: MemoryRegionType::Reserved,
}
}
/// Returns true if this region contains usable RAM
pub fn is_usable(&self) -> bool {
matches!(self.region_type, MemoryRegionType::Usable)
}
}
/// Memory map containing all memory regions
#[derive(Debug, Clone, Copy)]
pub struct MemoryMap {
/// Memory regions (unused entries have length 0)
pub regions: [MemoryRegion; MAX_MEMORY_REGIONS],
/// Number of valid entries
pub count: usize,
}
impl MemoryMap {
pub const fn empty() -> Self {
Self {
regions: [MemoryRegion::empty(); MAX_MEMORY_REGIONS],
count: 0,
}
}
/// Add a memory region to the map
pub fn add(&mut self, base: u64, length: u64, region_type: MemoryRegionType) -> bool {
if self.count >= MAX_MEMORY_REGIONS {
return false;
}
self.regions[self.count] = MemoryRegion {
base,
length,
region_type,
};
self.count += 1;
true
}
/// Get an iterator over valid memory regions
pub fn iter(&self) -> impl Iterator<Item = &MemoryRegion> {
self.regions[..self.count].iter()
}
/// Calculate total usable memory in bytes
pub fn total_usable_memory(&self) -> u64 {
self.iter()
.filter(|r| r.is_usable())
.map(|r| r.length)
.sum()
}
}
/// Framebuffer information (if available)
#[derive(Debug, Clone, Copy)]
pub struct FramebufferInfo {
/// Physical address of framebuffer
pub address: u64,
/// Width in pixels
pub width: u32,
/// Height in pixels
pub height: u32,
/// Bytes per scanline (pitch)
pub pitch: u32,
/// Bits per pixel
pub bpp: u8,
/// Framebuffer type (RGB, indexed, etc.)
pub fb_type: FramebufferType,
}
impl FramebufferInfo {
pub const fn none() -> Self {
Self {
address: 0,
width: 0,
height: 0,
pitch: 0,
bpp: 0,
fb_type: FramebufferType::Unknown,
}
}
pub fn is_available(&self) -> bool {
self.address != 0 && self.width > 0 && self.height > 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FramebufferType {
/// Indexed color (palette-based)
Indexed,
/// Direct RGB color
Rgb,
/// EGA text mode
EgaText,
/// Unknown type
Unknown,
}
/// ACPI information
#[derive(Debug, Clone, Copy)]
pub struct AcpiInfo {
/// RSDP (Root System Description Pointer) address
/// This is the ACPI 1.0 RSDP if rsdp_v2 is 0, or ACPI 2.0+ RSDP otherwise
pub rsdp: u64,
/// ACPI 2.0+ extended RSDP address (0 if not available)
pub rsdp_v2: u64,
}
impl AcpiInfo {
pub const fn none() -> Self {
Self {
rsdp: 0,
rsdp_v2: 0,
}
}
pub fn is_available(&self) -> bool {
self.rsdp != 0 || self.rsdp_v2 != 0
}
}
/// Unified boot information structure
///
/// This structure is populated by the boot path (UEFI or Multiboot2) and
/// passed to kernel_init(). It provides a common interface regardless of
/// how the kernel was booted.
#[derive(Debug, Clone, Copy)]
pub struct BootInfo {
/// How the kernel was booted
pub boot_method: BootMethod,
/// Memory map
pub memory_map: MemoryMap,
/// Framebuffer information (if available)
pub framebuffer: FramebufferInfo,
/// ACPI information
pub acpi: AcpiInfo,
/// Command line (null-terminated, may be empty)
pub cmdline: [u8; MAX_CMDLINE_LEN],
/// Length of command line (not including null terminator)
pub cmdline_len: usize,
/// Physical address where kernel is loaded
pub kernel_physical_base: u64,
/// Virtual address where kernel is mapped (for higher-half kernels)
pub kernel_virtual_base: u64,
}
impl BootInfo {
/// Create an empty BootInfo with the specified boot method
pub const fn new(boot_method: BootMethod) -> Self {
Self {
boot_method,
memory_map: MemoryMap::empty(),
framebuffer: FramebufferInfo::none(),
acpi: AcpiInfo::none(),
cmdline: [0u8; MAX_CMDLINE_LEN],
cmdline_len: 0,
kernel_physical_base: 0,
kernel_virtual_base: 0,
}
}
/// Set the command line
pub fn set_cmdline(&mut self, cmdline: &[u8]) {
let len = cmdline.len().min(MAX_CMDLINE_LEN - 1);
self.cmdline[..len].copy_from_slice(&cmdline[..len]);
self.cmdline[len] = 0; // Null terminate
self.cmdline_len = len;
}
/// Get the command line as a string slice
pub fn cmdline_str(&self) -> &str {
// Safety: we only store valid UTF-8 from bootloader strings
unsafe {
core::str::from_utf8_unchecked(&self.cmdline[..self.cmdline_len])
}
}
}

231
src/lib.rs Normal file
View File

@@ -0,0 +1,231 @@
//! XOmB - A Rust-based exokernel
//!
//! This library contains the core kernel logic that can be unit-tested
//! on the host system without requiring an emulator.
#![no_std]
// When testing on host, we need std
#[cfg(test)]
extern crate std;
// Re-export alloc for heap allocations (available after boot services)
#[cfg(any(feature = "uefi", test))]
extern crate alloc;
pub mod arch;
pub mod boot_info;
pub mod memory;
pub mod serial;
#[cfg(feature = "multiboot2")]
pub mod boot;
// Re-export boot_info types for convenience
pub use boot_info::{BootInfo, BootMethod, MemoryRegionType};
// Re-export memory types for convenience
pub use memory::{PhysAddr, Frame, VirtAddr};
/// Kernel version information
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const NAME: &str = env!("CARGO_PKG_NAME");
use core::fmt::Write;
use serial::SerialPort;
/// Initialize the kernel after bootloader handoff
///
/// This is the common entry point for both UEFI and Multiboot2 boot paths.
/// At this point, boot services have been exited and we have full control.
pub fn kernel_init(info: &BootInfo) -> ! {
// Get serial port for output
let mut serial = unsafe { SerialPort::new(0x3F8) };
writeln!(serial, "").ok();
writeln!(serial, ">>> Entering kernel_init()").ok();
writeln!(serial, " Boot method: {:?}", info.boot_method).ok();
// Report memory information from boot
let total_memory = info.memory_map.total_usable_memory();
writeln!(serial, " Total usable memory: {} MB", total_memory / (1024 * 1024)).ok();
writeln!(serial, " Memory regions: {}", info.memory_map.count).ok();
// Report framebuffer if available
if info.framebuffer.is_available() {
writeln!(serial, " Framebuffer: {}x{} @ {:#x}",
info.framebuffer.width,
info.framebuffer.height,
info.framebuffer.address).ok();
}
// Report ACPI if available
if info.acpi.is_available() {
writeln!(serial, " ACPI RSDP: {:#x}", info.acpi.rsdp).ok();
}
// Report command line if present
if info.cmdline_len > 0 {
writeln!(serial, " Command line: {}", info.cmdline_str()).ok();
}
// Initialize physical memory allocator
writeln!(serial, "").ok();
writeln!(serial, ">>> Initializing physical memory allocator...").ok();
memory::frame::init(info);
let (free_mem, total_mem) = memory::frame::memory_stats();
writeln!(serial, " Physical memory allocator initialized").ok();
writeln!(serial, " Free memory: {} MB / {} MB",
free_mem / (1024 * 1024),
total_mem / (1024 * 1024)).ok();
// Test allocating a few frames
writeln!(serial, "").ok();
writeln!(serial, ">>> Testing frame allocator...").ok();
match memory::frame::allocate_frame() {
Ok(frame) => {
writeln!(serial, " Allocated frame: {} (phys: {:#x})",
frame.number(), frame.start_address()).ok();
// Deallocate it
if memory::frame::deallocate_frame(frame).is_ok() {
writeln!(serial, " Deallocated frame successfully").ok();
}
}
Err(e) => {
writeln!(serial, " Failed to allocate frame: {:?}", e).ok();
}
}
// Test allocating a specific frame (e.g., for a device)
let test_addr = PhysAddr::new(0x200000); // 2MB mark
match memory::frame::allocate_frame_at(test_addr) {
Ok(frame) => {
writeln!(serial, " Allocated specific frame at {:#x}", frame.start_address()).ok();
let _ = memory::frame::deallocate_frame(frame);
}
Err(e) => {
writeln!(serial, " Could not allocate frame at {:#x}: {:?}", test_addr, e).ok();
}
}
let (free_mem_after, _) = memory::frame::memory_stats();
writeln!(serial, " Free memory after tests: {} MB", free_mem_after / (1024 * 1024)).ok();
// Test page table primitives
writeln!(serial, "").ok();
writeln!(serial, ">>> Testing page table primitives...").ok();
// Test 1: Read PML4 entries to verify recursive mapping works
writeln!(serial, " Reading PML4 entries via recursive mapping:").ok();
let pml4_0 = memory::paging::read_pml4(0);
let pml4_510 = memory::paging::read_pml4(510);
let pml4_511 = memory::paging::read_pml4(511);
writeln!(serial, " PML4[0] (identity): {:?}", pml4_0).ok();
writeln!(serial, " PML4[510] (recursive): {:?}", pml4_510).ok();
writeln!(serial, " PML4[511] (kernel): {:?}", pml4_511).ok();
// Test 2: Translate a known address (kernel code)
let kernel_addr = VirtAddr::new(0xFFFFFFFF80102000); // Kernel .text
writeln!(serial, " Translating kernel address {:#x}:", kernel_addr).ok();
if let Some(phys) = memory::paging::translate(kernel_addr) {
writeln!(serial, " -> Physical: {:#x}", phys).ok();
} else {
writeln!(serial, " -> Not mapped (unexpected!)").ok();
}
// Test 3: Get mapping info for kernel address
if let Some((_phys, size, flags)) = memory::paging::get_mapping_info(kernel_addr) {
writeln!(serial, " Page size: {:?}, flags: {:#x}", size, flags).ok();
}
// Test 4: Map a new 4KB page
// Use an unmapped address in kernel space - PML4[509] is unused (between user and recursive regions)
let test_virt = VirtAddr::new(0xFFFFFE8000000000);
writeln!(serial, " Mapping new 4KB page at {:#x}:", test_virt).ok();
// Allocate a physical frame
match memory::frame::allocate_frame() {
Ok(frame) => {
let phys = frame.start_address();
writeln!(serial, " Allocated frame at {:#x}", phys).ok();
// Map with KERNEL_DATA (PRESENT | WRITABLE | NO_EXECUTE)
let result = memory::paging::map_4kb(test_virt, phys, memory::paging::flags::KERNEL_DATA);
match result {
Ok(()) => {
writeln!(serial, " Mapped successfully!").ok();
// Verify the mapping
if let Some(translated) = memory::paging::translate(test_virt) {
writeln!(serial, " Verified: {:#x} -> {:#x}", test_virt, translated).ok();
}
// Write to the mapped page to verify it's accessible
unsafe {
let ptr = test_virt.as_u64() as *mut u64;
*ptr = 0xDEADBEEF_CAFEBABE;
let read_back = *ptr;
writeln!(serial, " Write/read test: {:#x}", read_back).ok();
}
// Unmap the page
match memory::paging::unmap_4kb(test_virt) {
Ok(unmapped_frame) => {
writeln!(serial, " Unmapped, frame: {}", unmapped_frame.number()).ok();
let _ = memory::frame::deallocate_frame(unmapped_frame);
}
Err(e) => {
writeln!(serial, " Unmap failed: {:?}", e).ok();
}
}
}
Err(e) => {
writeln!(serial, " Map failed: {:?}", e).ok();
let _ = memory::frame::deallocate_frame(frame);
}
}
}
Err(e) => {
writeln!(serial, " Frame allocation failed: {:?}", e).ok();
}
}
writeln!(serial, "").ok();
writeln!(serial, "Kernel initialization complete.").ok();
writeln!(serial, "Halting CPU.").ok();
// Halt the CPU
loop {
unsafe {
core::arch::asm!("cli; hlt", options(nostack, nomem));
}
}
}
/// Example function demonstrating testable kernel logic
pub fn add(a: u64, b: u64) -> u64 {
a.wrapping_add(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_overflow() {
assert_eq!(add(u64::MAX, 1), 0);
}
#[test]
fn test_version_exists() {
assert!(!VERSION.is_empty());
}
}

164
src/main.rs Normal file
View File

@@ -0,0 +1,164 @@
//! XOmB UEFI Entry Point
//!
//! This is the UEFI application entry point. It initializes the kernel
//! environment, collects boot information, exits boot services, and
//! transfers control to the common kernel_init().
#![no_std]
#![no_main]
extern crate alloc;
use core::fmt::Write;
use uefi::prelude::*;
use uefi::boot;
use uefi::mem::memory_map::{MemoryMap, MemoryType as UefiMemoryType};
use xomb::{VERSION, NAME, kernel_init};
use xomb::boot_info::{BootInfo, BootMethod, MemoryRegionType};
use xomb::serial::SerialPort;
/// Convert UEFI memory type to our unified MemoryRegionType
fn uefi_to_region_type(ty: UefiMemoryType) -> MemoryRegionType {
match ty {
UefiMemoryType::CONVENTIONAL => MemoryRegionType::Usable,
UefiMemoryType::LOADER_CODE | UefiMemoryType::LOADER_DATA => {
MemoryRegionType::BootloaderReclaimable
}
UefiMemoryType::BOOT_SERVICES_CODE | UefiMemoryType::BOOT_SERVICES_DATA => {
MemoryRegionType::BootloaderReclaimable
}
UefiMemoryType::RUNTIME_SERVICES_CODE | UefiMemoryType::RUNTIME_SERVICES_DATA => {
MemoryRegionType::Reserved
}
UefiMemoryType::ACPI_RECLAIM => MemoryRegionType::AcpiReclaimable,
UefiMemoryType::ACPI_NON_VOLATILE => MemoryRegionType::AcpiNvs,
UefiMemoryType::UNUSABLE => MemoryRegionType::BadMemory,
UefiMemoryType::RESERVED | UefiMemoryType::MMIO
| UefiMemoryType::MMIO_PORT_SPACE | UefiMemoryType::PAL_CODE => {
MemoryRegionType::Reserved
}
UefiMemoryType::PERSISTENT_MEMORY => MemoryRegionType::Usable,
_ => MemoryRegionType::Unknown(ty.0),
}
}
/// UEFI entry point
#[entry]
fn main() -> Status {
// Initialize UEFI services (logging, allocator)
uefi::helpers::init().expect("Failed to initialize UEFI helpers");
// Get a serial port for debugging output
let mut serial = unsafe { SerialPort::new(0x3F8) };
serial.init();
writeln!(serial, "").ok();
writeln!(serial, "================================").ok();
writeln!(serial, " {} v{}", NAME, VERSION).ok();
writeln!(serial, " UEFI Boot").ok();
writeln!(serial, "================================").ok();
writeln!(serial, "").ok();
// Also log to UEFI console
log::info!("{} v{} starting...", NAME, VERSION);
// Create unified boot info structure
let mut boot_info = BootInfo::new(BootMethod::Uefi);
// Query memory map while boot services are available
log::info!("Querying memory map...");
{
let memory_map = boot::memory_map(boot::MemoryType::LOADER_DATA)
.expect("Failed to get memory map");
// Convert UEFI memory map to our unified format
for desc in memory_map.entries() {
let base = desc.phys_start;
let length = desc.page_count * 4096;
let region_type = uefi_to_region_type(desc.ty);
boot_info.memory_map.add(base, length, region_type);
// Log each region
let type_str = match desc.ty {
UefiMemoryType::CONVENTIONAL => "Conventional",
UefiMemoryType::LOADER_CODE => "LoaderCode",
UefiMemoryType::LOADER_DATA => "LoaderData",
UefiMemoryType::BOOT_SERVICES_CODE => "BootServicesCode",
UefiMemoryType::BOOT_SERVICES_DATA => "BootServicesData",
UefiMemoryType::RUNTIME_SERVICES_CODE => "RuntimeServicesCode",
UefiMemoryType::RUNTIME_SERVICES_DATA => "RuntimeServicesData",
UefiMemoryType::RESERVED => "Reserved",
UefiMemoryType::ACPI_RECLAIM => "ACPIReclaim",
UefiMemoryType::ACPI_NON_VOLATILE => "ACPINVS",
UefiMemoryType::MMIO => "MMIO",
_ => "Other",
};
writeln!(serial, " {:#016x} - {:#016x} ({} bytes) {}",
base, base + length, length, type_str).ok();
}
}
let total_memory = boot_info.memory_map.total_usable_memory();
log::info!("Conventional memory available: {} MB", total_memory / (1024 * 1024));
writeln!(serial, "Total usable memory: {} MB", total_memory / (1024 * 1024)).ok();
// Try to find ACPI tables via UEFI configuration table
log::info!("Looking for ACPI tables...");
let acpi_guid_v2 = uefi::table::cfg::ACPI2_GUID;
let acpi_guid_v1 = uefi::table::cfg::ACPI_GUID;
for entry in uefi::system::with_config_table(|table| table.to_vec()) {
if entry.guid == acpi_guid_v2 {
boot_info.acpi.rsdp_v2 = entry.address as u64;
boot_info.acpi.rsdp = entry.address as u64;
writeln!(serial, "ACPI RSDP v2 at: {:#x}", entry.address as u64).ok();
log::info!("Found ACPI 2.0 RSDP at {:#x}", entry.address as u64);
} else if entry.guid == acpi_guid_v1 && boot_info.acpi.rsdp == 0 {
boot_info.acpi.rsdp = entry.address as u64;
writeln!(serial, "ACPI RSDP v1 at: {:#x}", entry.address as u64).ok();
log::info!("Found ACPI 1.0 RSDP at {:#x}", entry.address as u64);
}
}
// Note: Framebuffer would be obtained via GOP (Graphics Output Protocol)
// For now, leave it as unavailable - can be added later
log::info!("Framebuffer: not configured (GOP support TODO)");
writeln!(serial, "").ok();
log::info!("Exiting boot services...");
writeln!(serial, "Exiting UEFI boot services...").ok();
// Exit boot services - after this, no more UEFI services!
// This gives us full control of the machine.
let _memory_map = unsafe {
boot::exit_boot_services(boot::MemoryType::LOADER_DATA)
};
// We're now in a bare-metal environment, similar to post-multiboot2
// Only serial output works from here on
writeln!(serial, "Boot services exited successfully.").ok();
writeln!(serial, "").ok();
// Transfer to common kernel entry point
kernel_init(&boot_info)
}
/// Panic handler - required for no_std
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
// Try to log via UEFI if services are still available
// This may fail if we've exited boot services
let _ = log::error!("KERNEL PANIC: {}", info);
// Always write to serial - this works even after exit_boot_services
let mut serial = unsafe { SerialPort::new(0x3F8) };
let _ = writeln!(serial, "\n!!! KERNEL PANIC !!!");
let _ = writeln!(serial, "{}", info);
loop {
unsafe { core::arch::asm!("cli; hlt") };
}
}

314
src/memory/README.md Normal file
View File

@@ -0,0 +1,314 @@
# Physical Memory Allocator
This document describes the physical page frame allocator used by the XOmB exokernel to track and allocate physical memory pages.
## Overview
The kernel multiplexes hardware resources through the virtual memory system. To do this, it needs to allocate physical memory pages for:
- Page table entries (PML4, PDPT, PD, PT)
- Resource mappings for applications
- Kernel data structures
The allocator maintains a bitmap tracking which physical pages (frames) are free or in use. Applications may request specific physical pages for resources like device MMIO or DMA buffers.
## Design
### Bitmap Allocator
We use a bitmap-based allocator where each bit represents one 4KB physical frame:
- **0** = frame is free
- **1** = frame is allocated or reserved
The bitmap is stored as an array of `u64` words, where each word tracks 64 frames (256KB of physical memory).
```
Bitmap word 0: [frame 0-63]
Bitmap word 1: [frame 64-127]
...
Bitmap word N: [frame N*64 to N*64+63]
```
### Memory Limits
| Constant | Value | Description |
|----------|-------|-------------|
| `PAGE_SIZE` | 4096 bytes | Standard x86-64 page size |
| `MAX_PHYSICAL_MEMORY` | 16 GB | Maximum supported physical memory |
| `MAX_FRAMES` | 4,194,304 | Maximum number of 4KB frames |
| `BITMAP_WORDS` | 65,536 | Number of u64 words in bitmap |
| Bitmap size | 512 KB | Total bitmap memory footprint |
### Data Structures
#### PhysAddr
A wrapper type for physical addresses with utility methods:
```rust
pub struct PhysAddr(u64);
impl PhysAddr {
pub const fn new(addr: u64) -> Self; // Create with masking
pub const fn as_u64(self) -> u64; // Get raw value
pub const fn is_aligned(self) -> bool; // Check 4KB alignment
pub const fn align_down(self) -> Self; // Round down to page
pub const fn align_up(self) -> Self; // Round up to page
pub const fn containing_frame(self) -> Frame; // Get containing frame
}
```
Physical addresses on x86-64 are limited to 52 bits. The upper bits are masked on creation.
#### Frame
Represents a single 4KB physical page frame:
```rust
pub struct Frame {
number: usize, // Frame number = physical_address / PAGE_SIZE
}
impl Frame {
pub const fn from_number(number: usize) -> Self;
pub const fn containing_address(addr: PhysAddr) -> Self;
pub const fn number(self) -> usize;
pub const fn start_address(self) -> PhysAddr;
}
```
#### FrameAllocator
The main allocator structure:
```rust
pub struct FrameAllocator {
bitmap: [u64; BITMAP_WORDS], // 512KB bitmap
total_frames: usize, // Total usable frames
free_frames: usize, // Currently free frames
initialized: bool, // Initialization flag
next_free_hint: usize, // Optimization hint
}
```
## Initialization
The allocator is initialized from the boot memory map in two phases:
### Phase 1: Mark All As Used
```rust
for word in self.bitmap.iter_mut() {
*word = !0u64; // All bits set = all frames used
}
```
This ensures any gaps or reserved regions in the memory map remain marked as unavailable.
### Phase 2: Free Usable Regions
```rust
for region in memory_map.iter() {
if region.region_type == MemoryRegionType::Usable {
// Free each complete frame in the region
for frame_num in first_frame..last_frame {
self.mark_free(frame_num);
}
}
}
```
Only frames that fall completely within usable memory regions are marked as free.
### Phase 3: Reserve Kernel Memory
After initialization, we reserve memory used by the kernel:
```rust
// Reserve first 1MB (BIOS, real mode IVT, etc.)
allocator.reserve_range(PhysAddr::new(0), 1024 * 1024);
// Reserve kernel physical memory (16MB from load address)
allocator.reserve_kernel(PhysAddr::new(0x100000), 16 * 1024 * 1024);
```
## Allocation Algorithm
### Allocate Any Frame
The `allocate()` function finds and allocates any free frame:
```rust
pub fn allocate(&mut self) -> Result<Frame, FrameAllocatorError> {
// Start search from hint position
let start_word = self.next_free_hint / 64;
// Search for a word with at least one free bit
for word_idx in start_word..BITMAP_WORDS {
if self.bitmap[word_idx] != !0u64 {
// Find first free bit using: (!word).trailing_zeros()
let bit = self.find_free_bit(self.bitmap[word_idx]);
let frame_num = word_idx * 64 + bit;
self.mark_used(frame_num);
self.next_free_hint = frame_num + 1;
return Ok(Frame::from_number(frame_num));
}
}
// Wrap around if needed...
Err(FrameAllocatorError::OutOfMemory)
}
```
**Complexity**: O(n/64) worst case, but typically O(1) due to the hint optimization.
### Allocate Specific Frame
The `allocate_specific()` function allocates a particular physical frame:
```rust
pub fn allocate_specific(&mut self, frame: Frame) -> Result<(), FrameAllocatorError> {
let frame_num = frame.number();
if frame_num >= MAX_FRAMES {
return Err(FrameAllocatorError::InvalidFrame);
}
if self.is_allocated(frame_num) {
return Err(FrameAllocatorError::FrameInUse);
}
self.mark_used(frame_num);
Ok(())
}
```
This is essential for the exokernel design where applications may request specific physical pages for:
- Device MMIO regions
- DMA buffers with specific alignment
- Shared memory at known addresses
**Complexity**: O(1)
### Deallocate Frame
```rust
pub fn deallocate(&mut self, frame: Frame) -> Result<(), FrameAllocatorError> {
let frame_num = frame.number();
self.mark_free(frame_num);
// Update hint for faster future allocations
if frame_num < self.next_free_hint {
self.next_free_hint = frame_num;
}
Ok(())
}
```
**Complexity**: O(1)
## Bit Manipulation
The bitmap operations use efficient bitwise operations:
```rust
// Check if frame is allocated
fn is_allocated(&self, frame_num: usize) -> bool {
let word_idx = frame_num / 64;
let bit_idx = frame_num % 64;
(self.bitmap[word_idx] & (1u64 << bit_idx)) != 0
}
// Mark frame as used
fn mark_used(&mut self, frame_num: usize) {
let word_idx = frame_num / 64;
let bit_idx = frame_num % 64;
self.bitmap[word_idx] |= 1u64 << bit_idx;
}
// Mark frame as free
fn mark_free(&mut self, frame_num: usize) {
let word_idx = frame_num / 64;
let bit_idx = frame_num % 64;
self.bitmap[word_idx] &= !(1u64 << bit_idx);
}
// Find first free bit (0) in a word
fn find_free_bit(&self, word: u64) -> usize {
(!word).trailing_zeros() as usize
}
```
## Global Interface
The allocator is wrapped in a spinlock for thread-safety:
```rust
pub static FRAME_ALLOCATOR: Mutex<FrameAllocator> = Mutex::new(FrameAllocator::new());
```
Convenience functions provide a simple API:
```rust
// Initialize from boot info
pub fn init(boot_info: &BootInfo);
// Allocate a frame
pub fn allocate_frame() -> Result<Frame, FrameAllocatorError>;
// Allocate at specific address
pub fn allocate_frame_at(addr: PhysAddr) -> Result<Frame, FrameAllocatorError>;
// Deallocate a frame
pub fn deallocate_frame(frame: Frame) -> Result<(), FrameAllocatorError>;
// Get statistics
pub fn memory_stats() -> (free_bytes, total_bytes);
```
## Error Handling
```rust
pub enum FrameAllocatorError {
OutOfMemory, // No free frames available
FrameInUse, // Requested specific frame is already allocated
InvalidFrame, // Frame number exceeds MAX_FRAMES
NotInitialized, // Allocator not yet initialized
}
```
## Memory Layout Example
After boot on a system with 512MB RAM:
```
Physical Memory Layout:
0x00000000 - 0x00100000 [Reserved: BIOS, real mode]
0x00100000 - 0x01000000 [Reserved: Kernel + 16MB buffer]
0x01000000 - 0x1FFF0000 [Free: ~495MB available]
0x1FFF0000 - 0x20000000 [ACPI Reclaimable]
Frame Allocator State:
Total frames: ~131,000 (from usable regions)
Free frames: ~126,000 (after kernel reservation)
Free memory: ~494 MB
```
## Source Files
| File | Description |
|------|-------------|
| `src/memory/mod.rs` | Module root, constants, alignment functions |
| `src/memory/frame.rs` | PhysAddr, Frame, FrameAllocator implementation |
## Future Considerations
1. **Buddy Allocator**: For contiguous multi-frame allocations (superpages)
2. **NUMA Awareness**: Track which memory node frames belong to
3. **Memory Zones**: Separate low memory (<4GB) for legacy DMA
4. **Statistics**: Track allocation patterns, fragmentation metrics

542
src/memory/frame.rs Normal file
View File

@@ -0,0 +1,542 @@
//! Physical Frame Allocator
//!
//! This module provides a bitmap-based physical frame allocator for tracking
//! free and used physical memory pages. The allocator is initialized from the
//! boot memory map and supports:
//!
//! - Allocating any free frame
//! - Allocating a specific physical frame (for device MMIO, etc.)
//! - Deallocating frames
//!
//! The exokernel design allows applications to request specific physical pages
//! for resources, so the allocator must support targeted allocation.
use core::fmt;
use crate::boot_info::{BootInfo, MemoryMap, MemoryRegionType};
use super::{PAGE_SIZE, align_down, align_up};
/// Physical address wrapper
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct PhysAddr(u64);
impl PhysAddr {
/// Create a new physical address
#[inline]
pub const fn new(addr: u64) -> Self {
// On x86-64, physical addresses are limited to 52 bits
Self(addr & 0x000F_FFFF_FFFF_FFFF)
}
/// Create a physical address without masking (for known-good addresses)
#[inline]
pub const fn new_unchecked(addr: u64) -> Self {
Self(addr)
}
/// Get the raw address value
#[inline]
pub const fn as_u64(self) -> u64 {
self.0
}
/// Check if the address is page-aligned
#[inline]
pub const fn is_aligned(self) -> bool {
self.0 & (PAGE_SIZE as u64 - 1) == 0
}
/// Align the address down to the nearest page boundary
#[inline]
pub const fn align_down(self) -> Self {
Self(align_down(self.0, PAGE_SIZE as u64))
}
/// Align the address up to the nearest page boundary
#[inline]
pub const fn align_up(self) -> Self {
Self(align_up(self.0, PAGE_SIZE as u64))
}
/// Get the frame containing this address
#[inline]
pub const fn containing_frame(self) -> Frame {
Frame::containing_address(self)
}
}
impl fmt::Debug for PhysAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PhysAddr({:#x})", self.0)
}
}
impl fmt::Display for PhysAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:#x}", self.0)
}
}
impl fmt::LowerHex for PhysAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::LowerHex::fmt(&self.0, f)
}
}
impl fmt::UpperHex for PhysAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::UpperHex::fmt(&self.0, f)
}
}
/// A physical memory frame (4KB page)
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Frame {
/// Frame number (physical address / PAGE_SIZE)
number: usize,
}
impl Frame {
/// Create a frame from a frame number
#[inline]
pub const fn from_number(number: usize) -> Self {
Self { number }
}
/// Create a frame containing the given physical address
#[inline]
pub const fn containing_address(addr: PhysAddr) -> Self {
Self {
number: (addr.as_u64() / PAGE_SIZE as u64) as usize,
}
}
/// Create a frame from a page-aligned physical address
#[inline]
pub const fn from_start_address(addr: PhysAddr) -> Option<Self> {
if addr.is_aligned() {
Some(Self::containing_address(addr))
} else {
None
}
}
/// Get the frame number
#[inline]
pub const fn number(self) -> usize {
self.number
}
/// Get the start address of this frame
#[inline]
pub const fn start_address(self) -> PhysAddr {
PhysAddr::new_unchecked((self.number as u64) * PAGE_SIZE as u64)
}
}
impl fmt::Debug for Frame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Frame({})", self.number)
}
}
/// Errors that can occur during frame allocation
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameAllocatorError {
/// No free frames available
OutOfMemory,
/// Requested frame is already allocated
FrameInUse,
/// Requested frame is outside valid memory
InvalidFrame,
/// Allocator not initialized
NotInitialized,
}
/// Maximum physical memory we support (16 GB = 4M frames)
/// This determines the size of our bitmap
const MAX_PHYSICAL_MEMORY: u64 = 16 * 1024 * 1024 * 1024;
const MAX_FRAMES: usize = (MAX_PHYSICAL_MEMORY / PAGE_SIZE as u64) as usize;
/// Bitmap size in u64 words (each u64 tracks 64 frames)
const BITMAP_WORDS: usize = MAX_FRAMES / 64;
/// Bitmap-based physical frame allocator
///
/// Uses a bitmap where each bit represents one 4KB frame:
/// - 0 = frame is free
/// - 1 = frame is allocated or reserved
///
/// The bitmap is stored in kernel BSS, so it's automatically zeroed at boot.
pub struct FrameAllocator {
/// Bitmap tracking frame usage (1 = used, 0 = free)
bitmap: [u64; BITMAP_WORDS],
/// Total number of frames in the system
total_frames: usize,
/// Number of free frames
free_frames: usize,
/// Whether the allocator has been initialized
initialized: bool,
/// Hint for next allocation search (optimization)
next_free_hint: usize,
}
impl FrameAllocator {
/// Create a new, uninitialized frame allocator
pub const fn new() -> Self {
Self {
bitmap: [0; BITMAP_WORDS],
total_frames: 0,
free_frames: 0,
initialized: false,
next_free_hint: 0,
}
}
/// Initialize the allocator from the boot memory map
///
/// This marks all frames as used initially, then frees the usable regions.
/// This ensures reserved/MMIO regions stay marked as used.
pub fn init(&mut self, memory_map: &MemoryMap) {
// Start with all frames marked as used
for word in self.bitmap.iter_mut() {
*word = !0u64;
}
self.total_frames = 0;
self.free_frames = 0;
// Free the usable memory regions
for region in memory_map.iter() {
if region.region_type == MemoryRegionType::Usable {
let start_frame = Frame::containing_address(PhysAddr::new(region.base));
let end_addr = region.base + region.length;
let end_frame = Frame::containing_address(PhysAddr::new(end_addr));
// Align to frame boundaries (conservative: only free complete frames)
let first_frame = if PhysAddr::new(region.base).is_aligned() {
start_frame.number()
} else {
start_frame.number() + 1
};
let last_frame = end_frame.number();
for frame_num in first_frame..last_frame {
if frame_num < MAX_FRAMES {
self.mark_free(frame_num);
self.total_frames += 1;
self.free_frames += 1;
}
}
}
}
self.initialized = true;
self.next_free_hint = 0;
}
/// Mark frames used by the kernel as allocated
///
/// This should be called after init() to protect kernel memory.
/// The kernel spans from kernel_physical_base for some size.
pub fn reserve_kernel(&mut self, kernel_start: PhysAddr, kernel_size: usize) {
let start_frame = kernel_start.containing_frame().number();
let num_frames = (kernel_size + PAGE_SIZE - 1) / PAGE_SIZE;
for i in 0..num_frames {
let frame_num = start_frame + i;
if frame_num < MAX_FRAMES && !self.is_allocated(frame_num) {
self.mark_used(frame_num);
if self.free_frames > 0 {
self.free_frames -= 1;
}
}
}
}
/// Reserve a specific range of physical addresses
pub fn reserve_range(&mut self, start: PhysAddr, size: usize) {
self.reserve_kernel(start, size);
}
/// Allocate a free frame
///
/// Returns the allocated frame, or an error if no frames are available.
pub fn allocate(&mut self) -> Result<Frame, FrameAllocatorError> {
if !self.initialized {
return Err(FrameAllocatorError::NotInitialized);
}
if self.free_frames == 0 {
return Err(FrameAllocatorError::OutOfMemory);
}
// Search for a free frame starting from the hint
let start_word = self.next_free_hint / 64;
// Search from hint to end
for word_idx in start_word..BITMAP_WORDS {
if self.bitmap[word_idx] != !0u64 {
// This word has at least one free bit
let bit = self.find_free_bit(self.bitmap[word_idx]);
let frame_num = word_idx * 64 + bit;
self.mark_used(frame_num);
self.free_frames -= 1;
self.next_free_hint = frame_num + 1;
return Ok(Frame::from_number(frame_num));
}
}
// Wrap around and search from beginning to hint
for word_idx in 0..start_word {
if self.bitmap[word_idx] != !0u64 {
let bit = self.find_free_bit(self.bitmap[word_idx]);
let frame_num = word_idx * 64 + bit;
self.mark_used(frame_num);
self.free_frames -= 1;
self.next_free_hint = frame_num + 1;
return Ok(Frame::from_number(frame_num));
}
}
Err(FrameAllocatorError::OutOfMemory)
}
/// Allocate a specific physical frame
///
/// This is used when an application requests a specific physical page,
/// such as for device MMIO or DMA buffers.
pub fn allocate_specific(&mut self, frame: Frame) -> Result<(), FrameAllocatorError> {
if !self.initialized {
return Err(FrameAllocatorError::NotInitialized);
}
let frame_num = frame.number();
if frame_num >= MAX_FRAMES {
return Err(FrameAllocatorError::InvalidFrame);
}
if self.is_allocated(frame_num) {
return Err(FrameAllocatorError::FrameInUse);
}
self.mark_used(frame_num);
self.free_frames -= 1;
Ok(())
}
/// Deallocate a frame
pub fn deallocate(&mut self, frame: Frame) -> Result<(), FrameAllocatorError> {
if !self.initialized {
return Err(FrameAllocatorError::NotInitialized);
}
let frame_num = frame.number();
if frame_num >= MAX_FRAMES {
return Err(FrameAllocatorError::InvalidFrame);
}
if !self.is_allocated(frame_num) {
// Double-free is a bug, but we'll just ignore it
return Ok(());
}
self.mark_free(frame_num);
self.free_frames += 1;
// Update hint if this frame is before the current hint
if frame_num < self.next_free_hint {
self.next_free_hint = frame_num;
}
Ok(())
}
/// Get the number of free frames
#[inline]
pub fn free_count(&self) -> usize {
self.free_frames
}
/// Get the total number of usable frames
#[inline]
pub fn total_count(&self) -> usize {
self.total_frames
}
/// Get the amount of free memory in bytes
#[inline]
pub fn free_memory(&self) -> usize {
self.free_frames * PAGE_SIZE
}
/// Get the total amount of usable memory in bytes
#[inline]
pub fn total_memory(&self) -> usize {
self.total_frames * PAGE_SIZE
}
/// Check if a frame is allocated
#[inline]
fn is_allocated(&self, frame_num: usize) -> bool {
let word_idx = frame_num / 64;
let bit_idx = frame_num % 64;
(self.bitmap[word_idx] & (1u64 << bit_idx)) != 0
}
/// Mark a frame as used
#[inline]
fn mark_used(&mut self, frame_num: usize) {
let word_idx = frame_num / 64;
let bit_idx = frame_num % 64;
self.bitmap[word_idx] |= 1u64 << bit_idx;
}
/// Mark a frame as free
#[inline]
fn mark_free(&mut self, frame_num: usize) {
let word_idx = frame_num / 64;
let bit_idx = frame_num % 64;
self.bitmap[word_idx] &= !(1u64 << bit_idx);
}
/// Find the first free bit (0) in a word
#[inline]
fn find_free_bit(&self, word: u64) -> usize {
// Find the first zero bit using bitwise NOT and trailing zeros
(!word).trailing_zeros() as usize
}
}
// Global frame allocator instance
use spin::Mutex;
/// Global frame allocator, protected by a spinlock
pub static FRAME_ALLOCATOR: Mutex<FrameAllocator> = Mutex::new(FrameAllocator::new());
/// Initialize the global frame allocator
pub fn init(boot_info: &BootInfo) {
let mut allocator = FRAME_ALLOCATOR.lock();
allocator.init(&boot_info.memory_map);
// Reserve the kernel's physical memory
// The kernel is loaded at 1MB and spans some amount
// We conservatively reserve 16MB for the kernel and its data structures
allocator.reserve_kernel(
PhysAddr::new(boot_info.kernel_physical_base),
16 * 1024 * 1024,
);
// Reserve the first 1MB (real mode IVT, BIOS data, etc.)
allocator.reserve_range(PhysAddr::new(0), 1024 * 1024);
}
/// Allocate a physical frame from the global allocator
pub fn allocate_frame() -> Result<Frame, FrameAllocatorError> {
FRAME_ALLOCATOR.lock().allocate()
}
/// Allocate a specific physical frame
pub fn allocate_frame_at(addr: PhysAddr) -> Result<Frame, FrameAllocatorError> {
let frame = addr.containing_frame();
FRAME_ALLOCATOR.lock().allocate_specific(frame)?;
Ok(frame)
}
/// Deallocate a physical frame
pub fn deallocate_frame(frame: Frame) -> Result<(), FrameAllocatorError> {
FRAME_ALLOCATOR.lock().deallocate(frame)
}
/// Get current memory statistics
pub fn memory_stats() -> (usize, usize) {
let allocator = FRAME_ALLOCATOR.lock();
(allocator.free_memory(), allocator.total_memory())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::boot_info::MemoryMap;
fn create_test_memory_map() -> MemoryMap {
let mut map = MemoryMap::empty();
// Add some usable memory: 1MB to 128MB
map.add(0x100000, 127 * 1024 * 1024, MemoryRegionType::Usable);
map
}
#[test]
fn test_phys_addr() {
let addr = PhysAddr::new(0x1000);
assert_eq!(addr.as_u64(), 0x1000);
assert!(addr.is_aligned());
let unaligned = PhysAddr::new(0x1234);
assert!(!unaligned.is_aligned());
assert_eq!(unaligned.align_down().as_u64(), 0x1000);
assert_eq!(unaligned.align_up().as_u64(), 0x2000);
}
#[test]
fn test_frame() {
let frame = Frame::from_number(256);
assert_eq!(frame.number(), 256);
assert_eq!(frame.start_address().as_u64(), 256 * 4096);
let frame2 = Frame::containing_address(PhysAddr::new(0x100500));
assert_eq!(frame2.number(), 0x100);
}
#[test]
fn test_allocator_init() {
let mut allocator = FrameAllocator::new();
let map = create_test_memory_map();
allocator.init(&map);
assert!(allocator.initialized);
assert!(allocator.free_frames > 0);
assert!(allocator.total_frames > 0);
}
#[test]
fn test_allocate_deallocate() {
let mut allocator = FrameAllocator::new();
let map = create_test_memory_map();
allocator.init(&map);
let initial_free = allocator.free_count();
// Allocate a frame
let frame = allocator.allocate().unwrap();
assert_eq!(allocator.free_count(), initial_free - 1);
// Deallocate it
allocator.deallocate(frame).unwrap();
assert_eq!(allocator.free_count(), initial_free);
}
#[test]
fn test_allocate_specific() {
let mut allocator = FrameAllocator::new();
let map = create_test_memory_map();
allocator.init(&map);
// Allocate a specific frame in usable memory
let frame = Frame::from_number(512); // 2MB, should be in usable region
allocator.allocate_specific(frame).unwrap();
// Try to allocate it again - should fail
assert_eq!(
allocator.allocate_specific(frame),
Err(FrameAllocatorError::FrameInUse)
);
}
}

34
src/memory/mod.rs Normal file
View File

@@ -0,0 +1,34 @@
//! Memory Management
//!
//! This module provides physical and virtual memory management for the XOmB exokernel.
//! The kernel multiplexes hardware resources through the virtual memory system,
//! so it needs to track and allocate physical pages for page table entries
//! and resource mappings.
//!
//! ## Modules
//!
//! - `frame`: Physical frame allocator (bitmap-based)
//! - `paging`: Page table manipulation using recursive mapping
pub mod frame;
pub mod paging;
pub use frame::{PhysAddr, Frame, FrameAllocator, FrameAllocatorError};
pub use paging::{VirtAddr, PageTableEntry, PageSize, PagingError};
/// Page size constants
pub const PAGE_SIZE: usize = 4096;
pub const PAGE_SIZE_2MB: usize = 2 * 1024 * 1024;
pub const PAGE_SIZE_1GB: usize = 1024 * 1024 * 1024;
/// Align an address down to the nearest page boundary
#[inline]
pub const fn align_down(addr: u64, align: u64) -> u64 {
addr & !(align - 1)
}
/// Align an address up to the nearest page boundary
#[inline]
pub const fn align_up(addr: u64, align: u64) -> u64 {
(addr + align - 1) & !(align - 1)
}

757
src/memory/paging.rs Normal file
View File

@@ -0,0 +1,757 @@
//! Page Table Management
//!
//! This module provides primitives for manipulating x86-64 page tables using
//! the recursive mapping technique. PML4[510] points to the PML4 itself,
//! enabling access to any page table entry through virtual addresses.
//!
//! ## Recursive Mapping
//!
//! With PML4[510] as the self-reference entry:
//! - Recursive region base: 0xFFFF_FF00_0000_0000
//! - Any page table can be accessed by constructing the appropriate virtual address
//!
//! ## Page Table Hierarchy (4-level paging)
//!
//! ```text
//! PML4 (Page Map Level 4) - 512 entries, each covers 512 GB
//! └─► PDPT (Page Dir Ptr) - 512 entries, each covers 1 GB
//! └─► PD (Page Dir) - 512 entries, each covers 2 MB
//! └─► PT (Page Table) - 512 entries, each covers 4 KB
//! ```
use core::fmt;
use crate::memory::frame::{Frame, PhysAddr, allocate_frame, FrameAllocatorError};
/// Recursive mapping PML4 index (PML4[510] points to itself)
pub const RECURSIVE_INDEX: usize = 510;
/// Base virtual address for recursive mapping region
pub const RECURSIVE_BASE: u64 = 0xFFFF_FF00_0000_0000;
/// Number of entries in a page table (all levels)
pub const ENTRIES_PER_TABLE: usize = 512;
/// Page table entry flags
pub mod flags {
/// Page is present in memory
pub const PRESENT: u64 = 1 << 0;
/// Page is writable (otherwise read-only)
pub const WRITABLE: u64 = 1 << 1;
/// Page is accessible from user mode (ring 3)
pub const USER: u64 = 1 << 2;
/// Write-through caching
pub const WRITE_THROUGH: u64 = 1 << 3;
/// Disable caching for this page
pub const NO_CACHE: u64 = 1 << 4;
/// Page has been accessed (set by CPU)
pub const ACCESSED: u64 = 1 << 5;
/// Page has been written to (set by CPU)
pub const DIRTY: u64 = 1 << 6;
/// Huge page (2MB in PD, 1GB in PDPT)
pub const HUGE_PAGE: u64 = 1 << 7;
/// Global page (not flushed on CR3 change)
pub const GLOBAL: u64 = 1 << 8;
/// No execute (requires NXE bit in EFER)
pub const NO_EXECUTE: u64 = 1 << 63;
/// Mask for the physical address in a page table entry
pub const ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000;
/// Default flags for a kernel page table entry (present + writable)
pub const KERNEL_TABLE: u64 = PRESENT | WRITABLE;
/// Default flags for a kernel code page (present + no execute disabled)
pub const KERNEL_CODE: u64 = PRESENT;
/// Default flags for a kernel data page (present + writable + no execute)
pub const KERNEL_DATA: u64 = PRESENT | WRITABLE | NO_EXECUTE;
/// Default flags for user pages
pub const USER_TABLE: u64 = PRESENT | WRITABLE | USER;
pub const USER_CODE: u64 = PRESENT | USER;
pub const USER_DATA: u64 = PRESENT | WRITABLE | USER | NO_EXECUTE;
}
/// A page table entry (64 bits)
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct PageTableEntry(u64);
impl PageTableEntry {
/// Create an empty (non-present) entry
#[inline]
pub const fn empty() -> Self {
Self(0)
}
/// Create an entry with the given frame and flags
#[inline]
pub const fn new(frame: PhysAddr, flags: u64) -> Self {
Self((frame.as_u64() & flags::ADDR_MASK) | flags)
}
/// Get the raw entry value
#[inline]
pub const fn bits(self) -> u64 {
self.0
}
/// Check if the entry is present
#[inline]
pub const fn is_present(self) -> bool {
self.0 & flags::PRESENT != 0
}
/// Check if the entry is writable
#[inline]
pub const fn is_writable(self) -> bool {
self.0 & flags::WRITABLE != 0
}
/// Check if the entry is user-accessible
#[inline]
pub const fn is_user(self) -> bool {
self.0 & flags::USER != 0
}
/// Check if this is a huge page (2MB or 1GB)
#[inline]
pub const fn is_huge(self) -> bool {
self.0 & flags::HUGE_PAGE != 0
}
/// Get the physical address from the entry
#[inline]
pub const fn addr(self) -> PhysAddr {
PhysAddr::new(self.0 & flags::ADDR_MASK)
}
/// Get the frame this entry points to
#[inline]
pub const fn frame(self) -> Frame {
Frame::containing_address(self.addr())
}
/// Get the flags from the entry
#[inline]
pub const fn flags(self) -> u64 {
self.0 & !flags::ADDR_MASK
}
/// Set the flags, preserving the address
#[inline]
pub fn set_flags(&mut self, new_flags: u64) {
self.0 = (self.0 & flags::ADDR_MASK) | new_flags;
}
/// Set the address, preserving the flags
#[inline]
pub fn set_addr(&mut self, addr: PhysAddr) {
self.0 = (addr.as_u64() & flags::ADDR_MASK) | (self.0 & !flags::ADDR_MASK);
}
}
impl fmt::Debug for PageTableEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "PTE({:#x}, ", self.addr())?;
if self.is_present() { write!(f, "P")?; } else { write!(f, "-")?; }
if self.is_writable() { write!(f, "W")?; } else { write!(f, "-")?; }
if self.is_user() { write!(f, "U")?; } else { write!(f, "-")?; }
if self.is_huge() { write!(f, "H")?; } else { write!(f, "-")?; }
write!(f, ")")
}
}
/// Virtual address decomposition for 4-level paging
#[derive(Debug, Clone, Copy)]
pub struct VirtAddr(u64);
impl VirtAddr {
/// Create a new virtual address
#[inline]
pub const fn new(addr: u64) -> Self {
// Sign-extend from bit 47 for canonical addresses
Self(((addr << 16) as i64 >> 16) as u64)
}
/// Get the raw address value
#[inline]
pub const fn as_u64(self) -> u64 {
self.0
}
/// Get the PML4 index (bits 39-47)
#[inline]
pub const fn pml4_index(self) -> usize {
((self.0 >> 39) & 0x1FF) as usize
}
/// Get the PDPT index (bits 30-38)
#[inline]
pub const fn pdpt_index(self) -> usize {
((self.0 >> 30) & 0x1FF) as usize
}
/// Get the PD index (bits 21-29)
#[inline]
pub const fn pd_index(self) -> usize {
((self.0 >> 21) & 0x1FF) as usize
}
/// Get the PT index (bits 12-20)
#[inline]
pub const fn pt_index(self) -> usize {
((self.0 >> 12) & 0x1FF) as usize
}
/// Get the page offset (bits 0-11)
#[inline]
pub const fn page_offset(self) -> usize {
(self.0 & 0xFFF) as usize
}
/// Check if this is a canonical address
#[inline]
pub const fn is_canonical(self) -> bool {
let top_bits = self.0 >> 47;
top_bits == 0 || top_bits == 0x1FFFF
}
}
impl fmt::Display for VirtAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:#x}", self.0)
}
}
impl fmt::LowerHex for VirtAddr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::LowerHex::fmt(&self.0, f)
}
}
/// Page size variants
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageSize {
/// 4 KB page (standard)
Small,
/// 2 MB page (huge page via PD entry)
Large,
/// 1 GB page (huge page via PDPT entry)
Huge,
}
impl PageSize {
/// Get the size in bytes
pub const fn size(self) -> usize {
match self {
PageSize::Small => 4 * 1024, // 4 KB
PageSize::Large => 2 * 1024 * 1024, // 2 MB
PageSize::Huge => 1024 * 1024 * 1024, // 1 GB
}
}
}
/// Errors that can occur during page table operations
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PagingError {
/// Failed to allocate a frame for a new page table
FrameAllocationFailed,
/// The virtual address is not canonical
InvalidAddress,
/// The page is already mapped
AlreadyMapped,
/// The page is not mapped
NotMapped,
/// A parent entry is a huge page (can't traverse further)
HugePageConflict,
}
impl From<FrameAllocatorError> for PagingError {
fn from(_: FrameAllocatorError) -> Self {
PagingError::FrameAllocationFailed
}
}
// ============================================================================
// Recursive Mapping Address Calculations
// ============================================================================
/// Calculate the virtual address to access a PML4 entry via recursive mapping
///
/// Formula: 0xFFFFFF7FBFDFE000 + (pml4_idx * 8)
#[inline]
pub fn pml4_entry_addr(pml4_idx: usize) -> *mut PageTableEntry {
const PML4_BASE: u64 = 0xFFFF_FF7F_BFDF_E000;
(PML4_BASE + (pml4_idx as u64) * 8) as *mut PageTableEntry
}
/// Calculate the virtual address to access a PDPT entry via recursive mapping
///
/// Formula: 0xFFFFFF7FBFC00000 + (pml4_idx * 0x1000) + (pdpt_idx * 8)
#[inline]
pub fn pdpt_entry_addr(pml4_idx: usize, pdpt_idx: usize) -> *mut PageTableEntry {
const PDPT_BASE: u64 = 0xFFFF_FF7F_BFC0_0000;
(PDPT_BASE + (pml4_idx as u64) * 0x1000 + (pdpt_idx as u64) * 8) as *mut PageTableEntry
}
/// Calculate the virtual address to access a PD entry via recursive mapping
///
/// Formula: 0xFFFFFF7F80000000 + (pml4_idx * 0x200000) + (pdpt_idx * 0x1000) + (pd_idx * 8)
#[inline]
pub fn pd_entry_addr(pml4_idx: usize, pdpt_idx: usize, pd_idx: usize) -> *mut PageTableEntry {
const PD_BASE: u64 = 0xFFFF_FF7F_8000_0000;
(PD_BASE
+ (pml4_idx as u64) * 0x20_0000
+ (pdpt_idx as u64) * 0x1000
+ (pd_idx as u64) * 8) as *mut PageTableEntry
}
/// Calculate the virtual address to access a PT entry via recursive mapping
///
/// Formula: 0xFFFFFF0000000000 + (pml4_idx * 0x40000000) + (pdpt_idx * 0x200000)
/// + (pd_idx * 0x1000) + (pt_idx * 8)
#[inline]
pub fn pt_entry_addr(pml4_idx: usize, pdpt_idx: usize, pd_idx: usize, pt_idx: usize) -> *mut PageTableEntry {
const PT_BASE: u64 = 0xFFFF_FF00_0000_0000;
(PT_BASE
+ (pml4_idx as u64) * 0x4000_0000
+ (pdpt_idx as u64) * 0x20_0000
+ (pd_idx as u64) * 0x1000
+ (pt_idx as u64) * 8) as *mut PageTableEntry
}
/// Calculate the virtual address of a PDPT page via recursive mapping
///
/// After PML4[pml4_idx] is set, this address accesses the entire PDPT page.
#[inline]
fn pdpt_table_addr(pml4_idx: usize) -> *mut u64 {
const PDPT_BASE: u64 = 0xFFFF_FF7F_BFC0_0000;
(PDPT_BASE + (pml4_idx as u64) * 0x1000) as *mut u64
}
/// Calculate the virtual address of a PD page via recursive mapping
///
/// After PDPT[pml4_idx][pdpt_idx] is set, this address accesses the entire PD page.
#[inline]
fn pd_table_addr(pml4_idx: usize, pdpt_idx: usize) -> *mut u64 {
const PD_BASE: u64 = 0xFFFF_FF7F_8000_0000;
(PD_BASE + (pml4_idx as u64) * 0x20_0000 + (pdpt_idx as u64) * 0x1000) as *mut u64
}
/// Calculate the virtual address of a PT page via recursive mapping
///
/// After PD[pml4_idx][pdpt_idx][pd_idx] is set, this address accesses the entire PT page.
#[inline]
fn pt_table_addr(pml4_idx: usize, pdpt_idx: usize, pd_idx: usize) -> *mut u64 {
const PT_BASE: u64 = 0xFFFF_FF00_0000_0000;
(PT_BASE
+ (pml4_idx as u64) * 0x4000_0000
+ (pdpt_idx as u64) * 0x20_0000
+ (pd_idx as u64) * 0x1000) as *mut u64
}
// ============================================================================
// Page Table Entry Access
// ============================================================================
/// Read a PML4 entry
#[inline]
pub fn read_pml4(pml4_idx: usize) -> PageTableEntry {
unsafe { core::ptr::read_volatile(pml4_entry_addr(pml4_idx)) }
}
/// Write a PML4 entry
pub fn write_pml4(pml4_idx: usize, entry: PageTableEntry) {
unsafe {
let ptr = pml4_entry_addr(pml4_idx);
core::ptr::write_volatile(ptr, entry);
core::arch::asm!("mfence", options(nostack, preserves_flags));
}
}
/// Read a PDPT entry (requires PML4 entry to be present)
#[inline]
pub fn read_pdpt(pml4_idx: usize, pdpt_idx: usize) -> PageTableEntry {
unsafe { *pdpt_entry_addr(pml4_idx, pdpt_idx) }
}
/// Write a PDPT entry
#[inline]
pub fn write_pdpt(pml4_idx: usize, pdpt_idx: usize, entry: PageTableEntry) {
unsafe { *pdpt_entry_addr(pml4_idx, pdpt_idx) = entry; }
}
/// Read a PD entry (requires PML4 and PDPT entries to be present)
#[inline]
pub fn read_pd(pml4_idx: usize, pdpt_idx: usize, pd_idx: usize) -> PageTableEntry {
unsafe { *pd_entry_addr(pml4_idx, pdpt_idx, pd_idx) }
}
/// Write a PD entry
#[inline]
pub fn write_pd(pml4_idx: usize, pdpt_idx: usize, pd_idx: usize, entry: PageTableEntry) {
unsafe { *pd_entry_addr(pml4_idx, pdpt_idx, pd_idx) = entry; }
}
/// Read a PT entry (requires PML4, PDPT, and PD entries to be present)
#[inline]
pub fn read_pt(pml4_idx: usize, pdpt_idx: usize, pd_idx: usize, pt_idx: usize) -> PageTableEntry {
unsafe { *pt_entry_addr(pml4_idx, pdpt_idx, pd_idx, pt_idx) }
}
/// Write a PT entry
#[inline]
pub fn write_pt(pml4_idx: usize, pdpt_idx: usize, pd_idx: usize, pt_idx: usize, entry: PageTableEntry) {
unsafe { *pt_entry_addr(pml4_idx, pdpt_idx, pd_idx, pt_idx) = entry; }
}
// ============================================================================
// TLB Management
// ============================================================================
/// Invalidate a single TLB entry for the given virtual address
#[inline]
pub fn invalidate_page(addr: VirtAddr) {
unsafe {
core::arch::asm!("invlpg [{}]", in(reg) addr.as_u64(), options(nostack, preserves_flags));
}
}
/// Flush the entire TLB by reloading CR3
#[inline]
pub fn flush_tlb() {
unsafe {
// Memory barrier to ensure all prior writes are visible
core::arch::asm!("mfence", options(nostack, preserves_flags));
let cr3: u64;
core::arch::asm!("mov {}, cr3", out(reg) cr3, options(nostack, preserves_flags));
core::arch::asm!("mov cr3, {}", in(reg) cr3, options(nostack, preserves_flags));
}
}
// ============================================================================
// Page Table Creation and Mapping
// ============================================================================
/// Ensure a PML4 entry exists, creating a PDPT if necessary
fn ensure_pml4_entry(pml4_idx: usize, _flags: u64) -> Result<(), PagingError> {
let entry = read_pml4(pml4_idx);
if !entry.is_present() {
let frame = allocate_frame()?;
let phys = frame.start_address();
// Link the new PDPT into the PML4 first
// Use only table flags (PRESENT | WRITABLE) for intermediate entries
let table_flags = flags::PRESENT | flags::WRITABLE;
let new_entry = PageTableEntry::new(phys, table_flags);
write_pml4(pml4_idx, new_entry);
// Flush TLB so we can access the new PDPT via recursive mapping
flush_tlb();
// Zero the new page table via recursive mapping
// Now that PML4[pml4_idx] is set, pdpt_table_addr gives us access
zero_page_table(pdpt_table_addr(pml4_idx));
}
Ok(())
}
/// Ensure a PDPT entry exists, creating a PD if necessary
fn ensure_pdpt_entry(pml4_idx: usize, pdpt_idx: usize, flags: u64) -> Result<(), PagingError> {
ensure_pml4_entry(pml4_idx, flags)?;
let entry = read_pdpt(pml4_idx, pdpt_idx);
if entry.is_huge() {
return Err(PagingError::HugePageConflict);
}
if !entry.is_present() {
let frame = allocate_frame()?;
let phys = frame.start_address();
// Link the new PD into the PDPT first
// Use only table flags for intermediate entries
let table_flags = flags::PRESENT | flags::WRITABLE;
let new_entry = PageTableEntry::new(phys, table_flags);
write_pdpt(pml4_idx, pdpt_idx, new_entry);
// Flush TLB so we can access the new PD via recursive mapping
flush_tlb();
// Zero the new page table via recursive mapping
zero_page_table(pd_table_addr(pml4_idx, pdpt_idx));
}
Ok(())
}
/// Ensure a PD entry exists, creating a PT if necessary
fn ensure_pd_entry(pml4_idx: usize, pdpt_idx: usize, pd_idx: usize, flags: u64) -> Result<(), PagingError> {
ensure_pdpt_entry(pml4_idx, pdpt_idx, flags)?;
let entry = read_pd(pml4_idx, pdpt_idx, pd_idx);
if entry.is_huge() {
return Err(PagingError::HugePageConflict);
}
if !entry.is_present() {
let frame = allocate_frame()?;
let phys = frame.start_address();
// Link the new PT into the PD first
// Use only table flags for intermediate entries
let table_flags = flags::PRESENT | flags::WRITABLE;
let new_entry = PageTableEntry::new(phys, table_flags);
write_pd(pml4_idx, pdpt_idx, pd_idx, new_entry);
// Flush TLB so we can access the new PT via recursive mapping
flush_tlb();
// Zero the new page table via recursive mapping
zero_page_table(pt_table_addr(pml4_idx, pdpt_idx, pd_idx));
}
Ok(())
}
/// Zero a page table at the given virtual address
///
/// The page must already be mapped (accessible via the given address).
/// This writes 512 zero entries (4096 bytes total).
fn zero_page_table(virt_addr: *mut u64) {
unsafe {
for i in 0..512 {
core::ptr::write_volatile(virt_addr.add(i), 0);
}
}
}
/// Map a 4KB page
pub fn map_4kb(virt: VirtAddr, phys: PhysAddr, flags: u64) -> Result<(), PagingError> {
if !virt.is_canonical() {
return Err(PagingError::InvalidAddress);
}
let pml4_idx = virt.pml4_index();
let pdpt_idx = virt.pdpt_index();
let pd_idx = virt.pd_index();
let pt_idx = virt.pt_index();
// Ensure all parent tables exist
ensure_pd_entry(pml4_idx, pdpt_idx, pd_idx, flags)?;
// Check if already mapped
let existing = read_pt(pml4_idx, pdpt_idx, pd_idx, pt_idx);
if existing.is_present() {
return Err(PagingError::AlreadyMapped);
}
// Create the mapping
let entry = PageTableEntry::new(phys, flags | flags::PRESENT);
write_pt(pml4_idx, pdpt_idx, pd_idx, pt_idx, entry);
// Invalidate TLB for this address
invalidate_page(virt);
Ok(())
}
/// Map a 2MB huge page
pub fn map_2mb(virt: VirtAddr, phys: PhysAddr, flags: u64) -> Result<(), PagingError> {
if !virt.is_canonical() {
return Err(PagingError::InvalidAddress);
}
// Virtual address must be 2MB aligned
if virt.as_u64() & 0x1FFFFF != 0 {
return Err(PagingError::InvalidAddress);
}
let pml4_idx = virt.pml4_index();
let pdpt_idx = virt.pdpt_index();
let pd_idx = virt.pd_index();
// Ensure PML4 and PDPT entries exist
ensure_pdpt_entry(pml4_idx, pdpt_idx, flags)?;
// Check if already mapped
let existing = read_pd(pml4_idx, pdpt_idx, pd_idx);
if existing.is_present() {
return Err(PagingError::AlreadyMapped);
}
// Create the huge page mapping
let entry = PageTableEntry::new(phys, flags | flags::PRESENT | flags::HUGE_PAGE);
write_pd(pml4_idx, pdpt_idx, pd_idx, entry);
// Invalidate TLB
invalidate_page(virt);
Ok(())
}
/// Map a 1GB huge page
pub fn map_1gb(virt: VirtAddr, phys: PhysAddr, flags: u64) -> Result<(), PagingError> {
if !virt.is_canonical() {
return Err(PagingError::InvalidAddress);
}
// Virtual address must be 1GB aligned
if virt.as_u64() & 0x3FFFFFFF != 0 {
return Err(PagingError::InvalidAddress);
}
let pml4_idx = virt.pml4_index();
let pdpt_idx = virt.pdpt_index();
// Ensure PML4 entry exists
ensure_pml4_entry(pml4_idx, flags)?;
// Check if already mapped
let existing = read_pdpt(pml4_idx, pdpt_idx);
if existing.is_present() {
return Err(PagingError::AlreadyMapped);
}
// Create the huge page mapping
let entry = PageTableEntry::new(phys, flags | flags::PRESENT | flags::HUGE_PAGE);
write_pdpt(pml4_idx, pdpt_idx, entry);
// Invalidate TLB
invalidate_page(virt);
Ok(())
}
/// Unmap a 4KB page, returning the physical frame if it was mapped
pub fn unmap_4kb(virt: VirtAddr) -> Result<Frame, PagingError> {
if !virt.is_canonical() {
return Err(PagingError::InvalidAddress);
}
let pml4_idx = virt.pml4_index();
let pdpt_idx = virt.pdpt_index();
let pd_idx = virt.pd_index();
let pt_idx = virt.pt_index();
// Walk the page table hierarchy
let pml4_entry = read_pml4(pml4_idx);
if !pml4_entry.is_present() {
return Err(PagingError::NotMapped);
}
let pdpt_entry = read_pdpt(pml4_idx, pdpt_idx);
if !pdpt_entry.is_present() {
return Err(PagingError::NotMapped);
}
if pdpt_entry.is_huge() {
return Err(PagingError::HugePageConflict);
}
let pd_entry = read_pd(pml4_idx, pdpt_idx, pd_idx);
if !pd_entry.is_present() {
return Err(PagingError::NotMapped);
}
if pd_entry.is_huge() {
return Err(PagingError::HugePageConflict);
}
let pt_entry = read_pt(pml4_idx, pdpt_idx, pd_idx, pt_idx);
if !pt_entry.is_present() {
return Err(PagingError::NotMapped);
}
let frame = pt_entry.frame();
// Clear the entry
write_pt(pml4_idx, pdpt_idx, pd_idx, pt_idx, PageTableEntry::empty());
// Invalidate TLB
invalidate_page(virt);
Ok(frame)
}
/// Translate a virtual address to a physical address
pub fn translate(virt: VirtAddr) -> Option<PhysAddr> {
if !virt.is_canonical() {
return None;
}
let pml4_idx = virt.pml4_index();
let pdpt_idx = virt.pdpt_index();
let pd_idx = virt.pd_index();
let pt_idx = virt.pt_index();
let offset = virt.page_offset();
// Walk the page table hierarchy
let pml4_entry = read_pml4(pml4_idx);
if !pml4_entry.is_present() {
return None;
}
let pdpt_entry = read_pdpt(pml4_idx, pdpt_idx);
if !pdpt_entry.is_present() {
return None;
}
if pdpt_entry.is_huge() {
// 1GB page
let base = pdpt_entry.addr().as_u64();
let page_offset = virt.as_u64() & 0x3FFFFFFF; // Lower 30 bits
return Some(PhysAddr::new(base + page_offset));
}
let pd_entry = read_pd(pml4_idx, pdpt_idx, pd_idx);
if !pd_entry.is_present() {
return None;
}
if pd_entry.is_huge() {
// 2MB page
let base = pd_entry.addr().as_u64();
let page_offset = virt.as_u64() & 0x1FFFFF; // Lower 21 bits
return Some(PhysAddr::new(base + page_offset));
}
let pt_entry = read_pt(pml4_idx, pdpt_idx, pd_idx, pt_idx);
if !pt_entry.is_present() {
return None;
}
// 4KB page
let base = pt_entry.addr().as_u64();
Some(PhysAddr::new(base + offset as u64))
}
/// Get information about the mapping at a virtual address
pub fn get_mapping_info(virt: VirtAddr) -> Option<(PhysAddr, PageSize, u64)> {
if !virt.is_canonical() {
return None;
}
let pml4_idx = virt.pml4_index();
let pdpt_idx = virt.pdpt_index();
let pd_idx = virt.pd_index();
let pt_idx = virt.pt_index();
let pml4_entry = read_pml4(pml4_idx);
if !pml4_entry.is_present() {
return None;
}
let pdpt_entry = read_pdpt(pml4_idx, pdpt_idx);
if !pdpt_entry.is_present() {
return None;
}
if pdpt_entry.is_huge() {
return Some((pdpt_entry.addr(), PageSize::Huge, pdpt_entry.flags()));
}
let pd_entry = read_pd(pml4_idx, pdpt_idx, pd_idx);
if !pd_entry.is_present() {
return None;
}
if pd_entry.is_huge() {
return Some((pd_entry.addr(), PageSize::Large, pd_entry.flags()));
}
let pt_entry = read_pt(pml4_idx, pdpt_idx, pd_idx, pt_idx);
if !pt_entry.is_present() {
return None;
}
Some((pt_entry.addr(), PageSize::Small, pt_entry.flags()))
}

31
src/multiboot2_main.rs Normal file
View File

@@ -0,0 +1,31 @@
//! XOmB Multiboot2 Entry Point
//!
//! This is the Rust entry point for multiboot2 boot (used by Bochs/GRUB).
//! The actual entry is in assembly (boot/multiboot2_header.s), which then
//! calls into this code.
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use core::fmt::Write;
// Re-export the library
use xomb::serial::SerialPort;
// Pull in the multiboot2 entry point
pub use xomb::boot::multiboot2::multiboot2_entry;
/// Panic handler for multiboot2 boot
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
let mut serial = unsafe { SerialPort::new(0x3F8) };
let _ = writeln!(serial, "\n!!! KERNEL PANIC !!!");
let _ = writeln!(serial, "{}", info);
loop {
unsafe {
core::arch::asm!("cli; hlt", options(nostack, nomem));
}
}
}

164
src/serial.rs Normal file
View File

@@ -0,0 +1,164 @@
//! Serial port driver for debugging output
//!
//! Provides a simple UART driver for COM1 (0x3F8) that works
//! both during UEFI boot and after ExitBootServices.
use core::fmt::{self, Write};
/// Standard PC serial port (8250/16550 UART)
pub struct SerialPort {
port: u16,
}
impl SerialPort {
/// Create a new serial port instance
///
/// # Safety
/// The caller must ensure the port address is valid and not in use.
pub const unsafe fn new(port: u16) -> Self {
Self { port }
}
/// Initialize the serial port with standard settings
/// 115200 baud, 8N1
pub fn init(&mut self) {
unsafe {
// Disable interrupts
self.outb(self.port + 1, 0x00);
// Enable DLAB (set baud rate divisor)
self.outb(self.port + 3, 0x80);
// Set divisor to 1 (115200 baud)
self.outb(self.port + 0, 0x01); // Low byte
self.outb(self.port + 1, 0x00); // High byte
// 8 bits, no parity, one stop bit (8N1)
self.outb(self.port + 3, 0x03);
// Enable FIFO, clear them, with 14-byte threshold
self.outb(self.port + 2, 0xC7);
// Enable IRQs, RTS/DSR set
self.outb(self.port + 4, 0x0B);
// Set in loopback mode, test the serial chip
self.outb(self.port + 4, 0x1E);
// Test serial chip (send byte 0xAE and check if it returns same byte)
self.outb(self.port + 0, 0xAE);
// Check if serial is faulty (i.e., not the same byte as sent)
if self.inb(self.port + 0) != 0xAE {
return; // Serial port is faulty, but we continue anyway
}
// If serial is not faulty, set it in normal operation mode
// (not loopback, IRQs enabled, OUT#1 and OUT#2 bits enabled)
self.outb(self.port + 4, 0x0F);
}
}
/// Check if the transmit buffer is empty
fn is_transmit_empty(&self) -> bool {
unsafe { self.inb(self.port + 5) & 0x20 != 0 }
}
/// Write a single byte to the serial port
pub fn write_byte(&mut self, byte: u8) {
// Wait for transmit buffer to be empty
while !self.is_transmit_empty() {
core::hint::spin_loop();
}
unsafe {
self.outb(self.port, byte);
}
}
/// Read a single byte from the serial port (blocking)
pub fn read_byte(&self) -> u8 {
// Wait for data to be available
while !self.has_data() {
core::hint::spin_loop();
}
unsafe { self.inb(self.port) }
}
/// Check if data is available to read
pub fn has_data(&self) -> bool {
unsafe { self.inb(self.port + 5) & 0x01 != 0 }
}
/// Try to read a byte without blocking
pub fn try_read_byte(&self) -> Option<u8> {
if self.has_data() {
Some(unsafe { self.inb(self.port) })
} else {
None
}
}
#[inline]
unsafe fn outb(&self, port: u16, val: u8) {
unsafe {
core::arch::asm!("out dx, al", in("dx") port, in("al") val, options(nostack, preserves_flags));
}
}
#[inline]
unsafe fn inb(&self, port: u16) -> u8 {
let ret: u8;
unsafe {
core::arch::asm!("in al, dx", out("al") ret, in("dx") port, options(nostack, preserves_flags));
}
ret
}
}
impl Write for SerialPort {
fn write_str(&mut self, s: &str) -> fmt::Result {
for byte in s.bytes() {
if byte == b'\n' {
self.write_byte(b'\r');
}
self.write_byte(byte);
}
Ok(())
}
}
/// Global serial port instance for debug logging
///
/// # Safety
/// This is safe to use from a single thread or with proper synchronization.
#[macro_export]
macro_rules! serial_print {
($($arg:tt)*) => {{
use core::fmt::Write;
let mut serial = unsafe { $crate::serial::SerialPort::new(0x3F8) };
write!(serial, $($arg)*).ok();
}};
}
#[macro_export]
macro_rules! serial_println {
() => ($crate::serial_print!("\n"));
($($arg:tt)*) => {{
$crate::serial_print!($($arg)*);
$crate::serial_print!("\n");
}};
}
#[cfg(test)]
mod tests {
// Serial port tests would require mocking the I/O ports
// These are better suited for integration tests in the emulator
#[test]
fn test_serial_port_creation() {
// Just verify the struct can be created
let _port = unsafe { super::SerialPort::new(0x3F8) };
}
}

25
x86_64-xomb.json Normal file
View File

@@ -0,0 +1,25 @@
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse3,-ssse3,-sse4.1,-sse4.2,-avx,-avx2",
"code-model": "kernel",
"relocation-model": "static",
"position-independent-executables": false,
"static-position-independent-executables": false,
"pre-link-args": {
"ld.lld": [
"--gc-sections",
"-zmax-page-size=0x1000"
]
}
}