mirror of
https://github.com/xomboverlord/xomb.git
synced 2026-01-10 01:36:37 +01:00
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:
21
.cargo/config.toml
Normal file
21
.cargo/config.toml
Normal 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
36
.gitignore
vendored
Normal 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
180
Cargo.lock
generated
Normal 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
53
Cargo.toml
Normal 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
248
Makefile
Normal 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
56
bochsrc.txt
Normal 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
11
boot/grub/grub.cfg
Normal 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
56
build.rs
Normal 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
27
docs/MAIN.md
Normal 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
5
docs/MEMORY.md
Normal 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
180
docs/tasks/STAGE_1.md
Normal 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
95
linker-multiboot2.ld
Normal 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
13
rust-toolchain.toml
Normal 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
9
src/arch/mod.rs
Normal 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
91
src/arch/x86_64/mod.rs
Normal 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
7
src/boot/mod.rs
Normal 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
309
src/boot/multiboot2.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
241
src/boot/multiboot2_header.asm
Normal file
241
src/boot/multiboot2_header.asm
Normal 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
246
src/boot_info.rs
Normal 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
231
src/lib.rs
Normal 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
164
src/main.rs
Normal 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
314
src/memory/README.md
Normal 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
542
src/memory/frame.rs
Normal 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
34
src/memory/mod.rs
Normal 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
757
src/memory/paging.rs
Normal 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
31
src/multiboot2_main.rs
Normal 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
164
src/serial.rs
Normal 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
25
x86_64-xomb.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user