This document provides a comprehensive deep-dive into the design, architecture, and implementation details of Valhalla OS. It describes the system from the bootloader up to the user shell, explaining the design choices and technical hurdles overcome during development.
Valhalla OS is a 64-bit micro-kernel written in Rust. It runs on the x86_64 architecture and leverages specific hardware features like the Global Descriptor Table (GDT), Interrupt Descriptor Table (IDT), and Paging.
- Language: Rust (Nightly,
no_std) - Bootloader:
bootloadercrate (v0.9.x spec) - Architecture: Monolithic Kernel design (currently), aiming for micro-kernel characteristics.
- Build System: Cargo with
bootimagefor disk creation.
This layer handles the most basic interactions with the hardware, allowing the kernel to output information and survive basic crashes.
The kernel communicates with the user primarily through the VGA text buffer, a memory-mapped I/O region located at physical address 0xb8000.
- Memory-Mapped I/O: The buffer consists of 25 rows and 80 columns. Each screen character takes up 2 bytes: one for the ASCII character and one for the color attribute (foreground/background).
- Volatile Access: To prevent the Rust compiler from optimizing away memory writes (since it doesn't know about side effects on hardware), the
volatilecrate is used to wrap the buffer. - Concurrency: A global static
WRITERis protected by aspin::Mutex. Since the OS cannot use standard OS threads or mutexes yet, a spinlock is used to ensure that only one core (or interrupt handler) writes to the screen at a time. - Macros:
print!andprintln!are implemented by hooking into the global writer, providing a seamless high-level interface.
While the GDT is required for 64-bit mode, Valhalla OS utilizes it specifically for Stack Overflow Protection.
- The Double Fault Problem: If the kernel overflows the stack, it triggers a page fault. If the CPU cannot push the page fault frame onto the full stack, it triggers a Double Fault. If the Double Fault handler cannot run (because the stack is still full), the CPU Triple Faults and reboots.
- The Solution (IST): We set up an Interrupt Stack Table (IST) in the Task State Segment (TSS).
- Implementation:
- A dedicated stack is allocated in
gdt.rs. - This stack is registered in the IST at index
DOUBLE_FAULT_IST_INDEX. - The IDT entry for Double Faults is configured to physically switch the CPU to this fresh stack when the exception occurs, allowing the kernel to catch the panic gracefully.
- A dedicated stack is allocated in
This layer allows the OS to respond to asynchronous hardware events and CPU exceptions.
The IDT maps exception vectors to function handlers.
- Exceptions:
- Breakpoint: Used for unit testing (pauses execution).
- Page Fault: Critical for debugging memory issues. accessible via
CR2register. - Double Fault: As described above, handled on a separate stack.
- Calling Convention: Handlers use the
x86-interruptcalling convention, which saves all registers (not just caller-saved ones) to prevent corrupting the state of the interrupted process.
To handle external devices like the Timer and Keyboard, we us the 8259 Programmable Interrupt Controller (PIC).
- Remapping: By default, the PIC maps interrupts to ranges 0-15, contradicting CPU exceptions. We remap the generic interrupts to offsets 32-47.
PIC_1_OFFSET: 32
- Timer: defaults to ~18.2Hz (or configured otherwise). The handler sends an "End of Interrupt" (EOI) signal to the PIC to confirm processing.
Keyboard input is processed via Port I/O 0x60.
- Scancodes: The keyboard sends raw bytes (scancodes). We use
pc-keyboardto decode Scancode Set 1. - Processing:
- Interrupt fires ->
keyboard_interrupt_handlerreads byte. layouts::Us104Keymaps scancode to Char.- Character is pushed to
COMMAND_BUFFER. - If
\n(Enter) is pressed,COMMAND_READYflag is set to notify the shell.
- Interrupt fires ->
This is the most complex part of the kernel ("The Crown Jewel"), transforming raw physical RAM into a safe, dynamic environment.
- 4-Level Paging: The OS uses the standard x86_64 4-level page table structure.
- Offset Page Table: The bootloader maps the entire physical memory to a virtual address range.
- If physical memory is at
0x0, and offset is0xFFFF_8000_0000_0000, the kernel can access physical0x1000at virtual0xFFFF_8000_0000_1000. - This allows the kernel to easily edit page tables since it can "reach" the physical frames from virtual space.
- If physical memory is at
To create new page tables or map new memory, we need physical frames (4KiB chunks).
- Strategy:
BootInfoFrameAllocatorreads the memory map passed by the BIOS/UEFI. - Logic: It iterates through regions marked
Usable. It returns the address of the next available frame and increments a counter. This is a linear allocator; it does not yet support freeing frames (leaks memory if pages are unmapped).
To support standard Rust types like Vec, Box, and String, we implement a heap.
- Heap Address: Fixed at specific virtual address
0x_4444_4444_0000. - Heap Size: 100 KiB.
- Allocator: We use the
linked_list_allocatorcrate.- The
init_heapfunction manually maps the 100 KiB virtual range to new physical frames allocated by our Frame Allocator. - It then initializes the
LockedHeapglobal allocator, allowing safeno_stddynamic memory usage.
- The
Developing a bare-metal OS involves resolving unique errors that don't exist in standard software development.
| Issue | Technical Root Cause | Solution Implemented |
|---|---|---|
| Compiler Error: E0423 | ScancodeSet1 is a Unit Struct (zero-sized type) in the pc-keyboard crate, but text was treating it like a value. |
Explicitly instantiated it: ScancodeSet1::new() or ScancodeSet1 {}. |
| Bootloader Field Missing | The bootloader crate v0.10+ dramatically changed the BootInfo struct, removing physical_memory_offset. |
We pinned dependency to 0.9.23 and enabled the map_physical_memory feature in Cargo.toml. |
| Linker Error: Alloc | The alloc crate is part of the standard library but not linked by default in no_std. |
Added alloc to the build-std array in .cargo/config.toml to recompile it for our custom target. |
| Pointer Casting Panic | The heap initializer expects a raw pointer *mut u8 but we had a usize address. |
Performed an explicit cast HEAP_START as *mut u8. |
| Triple Fault Loops | Occurred early on when IDT entries were malformed or the Stack Overflow handler failed. | Defining a robust Double Fault Handler with a dedicated IST stack prevented the CPU from resetting, allowing us to see the panic message. |
The user interface (src/shell.rs) is a simple loop running in main.rs.
- Architecture: Event-driven. The main loop sleeps (
hlt) until an interrupt (Keyboard) wakes it. - Commands:
ping: Trivial test of responsiveness.heap_test: Critical test. It allocates aBox<i32>and prints the pointer. If memory management failed, this would crash the kernel.clear: Simulates a screen clear by printing newlines.