9.2 KiB
Simple UEFI Operating System
A minimal bootable UEFI operating system written in C that demonstrates UEFI boot loading, memory management, interrupt handling, and cooperative multitasking.
Features
- UEFI Boot: Boots directly on UEFI firmware via a PE32+ loader
- ELF64 Kernel Loader: Reads and maps a standalone kernel from the EFI partition
- Console I/O: Interactive keyboard input and screen output
- Interrupt Handling: IDT setup with CPU exception handlers (vectors 0–31) and hardware IRQ dispatch
- Memory Management: Bitmap-based physical memory manager (PMM), 4-level x86-64 paging, and a first-fit heap allocator with block coalescing
- Cooperative Multitasking: Process control blocks, assembly context switching, and a round-robin task scheduler
- Interactive Shell: Command registry with
help,man, built-in commands, and test utilities
Requirements
- GCC compiler
- GNU-EFI library
- QEMU with OVMF firmware
- Make
Installation
On Debian/Ubuntu:
make install-deps
Or manually:
sudo apt-get install gnu-efi qemu-system-x86 ovmf gcc binutils make xorriso mtools
On Arch Linux:
sudo pacman -S gnu-efi qemu-system-x86 edk2-ovmf gcc binutils make xorriso mtools
On Fedora/RHEL:
sudo dnf install gnu-efi qemu-system-x86 edk2-ovmf gcc binutils make xorriso mtools
Building
Build the UEFI loader and kernel:
make
This creates build/EFI/BOOT/BOOTX64.EFI (UEFI loader) and build/kernel.elf (kernel binary).
Running
Test with QEMU (no graphics mode):
make run
To build and run from an ISO image:
make runiso
The loader expects kernel.elf at the root of the EFI partition (next to the EFI/ directory).
QEMU Controls:
- Exit QEMU: Press
Ctrl+A, thenX - Shutdown OS: Type
shutdownin the OS
Commands
Once the OS boots, you can use these commands:
| Command | Description |
|---|---|
help |
Display all available commands |
man <command> |
Display detailed help for a command |
shutdown |
Shutdown the system |
clear |
Clear the screen |
about |
Display system information |
mem |
Display memory statistics (PMM, heap, paging) |
ps |
List all active tasks with PID, state, and name |
spawn [name] |
Create a demo background task |
memtest |
Run memory allocation/deallocation tests |
tasktest |
Spawn multiple concurrent tasks to test the scheduler |
Example Session
-> help
-> about
-> mem
-> memtest
-> spawn worker1
-> spawn worker2
-> ps
-> tasktest
-> shutdown
Testing Memory Management
The memtest command runs through four test phases:
- Heap allocation — allocates 8 blocks of increasing size (16 to 4096 bytes) via
kmalloc() - Heap free — frees all allocated blocks via
kfree()and verifies coalescing - PMM single page — allocates and frees a single 4 KB page
- PMM multi-page — allocates and frees 4 contiguous pages
Each phase reports addresses, success/failure, and usage statistics.
Testing Task Scheduling
The tasktest command:
- Spawns 3 worker tasks (
worker-A,worker-B,worker-C) - Each worker prints 3 progress steps, yielding between each
- The shell yields to let workers run in round-robin order
- Displays the task list after completion
You can also manually test with spawn and ps:
-> spawn myTask
Created task 'myTask' (PID 1)
-> ps
PID STATE SWITCHES NAME
--- ---------- -------- ----
0 RUNNING 5 kernel
1 READY 0 myTask
Active tasks: 2 / 32
Background tasks run whenever the shell yields (which happens automatically while waiting for keyboard input).
Project Structure
.
├── main.c # UEFI loader (reads and loads kernel ELF)
├── kernel.c # Kernel entry point and interactive shell loop
├── kernel.ld # Kernel linker script (base address 0x100000)
├── boot_info.h # Shared boot interface (BootInfo struct)
├── commands.c # Command registry and handler implementations
├── commands.h # Command interface definitions
├── string_utils.c # String utility functions (trim, compare, etc.)
├── string_utils.h # String utility declarations
├── idt.c # IDT setup, exception handlers, PIC EOI
├── idt.h # IDT types (ISRFrame) and init declaration
├── isr.S # ISR stub table (vectors 0–255) with common handler
├── memory.c # PMM (bitmap), paging (4-level), heap (first-fit)
├── memory.h # Memory subsystem declarations and constants
├── task.c # Task manager: PCB pool, scheduler, yield/exit
├── task.h # Task types (Task, TaskState) and API
├── context_switch.S # Assembly cooperative context switch (x86-64)
├── Makefile # Build configuration
└── README.md # This file
Architecture
Boot Flow
- UEFI Entry —
efi_main()inmain.cis called by firmware - GNU-EFI Init — library initialized with the system table
- Kernel Load —
kernel.elfis read from the EFI partition, ELF64 PT_LOAD segments are mapped into memory - BootInfo Setup — function pointers for console I/O, shutdown, and page allocation are packaged into a
BootInfostruct - Kernel Entry —
kmain()is called with theBootInfopointer
Kernel Initialization
- IDT — CPU exception vectors (0–31) are installed; firmware IRQ handlers are preserved
- Memory — PMM allocates a 16 MB page pool via UEFI, bitmap tracks free/used pages; paging reads CR3; heap carves 256 KB from the PMM
- Tasks — scheduler initializes; the running kernel becomes task 0
- Shell — interactive loop with non-blocking input and cooperative yielding
Memory Management
| Component | Description |
|---|---|
| PMM | Bitmap-based page-frame allocator managing a 16 MB pool (4096 pages); supports single and contiguous multi-page allocation |
| Paging | Walks/creates 4-level x86-64 page tables (PML4 → PDPT → PD → PT); supports map, unmap, and virtual-to-physical translation |
| Heap | First-fit free-list allocator with block splitting and bidirectional coalescing; 16-byte alignment; magic number corruption detection |
Task System
| Component | Description |
|---|---|
| PCB | Task struct with PID, state, name, saved RSP, stack, entry function, and scheduling metadata |
| States | FREE → READY → RUNNING → TERMINATED → FREE |
| Scheduler | Round-robin selection among READY tasks via task_yield() |
| Context Switch | Assembly routine saves/restores callee-saved registers (rbp, rbx, r12–r15) and RFLAGS, swaps RSP |
| Stack | Each task gets 32 KB (8 pages) allocated from PMM; freed on task_exit() |
| Limits | Up to 32 concurrent tasks; cooperative (voluntary yield) only |
Command System
- Commands are registered in a static array with metadata (name, description, usage, handler function)
execute_command()parses input, splits command and arguments, and dispatches to the matching handlermanprovides detailed help by looking up command metadata- New commands are added by writing a handler and appending to the
commands[]array
Building for Real Hardware
To create a bootable USB drive:
- Build the project:
make - Format a USB drive with GPT and create an EFI partition (FAT32)
- Mount the EFI partition
- Copy
build/EFI/BOOT/BOOTX64.EFIto/EFI/BOOT/on the USB drive - Copy
build/kernel.elfto the root of the EFI partition - Boot from the USB drive in UEFI mode
Warning: Always backup your data before creating bootable media!
Cleaning
Remove build artifacts:
make clean
Troubleshooting
GNU-EFI headers not found
Make sure GNU-EFI is installed. The headers are typically in /usr/include/efi.
If installed in a different location, update the EFI_INC variable in the Makefile.
OVMF firmware not found
OVMF files might be in different locations depending on your distribution:
/usr/share/OVMF//usr/share/qemu//usr/share/edk2-ovmf/
Update the paths in the Makefile if needed.
QEMU crashes or fails to start
Ensure you have QEMU with x86_64 support and OVMF firmware installed.
Technical Details
- Architecture: x86_64
- Firmware Interface: UEFI 2.x
- Development Library: GNU-EFI
- Loader Format: PE32+ (Portable Executable for EFI)
- Kernel Format: ELF64
- Boot Protocol: UEFI Boot Services
- Memory Model: Identity-mapped (UEFI), 4-level paging (kernel)
- Scheduling: Cooperative round-robin multitasking
License
This is a minimal educational example. Feel free to use and modify as needed.
Adding New Commands
- Open
commands.c - Add a forward declaration:
static void cmd_yourcommand(BootInfo *Boot, CHAR16 *Args); - Add an entry to the
commands[]array (before the sentinel) - Implement the handler function
- Rebuild with
make