Compare commits

...

12 Commits

Author SHA1 Message Date
08cb1db571 Added privilidges 2026-02-27 21:04:56 +00:00
de161801c4 Refine command execution and enhance documentation. Improved the CommandTaskContext structure for better task management and updated README.md with clearer instructions for 'memtest' and 'tasktest' commands. 2026-02-27 20:23:26 +00:00
9b1a70e3a5 Enhance command execution with improved concurrency and documentation updates. Refined the CommandTaskContext structure for better task management and clarified usage instructions for 'memtest' and 'tasktest' commands in README.md. 2026-02-27 20:14:24 +00:00
7ecf26cbd9 Add command task execution and improve command documentation
Introduced a new CommandTaskContext structure to facilitate command execution in separate tasks. Updated the execute_command function to spawn tasks for commands, enhancing concurrency. Improved documentation for the 'memtest' and 'tasktest' commands in README.md to clarify their functionality and usage. Updated the interactive prompt reference to 'starling>'.
2026-02-27 20:14:11 +00:00
d17daf7aac Update system branding and improve documentation clarity. Changed references from "Simple UEFI Operating System" to "Simple 64-bit Operating System" in multiple files. Enhanced comments in memory.c for better understanding of PMM initialization and paging initialization. 2026-02-27 19:57:26 +00:00
a3edb854f4 Refactor boot_info.h and related files for improved abstraction and consistency. Updated function signatures to use generic types, replaced UEFI-specific types with kernel types, and enhanced documentation for clarity. Adjusted kernel entry point and service wrappers to align with new structure. 2026-02-27 19:53:40 +00:00
13a281fa4f Better docs and structure 2026-02-26 21:33:16 +00:00
d449150169 Task scheduler 2026-02-26 21:08:06 +00:00
6658e4314b Iso added 2026-02-26 20:39:33 +00:00
770526efae Memory heap and allocator 2026-02-26 20:24:56 +00:00
c2dd9d1e89 Merge branch 'master' of https://git.jimmybinoculars.org.uk/JimmyBinoculars/Operator-system 2026-02-26 20:13:33 +00:00
95c0f15640 Added interrupt and exception handling 2026-02-26 20:12:45 +00:00
24 changed files with 5792 additions and 342 deletions

206
Makefile
View File

@@ -1,45 +1,56 @@
# Makefile for UEFI Operating System
# ==============================================================================
# Makefile for Simple UEFI Operating System
# ==============================================================================
# Architecture
# ---- Architecture -----------------------------------------------------------
ARCH = x86_64
# Compiler and tools
CC = gcc
LD = ld
# ---- Tools ------------------------------------------------------------------
CC = gcc
LD = ld
OBJCOPY = objcopy
# Directories
SRC_DIR = .
# ---- Directories ------------------------------------------------------------
SRC_DIR = .
BUILD_DIR = build
EFI_DIR = $(BUILD_DIR)/EFI/BOOT
EFI_DIR = $(BUILD_DIR)/EFI/BOOT
IMAGE_DIR = $(BUILD_DIR)/image
# GNU-EFI paths (common locations)
EFI_INC = /usr/include/efi
EFI_INCLUDES = -I$(EFI_INC) -I$(EFI_INC)/$(ARCH) -I$(EFI_INC)/protocol
EFI_LDS = /usr/lib/elf_$(ARCH)_efi.lds
EFI_CRT_OBJS = /usr/lib/crt0-efi-$(ARCH).o
# ---- GNU-EFI paths ---------------------------------------------------------
EFI_INC = /usr/include/efi
EFI_INCLUDES = -I$(EFI_INC) -I$(EFI_INC)/$(ARCH) -I$(EFI_INC)/protocol
EFI_LDS = /usr/lib/elf_$(ARCH)_efi.lds
EFI_CRT_OBJS = /usr/lib/crt0-efi-$(ARCH).o
EFI_LIB_PATHS = -L/usr/lib
# Compiler flags
CFLAGS = -ffreestanding -fno-stack-protector -fpic \
-fshort-wchar -mno-red-zone -Wall -Wextra \
$(EFI_INCLUDES) -DEFI_FUNCTION_WRAPPER
# ---- Compiler / linker flags ------------------------------------------------
# Flags shared by both the UEFI loader and the kernel
COMMON_CFLAGS = -ffreestanding -fno-stack-protector -fshort-wchar \
-mno-red-zone -Wall -Wextra $(EFI_INCLUDES)
# UEFI loader: position-independent (shared object → PE32+ conversion)
LOADER_CFLAGS = $(COMMON_CFLAGS) -fpic -DEFI_FUNCTION_WRAPPER
# Kernel: position-dependent static ELF
KERNEL_CFLAGS = $(COMMON_CFLAGS) -fno-pic
# Linker flags
LDFLAGS = -nostdlib -znocombreloc -T $(EFI_LDS) -shared \
-Bsymbolic $(EFI_LIB_PATHS) $(EFI_CRT_OBJS)
# Libraries
LIBS = -lefi -lgnuefi
# Target
TARGET = BOOTX64.EFI
TARGET_SO = bootx64.so
OBJ = $(BUILD_DIR)/main.o
# ---- Targets / objects ------------------------------------------------------
TARGET = BOOTX64.EFI
TARGET_SO = bootx64.so
LOADER_OBJ = $(BUILD_DIR)/main.o
KERNEL_TARGET = kernel.elf
KERNEL_OBJS = $(BUILD_DIR)/kernel.o $(BUILD_DIR)/string_utils.o $(BUILD_DIR)/commands.o
KERNEL_LD = kernel.ld
KERNEL_LD = kernel.ld
KERNEL_C_SRCS = kernel.c string_utils.c commands.c idt.c memory.c task.c
KERNEL_S_SRCS = isr.S context_switch.S
KERNEL_OBJS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(KERNEL_C_SRCS)) \
$(patsubst %.S,$(BUILD_DIR)/%.o,$(KERNEL_S_SRCS))
# QEMU settings
QEMU = qemu-system-x86_64
@@ -76,60 +87,120 @@ QEMU_FLAGS = -machine q35 \
-drive if=pflash,format=raw,file=$(BUILD_DIR)/OVMF_VARS.fd \
-drive format=raw,file=fat:rw:$(BUILD_DIR) \
-net none \
-no-reboot
-no-reboot
# Phony targets
.PHONY: all clean run setup install-deps
# ==============================================================================
# Phony targets
# ==============================================================================
.PHONY: all clean run setup install-deps iso runiso help
# ---- Default ----------------------------------------------------------------
# Default target
all: setup $(EFI_DIR)/$(TARGET) $(BUILD_DIR)/$(KERNEL_TARGET)
# ==============================================================================
# ISO image
# ==============================================================================
ISO_FILE = $(IMAGE_DIR)/os.iso
ESP_IMG = $(IMAGE_DIR)/staging/esp.img
iso: all
@echo "Creating FAT ESP image for UEFI boot..."
@mkdir -p $(IMAGE_DIR)/staging
@# Calculate size: EFI binary + kernel + overhead, rounded up to MiB
@SIZE_KB=$$(( ($$(stat -c%s $(EFI_DIR)/$(TARGET)) + $$(stat -c%s $(BUILD_DIR)/$(KERNEL_TARGET)) + 1048576) / 1024 )); \
dd if=/dev/zero of=$(ESP_IMG) bs=1K count=$$SIZE_KB 2>/dev/null
mkfs.fat -F 12 $(ESP_IMG) >/dev/null
mmd -i $(ESP_IMG) ::EFI
mmd -i $(ESP_IMG) ::EFI/BOOT
mcopy -i $(ESP_IMG) $(EFI_DIR)/$(TARGET) ::EFI/BOOT/BOOTX64.EFI
mcopy -i $(ESP_IMG) $(BUILD_DIR)/$(KERNEL_TARGET) ::kernel.elf
@echo "Creating ISO image..."
xorriso -as mkisofs \
-o $(ISO_FILE) \
-e esp.img \
-no-emul-boot \
$(IMAGE_DIR)/staging
@echo "ISO image created: $(ISO_FILE)"
# ==============================================================================
# Run targets
# ==============================================================================
# Run from ISO with QEMU
runiso: iso
@echo "Starting QEMU from ISO..."
@echo "Using OVMF firmware: $(OVMF_CODE)"
@if [ ! -f $(BUILD_DIR)/OVMF_VARS.fd ]; then \
echo "Creating OVMF_VARS.fd from template..."; \
cp $(OVMF_VARS_TEMPLATE) $(BUILD_DIR)/OVMF_VARS.fd 2>/dev/null || \
dd if=/dev/zero of=$(BUILD_DIR)/OVMF_VARS.fd bs=1M count=64 2>/dev/null; \
fi
$(QEMU) -machine q35 \
-drive if=pflash,format=raw,readonly=on,file=$(OVMF_CODE) \
-drive if=pflash,format=raw,file=$(BUILD_DIR)/OVMF_VARS.fd \
-cdrom $(ISO_FILE) \
-net none \
-nographic \
-no-reboot
# ==============================================================================
# Directory setup
# ==============================================================================
# Create necessary directories
setup:
@mkdir -p $(BUILD_DIR)
@mkdir -p $(EFI_DIR)
@mkdir -p $(IMAGE_DIR)
# Compile source files
# ==============================================================================
# Compilation rules
# ==============================================================================
# ---- UEFI loader object (PIC, linked as shared object → PE32+) ----
$(BUILD_DIR)/main.o: $(SRC_DIR)/main.c
@echo " CC $<"
@$(CC) $(LOADER_CFLAGS) -c $< -o $@
# ---- Kernel objects (position-dependent, linked into static ELF) ----
# Pattern rules replace the per-file rules for every kernel source.
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@echo "Compiling $<..."
$(CC) $(CFLAGS) -c $< -o $@
@echo " CC $<"
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
# Compile kernel source
$(BUILD_DIR)/kernel.o: $(SRC_DIR)/kernel.c
@echo "Compiling kernel.c..."
$(CC) -ffreestanding -fno-stack-protector -fno-pic -fshort-wchar \
-mno-red-zone -Wall -Wextra $(EFI_INCLUDES) -c $< -o $@
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.S
@echo " AS $<"
@$(CC) $(KERNEL_CFLAGS) -c $< -o $@
$(BUILD_DIR)/string_utils.o: $(SRC_DIR)/string_utils.c
@echo "Compiling string_utils.c..."
$(CC) -ffreestanding -fno-stack-protector -fno-pic -fshort-wchar \
-mno-red-zone -Wall -Wextra $(EFI_INCLUDES) -c $< -o $@
# ==============================================================================
# Linking
# ==============================================================================
$(BUILD_DIR)/commands.o: $(SRC_DIR)/commands.c
@echo "Compiling commands.c..."
$(CC) -ffreestanding -fno-stack-protector -fno-pic -fshort-wchar \
-mno-red-zone -Wall -Wextra $(EFI_INCLUDES) -c $< -o $@
# ---- Kernel ELF ----
# Link kernel ELF
$(BUILD_DIR)/$(KERNEL_TARGET): $(KERNEL_OBJS) $(KERNEL_LD)
@echo "Linking kernel ELF..."
$(LD) -nostdlib -T $(KERNEL_LD) $(KERNEL_OBJS) -o $@
@echo " LD $@"
@$(LD) -nostdlib -T $(KERNEL_LD) $(KERNEL_OBJS) -o $@
# Link to create shared object
$(BUILD_DIR)/$(TARGET_SO): $(OBJ)
@echo "Linking $@..."
$(LD) $(LDFLAGS) $(OBJ) -o $@ $(LIBS)
# ---- UEFI loader shared object ----
$(BUILD_DIR)/$(TARGET_SO): $(LOADER_OBJ)
@echo " LD $@"
@$(LD) $(LDFLAGS) $(LOADER_OBJ) -o $@ $(LIBS)
# ---- Convert to PE32+ EFI application ----
# Convert to EFI application
$(EFI_DIR)/$(TARGET): $(BUILD_DIR)/$(TARGET_SO)
@echo "Creating EFI application..."
$(OBJCOPY) -j .text -j .sdata -j .data -j .dynamic \
-j .dynsym -j .rel -j .rela -j .reloc \
--target=efi-app-$(ARCH) $< $@
@echo " OBJCOPY $@"
@$(OBJCOPY) -j .text -j .sdata -j .data -j .dynamic \
-j .dynsym -j .rel -j .rela -j .reloc \
--target=efi-app-$(ARCH) $< $@
@echo "Build complete: $@"
# Run with QEMU
# Run with QEMU (FAT directory)
run: all
@echo "Starting QEMU..."
@echo "Using OVMF firmware: $(OVMF_CODE)"
@@ -145,26 +216,35 @@ run: all
@echo "================================================"
$(QEMU) $(QEMU_FLAGS)
# ==============================================================================
# Maintenance
# ==============================================================================
# Clean build artifacts
clean:
@echo "Cleaning build directory..."
rm -rf $(BUILD_DIR)
@echo "Clean complete."
# Install dependencies (for Debian/Ubuntu)
# Install dependencies (Debian/Ubuntu)
install-deps:
@echo "Installing dependencies..."
sudo apt-get update
sudo apt-get install -y gnu-efi qemu-system-x86 ovmf gcc binutils make
sudo apt-get install -y gnu-efi qemu-system-x86 ovmf gcc binutils make xorriso mtools
@echo "Dependencies installed."
# Help target
# ==============================================================================
# Help
# ==============================================================================
help:
@echo "UEFI Operating System Makefile"
@echo ""
@echo "Targets:"
@echo " all - Build the UEFI application (default)"
@echo " run - Build and run with QEMU"
@echo " iso - Create a bootable ISO image"
@echo " runiso - Build, create ISO, and run in QEMU from ISO"
@echo " clean - Remove build artifacts"
@echo " install-deps - Install required dependencies (Debian/Ubuntu)"
@echo " help - Display this help message"
@@ -172,5 +252,7 @@ help:
@echo "Usage:"
@echo " make # Build the OS"
@echo " make run # Build and run in QEMU"
@echo " make iso # Create ISO image"
@echo " make runiso # Build, create ISO, and run in QEMU from ISO"
@echo " make clean # Clean build files"
@echo " make install-deps # Install dependencies"

371
README.md
View File

@@ -1,25 +1,33 @@
# Simple UEFI Operating System
A minimal bootable UEFI operating system written in C that demonstrates basic UEFI functionality.
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
- **Console I/O**: Interactive keyboard input and screen output
- **System Information**: Displays firmware details
- **Kernel Loader**: Loads an ELF64 kernel from the EFI partition
- **Simple Commands**: Minimal interactive command interface
- **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 031) 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
## Getting Started
### Requirements
- GCC cross-compiler (or native x86-64 GCC)
- GNU-EFI library and headers
- QEMU with OVMF firmware
- Make
- GNU Make
- `xorriso` and `mtools` (for ISO builds only)
## Installation
### Installation
### On Debian/Ubuntu:
<details>
<summary><strong>Debian / Ubuntu</strong></summary>
```bash
make install-deps
@@ -28,171 +36,294 @@ make install-deps
Or manually:
```bash
sudo apt-get install gnu-efi qemu-system-x86 ovmf gcc binutils make
sudo apt-get install gnu-efi qemu-system-x86 ovmf gcc binutils make xorriso mtools
```
### On Arch Linux:
</details>
<details>
<summary><strong>Arch Linux</strong></summary>
```bash
sudo pacman -S gnu-efi qemu-system-x86 edk2-ovmf gcc binutils make
sudo pacman -S gnu-efi qemu-system-x86 edk2-ovmf gcc binutils make xorriso mtools
```
### On Fedora/RHEL:
</details>
<details>
<summary><strong>Fedora / RHEL</strong></summary>
```bash
sudo dnf install gnu-efi qemu-system-x86 edk2-ovmf gcc binutils make
sudo dnf install gnu-efi qemu-system-x86 edk2-ovmf gcc binutils make xorriso mtools
```
## Building
</details>
Build the UEFI application and kernel:
### Building
```bash
make
make # build both UEFI loader and kernel
```
This will create `build/EFI/BOOT/BOOTX64.EFI` and `build/kernel.elf`.
This produces two binaries:
## Running
| Output | Description |
|--------|-------------|
| `build/EFI/BOOT/BOOTX64.EFI` | UEFI loader (PE32+) |
| `build/kernel.elf` | Kernel binary (ELF64) |
Test with QEMU (no graphics mode):
### Running
```bash
make run
make run # build and run with QEMU (FAT directory, nographic)
make runiso # build, create ISO, and run with QEMU
```
The loader expects `kernel.elf` at the root of the EFI partition (next to the `EFI/` directory).
### QEMU Controls:
- **Exit QEMU**: Press `Ctrl+A`, then `X`
- **Shutdown OS**: Type `shutdown` in the OS
| Key / Command | Action |
|---------------|--------|
| `Ctrl+A`, then `X` | Exit QEMU |
| `shutdown` (in shell) | Power off the OS |
## Commands in the OS
### Make Targets
Once the OS boots, you can use these commands:
| Target | Description |
|--------|-------------|
| `all` | Build the UEFI loader and kernel (default) |
| `run` | Build and run with QEMU |
| `iso` | Create a bootable ISO image |
| `runiso` | Build, create ISO, and run in QEMU |
| `clean` | Remove all build artifacts |
| `install-deps` | Install required packages (Debian/Ubuntu) |
| `help` | Print target summary |
- `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
---
## Usage
### Shell Commands
Once the OS boots, the Starling Terminal displays an interactive prompt (`starling>`). The following built-in commands are available:
| Command | Description |
|---------|-------------|
| `help` | List all available commands |
| `man <command>` | Show detailed help for a command |
| `shutdown` | Shut down 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 (optional argument sets the task name) |
| `memtest` | Run memory allocation/deallocation and PMM tests |
| `tasktest` | Spawn multiple concurrent tasks to test the scheduler |
In addition, the Starling Terminal itself recognises:
| Command | Description |
|---------|-------------|
| `starling` | Spawn a nested Starling Terminal at increased depth |
| `exit` | Exit the current nested Starling Terminal (not allowed at depth 0) |
### Example Session
Example usage:
```
-> help
-> man shutdown
-> clear
-> about
-> mem
-> memtest
-> spawn worker1
-> spawn worker2
-> ps
-> tasktest
-> shutdown
```
### Testing Memory Management
The `memtest` command runs four phases:
1. **Heap allocation** — allocates 8 blocks of increasing size (164096 bytes) via `kmalloc()`
2. **Heap free** — frees all blocks via `kfree()` and verifies coalescing
3. **PMM single page** — allocates and frees a single 4 KB page
4. **PMM multi-page** — allocates and frees 4 contiguous pages
Each phase reports addresses, success/failure, and usage statistics.
### Testing Task Scheduling
The `tasktest` command:
1. Spawns 3 worker tasks (`worker-A`, `worker-B`, `worker-C`)
2. Each worker prints 3 progress steps, yielding between each
3. The shell yields to let workers run in round-robin order
4. Displays the task list after completion
You can also test manually 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).
---
## Architecture
### Boot Flow
```
UEFI Firmware
└─ efi_main() [main.c] PE32+ UEFI application
├─ InitializeLib() GNU-EFI setup
├─ read_file_to_buffer() read kernel.elf from ESP
├─ load_elf_kernel() parse ELF64, map PT_LOAD segments
├─ populate BootInfo wrap UEFI services as fn pointers
└─ kmain(&Boot) [kernel.c] jump to kernel
├─ idt_init() install exception handlers
├─ memory_init() PMM → paging → heap
├─ task_init() scheduler + task 0
└─ Starling Terminal task interactive read-eval-print loop
```
### Memory Management
| Layer | Description |
|-------|-------------|
| **PMM** | Bitmap-based page-frame allocator managing a 16 MB pool (4096 pages). Supports single and contiguous multi-page allocation. |
| **Paging** | Walks and creates 4-level x86-64 page tables (PML4 → PDPT → PD → PT). Supports map, unmap, and virtual-to-physical translation for 4 KB, 2 MB, and 1 GB page sizes. |
| **Heap** | First-fit free-list allocator with block splitting and bidirectional coalescing. 16-byte alignment. Magic-number corruption detection. |
### Task System
| Aspect | Detail |
|--------|--------|
| **PCB** | `Task` struct: PID, state, name, saved RSP, stack base, entry function, scheduling metadata |
| **States** | `FREE``READY``RUNNING``TERMINATED``FREE` |
| **Scheduler** | Round-robin among `READY` tasks via `task_yield()` |
| **Context Switch** | Assembly routine saves/restores callee-saved registers (`rbp`, `rbx`, `r12``r15`) and `RFLAGS`, then swaps RSP |
| **Stack** | 32 KB (8 pages) per task, 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 in `commands.c`:
```c
static Command commands[] = {
{ L"name", L"description", L"usage", handler_fn },
...
{ NULL, NULL, NULL, NULL } /* sentinel */
};
```
`execute_command()` splits user input into command + arguments and dispatches to the matching handler. `man` provides detailed help by looking up command metadata.
---
## Project Structure
```
.
├── main.c # UEFI loader (reads and loads kernel ELF)
├── kernel.c # Kernel entry point and main loop
├── commands.c # Command registry and handlers
├── commands.h # Command interface definitions
├── string_utils.c # String utility functions
├── string_utils.h # String utility declarations
├── kernel.ld # Kernel linker script
├── boot_info.h # Shared boot interface
├── Makefile # Build configuration
└── README.md # This file
├── main.c # UEFI loader reads kernel.elf, sets up BootInfo
├── kernel.c # Kernel entry point (kmain) and interactive shell
├── kernel.ld # Linker script — kernel loaded at 0x100000
├── boot_info.h # BootInfo struct shared between loader and kernel
├── commands.c # Command registry and all handler implementations
├── commands.h # Command type and dispatch API
├── string_utils.c # CHAR16 string helpers (trim, compare)
├── string_utils.h # String utility declarations
├── idt.c # IDT setup, exception handlers, PIC EOI
├── idt.h # ISRFrame layout and idt_init() declaration
├── isr.S # ISR stub table (vectors 0255) and common handler
├── memory.c # PMM (bitmap), paging (4-level), heap (first-fit)
├── memory.h # Memory subsystem constants, types, and API
├── task.c # PCB pool, round-robin scheduler, yield/exit
├── task.h # Task types (Task, TaskState) and task API
├── context_switch.S # Cooperative context switch (x86-64 assembly)
├── Makefile # Build system
├── README.md # This file
└── build/ # Build output (generated)
├── EFI/BOOT/BOOTX64.EFI
├── kernel.elf
└── image/ # ISO staging area
```
## How It Works
---
1. **UEFI Entry Point**: The `efi_main` function is the entry point called by UEFI firmware
2. **Initialization**: GNU-EFI library is initialized with the system table
3. **Console Setup**: Output is configured and screen is cleared
4. **Kernel Load**: The loader reads `kernel.elf` and maps its loadable segments
5. **Main Loop**: The kernel enters a loop waiting for keyboard input
6. **Command Processing**: User commands are parsed and dispatched to registered handlers
7. **Command Execution**: Each command is executed via its handler function
8. **Shutdown**: UEFI runtime services are used to shutdown the system
## Development
**Command System Architecture:**
- Commands are registered in a static array with metadata (name, description, usage, handler)
- The `execute_command()` function parses input, splits command and arguments, and dispatches to handlers
- The `man` command provides detailed help by looking up command metadata
- New commands can be easily added by implementing a handler and registering it
### Adding a New Command
## Building for Real Hardware
1. Open `commands.c`
2. Add a forward declaration:
```c
static void cmd_foo(BootInfo *Boot, CHAR16 *Args);
```
3. Append an entry to the `commands[]` array (before the `NULL` sentinel):
```c
{ L"foo", L"Short description", L"Usage: foo [args]\n\r Details.", cmd_foo },
```
4. Implement `cmd_foo`
5. Rebuild with `make`
To create a bootable USB drive:
### Building for Real Hardware
1. Build the project: `make`
2. Format a USB drive with GPT and create an EFI partition (FAT32)
3. Mount the EFI partition
4. Copy `build/EFI/BOOT/BOOTX64.EFI` to `/EFI/BOOT/` on the USB drive
5. Copy `build/kernel.elf` to the root of the EFI partition
6. Boot from the USB drive in UEFI mode
2. Format a USB drive with GPT and a FAT32 EFI System Partition
3. Mount the ESP
4. Copy `build/EFI/BOOT/BOOTX64.EFI` → `<mount>/EFI/BOOT/BOOTX64.EFI`
5. Copy `build/kernel.elf` → `<mount>/kernel.elf`
6. Boot from the USB in UEFI mode
**Warning**: Always backup your data before creating bootable media!
> **Warning:** Always back up your data before creating bootable media.
## Cleaning
Remove build artifacts:
```bash
make clean
```
---
## Troubleshooting
### GNU-EFI headers not found
| Problem | Solution |
|---------|----------|
| **GNU-EFI headers not found** | Install GNU-EFI. Headers are typically at `/usr/include/efi`. Update `EFI_INC` in the Makefile if installed elsewhere. |
| **OVMF firmware not found** | Check `/usr/share/OVMF/`, `/usr/share/qemu/`, or `/usr/share/edk2-ovmf/`. The Makefile auto-detects common paths. |
| **QEMU crashes or won't start** | Ensure `qemu-system-x86_64` and OVMF are installed. Verify with `qemu-system-x86_64 --version`. |
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
- **Executable Format**: PE32+ (Portable Executable for EFI)
- **Boot Protocol**: UEFI Boot Services
## License
This is a minimal educational example. Feel free to use and modify as needed.
## Further Development
The codebase is now modular and easy to extend:
**Adding New Commands:**
1. Open `commands.c`
2. Create a new handler function: `static void cmd_yourcommand(BootInfo *Boot, CHAR16 *Args)`
3. Add your command to the `commands[]` array with name, description, usage, and handler
4. Rebuild with `make`
**Other Ideas for expansion:**
- File system support
- Memory management
- Graphics output
- Network stack
- Device drivers
- Multi-tasking
| Property | Value |
|----------|-------|
| Architecture | x86-64 (long mode) |
| Firmware Interface | UEFI 2.x |
| Development Library | GNU-EFI |
| Loader Format | PE32+ (EFI application) |
| Kernel Format | ELF64 (static, loaded at 0x100000) |
| Boot Protocol | UEFI Boot Services |
| Memory Model | Identity-mapped (UEFI), 4-level paging (kernel) |
| Scheduling | Cooperative round-robin multitasking |
## Resources
- [UEFI Specification](https://uefi.org/specifications)
- [GNU-EFI Documentation](https://sourceforge.net/projects/gnu-efi/)
- [OSDev Wiki](https://wiki.osdev.org/)
## License
This is a minimal educational example. Feel free to use and modify as needed.

View File

@@ -1,19 +1,67 @@
/*
* boot_info.h Shared interface between the firmware loader and kernel.
*
* The BootInfo struct is populated by the platform-specific loader
* (currently the UEFI loader in main.c) and passed to the kernel entry
* point (kmain). It exposes only generic types and function pointers so
* that the kernel remains independent of any particular firmware API.
*/
#ifndef BOOT_INFO_H
#define BOOT_INFO_H
#include <efi.h>
/*
* Status code returned by loader-provided services (0 = success).
* The underlying integer type (UINT64) must be provided by including
* either the firmware headers (e.g. <efi.h>) or the kernel type
* header before including this file.
*/
typedef UINT64 KSTATUS;
/* Simple key event used by the kernel's input path. */
typedef struct {
UINT16 scan_code;
CHAR16 unicode_char;
} KeyEvent;
/* Printf-style print function (typically backed by firmware console). */
typedef UINTN (*KernelPrintFn)(const CHAR16 *Format, ...);
/* Console output helpers. */
typedef KSTATUS (*ConsoleClearFn)(void);
typedef KSTATUS (*ConsoleSetAttrFn)(UINTN Attribute);
/* Keyboard input helpers. */
typedef KSTATUS (*KeyReadFn)(KeyEvent *Key);
/*
* BootInfo everything the kernel needs from the loader at boot time.
*
* All function pointers are optional: the kernel must NULL-check before
* calling. If a service is unavailable the corresponding field is NULL.
*/
typedef struct {
EFI_SYSTEM_TABLE *SystemTable;
KernelPrintFn print;
EFI_STATUS (*clear_screen)(void);
EFI_STATUS (*set_attribute)(UINTN Attribute);
EFI_STATUS (*read_key)(EFI_INPUT_KEY *Key);
void (*shutdown)(void);
/* Console I/O */
KernelPrintFn print; /* formatted text output */
ConsoleClearFn clear_screen; /* clear the console */
ConsoleSetAttrFn set_attribute; /* set text colour/attr */
KeyReadFn read_key; /* blocking key read */
KeyReadFn try_read_key; /* non-blocking key read */
/* System control */
void (*shutdown)(void); /* power-off the machine */
/* Physical memory */
KSTATUS (*alloc_pages)(UINTN pages, UINT64 *addr);
KSTATUS (*free_pages)(UINT64 addr, UINTN pages);
/* Firmware metadata (for informational commands only). */
const CHAR16 *firmware_vendor;
UINT32 firmware_major;
UINT32 firmware_minor;
} BootInfo;
/* Kernel entry point signature (called by the loader). */
typedef void (*KernelEntryFn)(BootInfo *Boot);
#endif
#endif /* BOOT_INFO_H */

View File

@@ -1,7 +1,23 @@
#include <efi.h>
/*
* commands.c Shell command registry and handler implementations.
*
* Each command is a { name, description, usage, handler } entry in the
* static `commands[]` table. execute_command() splits user input into
* command + arguments and dispatches to the matching handler.
*
* To add a new command:
* 1. Write a static handler cmd_foo(BootInfo *Boot, CHAR16 *Args)
* 2. Add a forward declaration above the table
* 3. Append an entry to commands[] (before the sentinel)
*/
#include "kernel_types.h"
#include "commands.h"
#include "string_utils.h"
#include "memory.h"
#include "task.h"
/* Null-safe print helper used throughout the kernel. */
#define SAFE_PRINT(Boot, ...) \
do { \
if ((Boot) != NULL && (Boot)->print != NULL) { \
@@ -9,80 +25,185 @@
} \
} while (0)
// Forward declarations
/* ================================================================
* Forward declarations
* ================================================================ */
static void cmd_shutdown(BootInfo *Boot, CHAR16 *Args);
static void cmd_help(BootInfo *Boot, CHAR16 *Args);
static void cmd_man(BootInfo *Boot, CHAR16 *Args);
static void cmd_clear(BootInfo *Boot, CHAR16 *Args);
static void cmd_about(BootInfo *Boot, CHAR16 *Args);
static void cmd_mem(BootInfo *Boot, CHAR16 *Args);
static void cmd_ps(BootInfo *Boot, CHAR16 *Args);
static void cmd_spawn(BootInfo *Boot, CHAR16 *Args);
static void cmd_memtest(BootInfo *Boot, CHAR16 *Args);
static void cmd_tasktest(BootInfo *Boot, CHAR16 *Args);
static void cmd_kusr(BootInfo *Boot, CHAR16 *Args);
/* Small helper struct used to pass arguments into per-command tasks. */
typedef struct {
BootInfo *Boot;
CommandHandlerFn handler;
CHAR16 args[128];
} CommandTaskContext;
static void command_task_entry(void *arg);
/* Local string copy helper (wide-char, bounded). */
static void wstrcpy16_local(CHAR16 *dst, const CHAR16 *src, UINTN max)
{
UINTN i = 0;
if (dst == NULL || max == 0) {
return;
}
while (src != NULL && src[i] != L'\0' && i < (max - 1)) {
dst[i] = src[i];
i++;
}
dst[i] = L'\0';
}
/* ================================================================
* Command registry
* ================================================================ */
// Command registry
static Command commands[] = {
{
L"shutdown",
L"Shutdown the system",
L"Usage: shutdown\n\r Initiates a system shutdown using UEFI runtime services.",
TASK_PRIV_KERNEL,
cmd_shutdown
},
{
L"help",
L"Display available commands",
L"Usage: help\n\r Lists all available commands with brief descriptions.",
TASK_PRIV_USER,
cmd_help
},
{
L"man",
L"Display manual page for a command",
L"Usage: man <command>\n\r Shows detailed help for the specified command.",
TASK_PRIV_USER,
cmd_man
},
{
L"clear",
L"Clear the screen",
L"Usage: clear\n\r Clears the console screen.",
TASK_PRIV_USER,
cmd_clear
},
{
L"about",
L"Display system information",
L"Usage: about\n\r Shows information about this operating system.",
TASK_PRIV_USER,
cmd_about
},
{NULL, NULL, NULL, NULL} // Sentinel
{
L"mem",
L"Display memory statistics",
L"Usage: mem\n\r Shows physical memory, heap, and paging information.",
TASK_PRIV_KERNEL,
cmd_mem
},
{
L"ps",
L"List running tasks",
L"Usage: ps\n\r Displays all active tasks with PID, state, and name.",
TASK_PRIV_DRIVER,
cmd_ps
},
{
L"spawn",
L"Spawn a demo background task",
L"Usage: spawn [name]\n\r Creates a cooperative demo task.\n\r Optional argument sets the task name.",
TASK_PRIV_DRIVER,
cmd_spawn
},
{
L"memtest",
L"Test memory allocation and deallocation",
L"Usage: memtest\n\r"
L" Run memory tests in four phases:\n\r"
L" 1) Heap allocation of 8 blocks (164096 bytes) via kmalloc()\n\r"
L" 2) Heap free and coalescing verification via kfree()\n\r"
L" 3) Single-page PMM allocate/free via pmm_alloc_page()/pmm_free_page()\n\r"
L" 4) Multi-page (4-page) PMM allocate/free via pmm_alloc_pages()/pmm_free_pages()\n\r",
TASK_PRIV_KERNEL,
cmd_memtest
},
{
L"tasktest",
L"Test task scheduler with multiple tasks",
L"Usage: tasktest\n\r"
L" Spawns three worker tasks (worker-A/B/C) that run cooperatively,\n\r"
L" each printing three progress steps and yielding between them.\n\r"
L" After the workers finish, prints the final task list to\n\r"
L" demonstrate the cooperative round-robin scheduler.",
TASK_PRIV_DRIVER,
cmd_tasktest
},
{
L"kusr",
L"Run a command with kernel privilege",
L"Usage: kusr <command> [args...]\n\r"
L" Temporarily elevates the current task to kernel privilege,\n\r"
L" executes the given command, then restores the original level.",
TASK_PRIV_USER,
cmd_kusr
},
{NULL, NULL, NULL, 0, NULL} /* sentinel */
};
/* ================================================================
* System control
* ================================================================ */
static void request_shutdown(BootInfo *Boot)
{
Task *caller;
if (Boot == NULL) {
return;
}
/* Subsystem-level privilege enforcement: shutdown requires KERNEL. */
caller = task_current();
if (caller != NULL && task_get_privilege(caller) < TASK_PRIV_KERNEL) {
SAFE_PRINT(Boot, L"Permission denied: shutdown requires kernel privilege.\n\r");
return;
}
if (Boot->shutdown != NULL) {
Boot->shutdown();
return;
}
if (Boot->SystemTable != NULL &&
Boot->SystemTable->RuntimeServices != NULL &&
Boot->SystemTable->RuntimeServices->ResetSystem != NULL) {
Boot->SystemTable->RuntimeServices->ResetSystem(
EfiResetShutdown, EFI_SUCCESS, 0, NULL);
return;
}
SAFE_PRINT(Boot, L"Shutdown service unavailable.\n\r");
}
/* ================================================================
* Built-in command handlers
* ================================================================ */
static void cmd_shutdown(BootInfo *Boot, CHAR16 *Args)
{
(void)Args; // Unused
(void)Args;
SAFE_PRINT(Boot, L"Shutting down...\n\r");
request_shutdown(Boot);
}
static void cmd_help(BootInfo *Boot, CHAR16 *Args)
{
(void)Args; // Unused
(void)Args;
show_help(Boot);
}
@@ -116,13 +237,14 @@ static void cmd_man(BootInfo *Boot, CHAR16 *Args)
static void cmd_clear(BootInfo *Boot, CHAR16 *Args)
{
EFI_STATUS Status;
(void)Args; // Unused
KSTATUS Status;
(void)Args;
if (Boot != NULL && Boot->clear_screen != NULL) {
Status = Boot->clear_screen();
if (EFI_ERROR(Status)) {
SAFE_PRINT(Boot, L"Failed to clear screen: %r\n\r", Status);
if (Status != 0) {
SAFE_PRINT(Boot, L"Failed to clear screen (status=%ld)\n\r",
(UINT64)Status);
}
} else {
SAFE_PRINT(Boot, L"Clear screen function unavailable.\n\r");
@@ -131,25 +253,263 @@ static void cmd_clear(BootInfo *Boot, CHAR16 *Args)
static void cmd_about(BootInfo *Boot, CHAR16 *Args)
{
(void)Args; // Unused
(void)Args;
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"================================================\n\r");
SAFE_PRINT(Boot, L" Simple UEFI Operating System\n\r");
SAFE_PRINT(Boot, L" Simple 64-bit Operating System\n\r");
SAFE_PRINT(Boot, L"================================================\n\r");
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"A minimal bootable UEFI operating system written in C.\n\r");
SAFE_PRINT(Boot, L"A minimal bootable operating system written in C.\n\r");
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Features:\n\r");
SAFE_PRINT(Boot, L" - UEFI Boot\n\r");
SAFE_PRINT(Boot, L" - Flexible bootloader interface\n\r");
SAFE_PRINT(Boot, L" - Console I/O\n\r");
SAFE_PRINT(Boot, L" - ELF64 Kernel Loader\n\r");
SAFE_PRINT(Boot, L" - Memory Management (PMM + Heap)\n\r");
SAFE_PRINT(Boot, L" - Cooperative Multitasking\n\r");
SAFE_PRINT(Boot, L" - Interactive Command Interface\n\r");
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Type 'help' for available commands.\n\r");
SAFE_PRINT(Boot, L"\n\r");
}
static void cmd_mem(BootInfo *Boot, CHAR16 *Args)
{
(void)Args;
memory_print_stats(Boot);
}
/* ----------------------------------------------------------------
* Demo task runs cooperatively, prints progress, then exits
* ---------------------------------------------------------------- */
static void demo_task_fn(void *arg)
{
BootInfo *Boot = (BootInfo *)arg;
Task *self = task_current();
UINTN i;
SAFE_PRINT(Boot, L"[%s:%d] started\n\r", self->name, self->pid);
for (i = 0; i < 5; i++) {
SAFE_PRINT(Boot, L"[%s:%d] tick %d/5\n\r",
self->name, self->pid, i + 1);
task_yield();
}
SAFE_PRINT(Boot, L"[%s:%d] finished\n\r", self->name, self->pid);
}
static void cmd_ps(BootInfo *Boot, CHAR16 *Args)
{
(void)Args;
task_print_list(Boot);
}
static void cmd_spawn(BootInfo *Boot, CHAR16 *Args)
{
Task *t;
const CHAR16 *name;
if (Args != NULL && Args[0] != L'\0') {
trim_spaces_inplace(Args);
name = Args;
} else {
name = L"demo";
}
t = task_create(name, demo_task_fn, Boot);
if (t == NULL) {
SAFE_PRINT(Boot, L"Failed to create task (out of slots or memory).\n\r");
return;
}
SAFE_PRINT(Boot, L"Created task '%s' (PID %d)\n\r", t->name, t->pid);
}
/* ----------------------------------------------------------------
* memtest exercise the heap and PMM allocators
* ---------------------------------------------------------------- */
static void cmd_memtest(BootInfo *Boot, CHAR16 *Args)
{
void *ptrs[8];
UINTN sizes[] = { 16, 64, 128, 256, 512, 1024, 2048, 4096 };
UINTN i;
UINT64 page;
UINTN h_total, h_used, h_free, h_blocks;
Task *caller;
(void)Args;
/* Subsystem-level privilege enforcement: memtest requires KERNEL. */
caller = task_current();
if (caller != NULL && task_get_privilege(caller) < TASK_PRIV_KERNEL) {
SAFE_PRINT(Boot, L"Permission denied: memtest requires kernel privilege.\n\r");
return;
}
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Memory Test\n\r");
SAFE_PRINT(Boot, L"================================================\n\r");
SAFE_PRINT(Boot, L"\n\r");
/* --- Heap allocation test --- */
SAFE_PRINT(Boot, L"[1] Heap allocation test\n\r");
for (i = 0; i < 8; i++) {
ptrs[i] = kmalloc(sizes[i]);
if (ptrs[i] != NULL) {
SAFE_PRINT(Boot, L" kmalloc(%4d) = 0x%lx OK\n\r",
sizes[i], (UINT64)(UINTN)ptrs[i]);
} else {
SAFE_PRINT(Boot, L" kmalloc(%4d) = NULL FAIL\n\r", sizes[i]);
}
}
heap_get_stats(&h_total, &h_used, &h_free, &h_blocks);
SAFE_PRINT(Boot, L" Heap after alloc: used=%d free=%d blocks=%d\n\r",
h_used, h_free, h_blocks);
/* --- Heap free test --- */
SAFE_PRINT(Boot, L"\n\r[2] Heap free test\n\r");
for (i = 0; i < 8; i++) {
if (ptrs[i] != NULL) {
kfree(ptrs[i]);
SAFE_PRINT(Boot, L" kfree(0x%lx) OK\n\r",
(UINT64)(UINTN)ptrs[i]);
}
}
heap_get_stats(&h_total, &h_used, &h_free, &h_blocks);
SAFE_PRINT(Boot, L" Heap after free: used=%d free=%d blocks=%d\n\r",
h_used, h_free, h_blocks);
/* --- PMM page allocation test --- */
SAFE_PRINT(Boot, L"\n\r[3] PMM page allocation test\n\r");
SAFE_PRINT(Boot, L" Free pages before: %d\n\r", pmm_get_free_pages());
page = pmm_alloc_page();
if (page != 0) {
SAFE_PRINT(Boot, L" pmm_alloc_page() = 0x%lx OK\n\r", page);
SAFE_PRINT(Boot, L" Free pages after alloc: %d\n\r", pmm_get_free_pages());
pmm_free_page(page);
SAFE_PRINT(Boot, L" pmm_free_page() OK\n\r");
SAFE_PRINT(Boot, L" Free pages after free: %d\n\r", pmm_get_free_pages());
} else {
SAFE_PRINT(Boot, L" pmm_alloc_page() = 0 FAIL (out of pages)\n\r");
}
/* --- Multi-page allocation test --- */
SAFE_PRINT(Boot, L"\n\r[4] PMM multi-page allocation test (4 pages)\n\r");
page = pmm_alloc_pages(4);
if (page != 0) {
SAFE_PRINT(Boot, L" pmm_alloc_pages(4) = 0x%lx OK\n\r", page);
SAFE_PRINT(Boot, L" Free pages after alloc: %d\n\r", pmm_get_free_pages());
pmm_free_pages(page, 4);
SAFE_PRINT(Boot, L" pmm_free_pages() OK\n\r");
SAFE_PRINT(Boot, L" Free pages after free: %d\n\r", pmm_get_free_pages());
} else {
SAFE_PRINT(Boot, L" pmm_alloc_pages(4) = 0 FAIL\n\r");
}
SAFE_PRINT(Boot, L"\n\rAll memory tests completed.\n\r\n\r");
}
/* ----------------------------------------------------------------
* tasktest spawn multiple concurrent tasks to exercise scheduler
* ---------------------------------------------------------------- */
static void worker_task_fn(void *arg)
{
BootInfo *Boot = (BootInfo *)arg;
Task *self = task_current();
UINTN i;
for (i = 0; i < 3; i++) {
SAFE_PRINT(Boot, L" [%s:%d] working... step %d/3\n\r",
self->name, self->pid, i + 1);
task_yield();
}
SAFE_PRINT(Boot, L" [%s:%d] done\n\r", self->name, self->pid);
}
static void cmd_tasktest(BootInfo *Boot, CHAR16 *Args)
{
Task *t1, *t2, *t3;
UINTN i;
(void)Args;
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Task Scheduler Test\n\r");
SAFE_PRINT(Boot, L"================================================\n\r");
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Spawning 3 worker tasks...\n\r\n\r");
t1 = task_create(L"worker-A", worker_task_fn, Boot);
t2 = task_create(L"worker-B", worker_task_fn, Boot);
t3 = task_create(L"worker-C", worker_task_fn, Boot);
if (t1 == NULL || t2 == NULL || t3 == NULL) {
SAFE_PRINT(Boot, L"Failed to create one or more tasks.\n\r");
return;
}
SAFE_PRINT(Boot, L"Created: %s (PID %d), %s (PID %d), %s (PID %d)\n\r",
t1->name, t1->pid, t2->name, t2->pid, t3->name, t3->pid);
SAFE_PRINT(Boot, L"\n\rYielding to let workers run:\n\r\n\r");
/* Yield enough times for all workers to complete (3 tasks x 3 steps) */
for (i = 0; i < 12; i++) {
task_yield();
}
SAFE_PRINT(Boot, L"\n\rTask list after test:\n\r");
task_print_list(Boot);
SAFE_PRINT(Boot, L"Task scheduler test completed.\n\r\n\r");
}
/* ----------------------------------------------------------------
* kusr run a command with escalated privilege
* ---------------------------------------------------------------- */
static void cmd_kusr(BootInfo *Boot, CHAR16 *Args)
{
Task *self;
TaskPrivilege saved_priv;
if (Args == NULL || Args[0] == L'\0') {
SAFE_PRINT(Boot, L"Usage: kusr <command> [args...]\n\r");
return;
}
self = task_current();
if (self == NULL) {
SAFE_PRINT(Boot, L"kusr: no task context available.\n\r");
return;
}
/* Elevate, dispatch, restore. */
saved_priv = task_get_privilege(self);
task_set_privilege(self, TASK_PRIV_KERNEL);
{
Task *cmd_task = execute_command(Boot, Args, TASK_PRIV_KERNEL);
if (cmd_task != NULL) {
task_wait(cmd_task);
}
}
task_set_privilege(self, saved_priv);
}
/* ================================================================
* Public API
* ================================================================ */
/* Print a formatted table of all registered commands. */
void show_help(BootInfo *Boot)
{
UINTN i = 0;
@@ -161,7 +521,7 @@ void show_help(BootInfo *Boot)
SAFE_PRINT(Boot, L"Available commands:\n\r");
SAFE_PRINT(Boot, L"\n\r");
// Find longest command name for formatting
/* Find the longest command name for column alignment */
for (i = 0; commands[i].name != NULL; i++) {
name_len = 0;
while (commands[i].name[name_len] != L'\0') {
@@ -175,7 +535,7 @@ void show_help(BootInfo *Boot)
for (i = 0; commands[i].name != NULL; i++) {
SAFE_PRINT(Boot, L" %s", commands[i].name);
// Add padding
/* Pad to align descriptions */
name_len = 0;
while (commands[i].name[name_len] != L'\0') {
name_len++;
@@ -192,51 +552,110 @@ void show_help(BootInfo *Boot)
SAFE_PRINT(Boot, L"\n\r");
}
void execute_command(BootInfo *Boot, CHAR16 *Input)
/*
* Parse a line of user input into command + arguments and dispatch
* by spawning a dedicated task for the matching handler. Unknown
* commands print an error.
*
* Returns:
* - Pointer to the spawned Task for the command, or NULL if the
* command was not found or had to run synchronously.
*/
Task *execute_command(BootInfo *Boot, CHAR16 *Input, TaskPrivilege caller_priv)
{
CHAR16 *cmd_start = NULL;
CHAR16 *args_start = NULL;
UINTN i = 0;
CommandTaskContext *ctx;
Task *t;
if (Boot == NULL || Input == NULL) {
return;
return NULL;
}
trim_spaces_inplace(Input);
if (Input[0] == L'\0') {
return;
return NULL;
}
// Split command and arguments
/* Split input into command and argument strings */
cmd_start = Input;
args_start = Input;
// Find first space
/* Advance past the command keyword */
while (*args_start != L'\0' && !is_space16(*args_start)) {
args_start++;
}
// If we found a space, null-terminate command and advance to args
/* NUL-terminate the command and skip leading whitespace in args */
if (*args_start != L'\0') {
*args_start = L'\0';
args_start++;
// Skip leading spaces in args
/* skip leading whitespace in args */
while (*args_start != L'\0' && is_space16(*args_start)) {
args_start++;
}
}
// Search for command
/* Look up and dispatch the command */
for (i = 0; commands[i].name != NULL; i++) {
if (ascii_streq_ci(cmd_start, commands[i].name)) {
commands[i].handler(Boot, args_start);
return;
/* Allocate a context block for the command task. */
ctx = (CommandTaskContext *)kmalloc(sizeof(CommandTaskContext));
if (ctx == NULL) {
SAFE_PRINT(Boot, L"Failed to allocate command context; running in core thread.\n\r");
commands[i].handler(Boot, args_start);
return NULL;
}
ctx->Boot = Boot;
ctx->handler = commands[i].handler;
wstrcpy16_local(ctx->args, args_start, sizeof(ctx->args) / sizeof(ctx->args[0]));
t = task_create_with_priv(commands[i].name,
command_task_entry,
ctx,
caller_priv);
if (t == NULL) {
SAFE_PRINT(Boot, L"Failed to create task for command '%s'; running in core thread.\n\r",
commands[i].name);
kfree(ctx);
commands[i].handler(Boot, args_start);
return NULL;
}
SAFE_PRINT(Boot, L"[starling] spawned '%s' as PID %d\n\r", t->name, t->pid);
return t;
}
}
// Command not found
/* Command not found */
SAFE_PRINT(Boot, L"Unknown command: %s\n\r", cmd_start);
SAFE_PRINT(Boot, L"Type 'help' for a list of available commands.\n\r");
return NULL;
}
/* ----------------------------------------------------------------
* Command task entry executes one command in its own task context
* ---------------------------------------------------------------- */
static void command_task_entry(void *arg)
{
CommandTaskContext *ctx = (CommandTaskContext *)arg;
BootInfo *Boot = NULL;
if (ctx == NULL) {
return;
}
Boot = ctx->Boot;
if (ctx->handler != NULL) {
ctx->handler(Boot, ctx->args);
}
/* Context was heap-allocated in execute_command. */
kfree(ctx);
}

View File

@@ -1,18 +1,42 @@
/*
* commands.h Interactive shell command interface.
*
* Defines the Command structure used by the command registry and the
* public API for command execution and help display.
*/
#ifndef COMMANDS_H
#define COMMANDS_H
#include "boot_info.h"
#include "task.h"
/* Handler function signature: receives BootInfo and any argument text. */
typedef void (*CommandHandlerFn)(BootInfo *Boot, CHAR16 *Args);
/*
* Command describes a single shell command.
*
* An array of these (terminated by a sentinel with name == NULL) forms
* the command registry in commands.c.
*/
typedef struct {
const CHAR16 *name;
const CHAR16 *description;
const CHAR16 *usage;
CommandHandlerFn handler;
const CHAR16 *name; /* command keyword (e.g. L"help") */
const CHAR16 *description; /* one-line summary for `help` */
const CHAR16 *usage; /* detailed text shown by `man` */
TaskPrivilege min_priv; /* minimum privilege required */
CommandHandlerFn handler; /* function that executes the cmd */
} Command;
void execute_command(BootInfo *Boot, CHAR16 *Input);
/* Parse and dispatch a line of user input.
*
* Returns:
* - Pointer to a Task representing the spawned command process,
* or NULL if the command was not found or ran synchronously.
*/
Task *execute_command(BootInfo *Boot, CHAR16 *Input, TaskPrivilege caller_priv);
/* Print a formatted list of all registered commands. */
void show_help(BootInfo *Boot);
#endif
#endif /* COMMANDS_H */

52
context_switch.S Normal file
View File

@@ -0,0 +1,52 @@
/*
* context_switch.S Cooperative context switch for x86-64.
*
* void context_switch(UINT64 *old_rsp, UINT64 new_rsp);
*
* %rdi = pointer to save current RSP (old task's PCB field)
* %rsi = RSP value to restore (new task's saved RSP)
*
* Saves callee-saved registers (rbp, rbx, r12-r15) and RFLAGS on
* the current stack, stores RSP into *old_rsp, loads new_rsp into
* RSP, restores registers, and returns into the new task's context.
*
* This is a standard cooperative switch: only callee-saved state
* needs preserving because the C calling convention guarantees that
* caller-saved registers are already handled by the compiler.
*/
.text
.global context_switch
/* ----------------------------------------------------------------
* Entry: save old context, load new context, return
* ---------------------------------------------------------------- */
context_switch:
/* Save callee-saved registers and flags on the current stack */
pushq %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
pushfq
/* Store current RSP into *old_rsp */
movq %rsp, (%rdi)
/* Switch to the new task's stack */
movq %rsi, %rsp
/* Restore callee-saved registers and flags from the new stack */
popfq
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
ret
.section .note.GNU-stack,"",@progbits

View File

@@ -0,0 +1,528 @@
## Starling Terminal and command pipeline
User interaction with the kernel is mediated by the **Starling Terminal** and a command registry implemented in `kernel.c` and `commands.c`.
- The **Starling Terminal** is a task that runs a readevalprint loop, reading keystrokes via `BootInfo` services and dispatching commands.
- The **command subsystem** maintains a table of commands, each with a name, description, usage string, minimum privilege level, and handler function.
- Each command typically runs in its own task so that long-running work does not block the terminal.
---
## Starling Terminal task
The Starling Terminal is implemented as a task entry function in `kernel.c`:
```47:163:/home/lochlan/Documents/Coding/c/os/kernel.c
static void starling_terminal_task(void *arg)
{
StarlingContext *ctx = (StarlingContext *)arg;
BootInfo *Boot = NULL;
KeyEvent Key;
KSTATUS Status;
UINTN read_errors = 0;
CHAR16 line[128];
UINTN len = 0;
UINTN depth = 0;
TaskPrivilege shell_priv;
if (ctx == NULL || ctx->Boot == NULL) {
return;
}
Boot = ctx->Boot;
depth = ctx->depth;
shell_priv = ctx->shell_priv;
SAFE_PRINT(Boot, L"\n\r[Starling Terminal depth %d, priv %d] ready.\n\r\n\r",
depth, (INT32)shell_priv);
SAFE_PRINT(Boot, L"starling> ");
while (TRUE) {
/* Try non-blocking read first; yield to other tasks while idle */
if (Boot->try_read_key != NULL) {
Status = Boot->try_read_key(&Key);
if (Status != 0) {
task_yield();
continue;
}
} else if (Boot->read_key != NULL) {
Status = Boot->read_key(&Key);
} else {
SAFE_PRINT(Boot, L"Console input unavailable.\n\r");
break;
}
if (Status != 0) {
read_errors++;
if (read_errors == 1 || (read_errors % 64) == 0) {
SAFE_PRINT(Boot, L"read_key failed (status=%ld)\n\r",
(UINT64)Status);
}
continue;
}
read_errors = 0;
if (Key.unicode_char == L'\r' || Key.unicode_char == L'\n') {
/* Enter pressed: execute the buffered command */
line[len] = L'\0';
SAFE_PRINT(Boot, L"\n\r");
trim_spaces_inplace(line);
...
} else {
Task *cmd_task = execute_command(Boot, line, shell_priv);
/* If a command task was spawned, wait for it to finish. */
if (cmd_task != NULL) {
task_wait(cmd_task);
}
/* Reset for next command */
len = 0;
SAFE_PRINT(Boot, L"starling> ");
}
} else if (Key.scan_code == 0x08 || Key.unicode_char == L'\b' || Key.unicode_char == 0x7F) {
/* Backspace */
if (len > 0) {
len--;
SAFE_PRINT(Boot, L"\b \b");
}
} else if (Key.unicode_char >= 32 && Key.unicode_char < 127) {
/* Printable ASCII */
if (len < (sizeof(line) / sizeof(line[0]) - 1)) {
line[len++] = Key.unicode_char;
SAFE_PRINT(Boot, L"%c", Key.unicode_char);
}
}
}
/* Free our context on exit (allocated by the spawner). */
kfree(ctx);
}
```
Notable design choices:
- **Non-blocking polling**: When `Boot->try_read_key` is available, the terminal uses it and calls `task_yield` if no key is present. This avoids monopolising the CPU while idle.
- **Line editing**: A fixed-size buffer `line[128]` accumulates ASCII characters. Backspace decrements `len` and erases the last character on screen.
- **Command execution**: When the user presses Enter, the line is trimmed and either:
- Handled as a built-in shell control command (`exit`, `starling`).
- Sent to `execute_command` for lookup and execution.
- **Nested shells**: The `starling` command spawns a nested Starling Terminal task with increased `depth`, allowing recursive shells.
---
## Spawning the terminal
`kmain` spawns the initial Starling Terminal as its own task and then turns the core thread into an idle loop:
```221:253:/home/lochlan/Documents/Coding/c/os/kernel.c
ctx = (StarlingContext *)kmalloc(sizeof(StarlingContext));
if (ctx == NULL) {
SAFE_PRINT(Boot, L"Failed to allocate Starling Terminal context; starting inline.\n\r");
StarlingContext inline_ctx;
inline_ctx.Boot = Boot;
inline_ctx.depth = 0;
inline_ctx.shell_priv = TASK_PRIV_USER;
starling_terminal_task(&inline_ctx);
return;
}
ctx->Boot = Boot;
ctx->depth = 0;
ctx->shell_priv = TASK_PRIV_USER;
terminal_task = task_create_with_priv(L"starling-term",
starling_terminal_task,
ctx,
TASK_PRIV_USER);
if (terminal_task == NULL) {
SAFE_PRINT(Boot, L"Failed to start Starling Terminal task; falling back to kernel loop.\n\r");
...
starling_terminal_task(Boot);
return;
}
SAFE_PRINT(Boot, L"[core] Started Starling Terminal (PID %d).\n\r", terminal_task->pid);
/* Core thread becomes an idle loop, yielding to the terminal and others. */
while (TRUE) {
task_yield();
}
```
This ensures that:
- The terminal runs as a **regular task** managed by the cooperative scheduler.
- The core thread remains available to run other tasks or future subsystems, rather than being permanently blocked in terminal I/O.
---
## Command registry (`commands[]`)
The command registry is defined in `commands.c` as a static array:
```73:164:/home/lochlan/Documents/Coding/c/os/commands.c
static Command commands[] = {
{
L"shutdown",
L"Shutdown the system",
L"Usage: shutdown\n\r Initiates a system shutdown using UEFI runtime services.",
TASK_PRIV_KERNEL,
cmd_shutdown
},
{
L"help",
L"Display available commands",
L"Usage: help\n\r Lists all available commands with brief descriptions.",
TASK_PRIV_USER,
cmd_help
},
{
L"man",
L"Display manual page for a command",
L"Usage: man <command>\n\r Shows detailed help for the specified command.",
TASK_PRIV_USER,
cmd_man
},
{
L"clear",
L"Clear the screen",
L"Usage: clear\n\r Clears the console screen.",
TASK_PRIV_USER,
cmd_clear
},
{
L"about",
L"Display system information",
L"Usage: about\n\r Shows information about this operating system.",
TASK_PRIV_USER,
cmd_about
},
{
L"mem",
L"Display memory statistics",
L"Usage: mem\n\r Shows physical memory, heap, and paging information.",
TASK_PRIV_KERNEL,
cmd_mem
},
{
L"ps",
L"List running tasks",
L"Usage: ps\n\r Displays all active tasks with PID, state, and name.",
TASK_PRIV_DRIVER,
cmd_ps
},
{
L"spawn",
L"Spawn a demo background task",
L"Usage: spawn [name]\n\r Creates a cooperative demo task.\n\r Optional argument sets the task name.",
TASK_PRIV_DRIVER,
cmd_spawn
},
{
L"memtest",
L"Test memory allocation and deallocation",
...
TASK_PRIV_KERNEL,
cmd_memtest
},
{
L"tasktest",
L"Test task scheduler with multiple tasks",
...
TASK_PRIV_DRIVER,
cmd_tasktest
},
{
L"kusr",
L"Run a command with kernel privilege",
L"Usage: kusr <command> [args...]\n\r"
L" Temporarily elevates the current task to kernel privilege,\n\r"
L" executes the given command, then restores the original level.",
TASK_PRIV_USER,
cmd_kusr
},
{NULL, NULL, NULL, 0, NULL} /* sentinel */
};
```
Each `Command` entry includes:
- `name` the token typed at the prompt.
- `description` a short summary used by `help`.
- `usage` a longer description and usage details for `man`.
- `min_priv` the minimum `TaskPrivilege` required to run the command (see `task.h`).
- `handler` a function of type:
```14:19:/home/lochlan/Documents/Coding/c/os/commands.h
typedef void (*CommandHandlerFn)(BootInfo *Boot, CHAR16 *Args);
```
To add a new command, follow the guide in the file header:
```8:12:/home/lochlan/Documents/Coding/c/os/commands.c
* To add a new command:
* 1. Write a static handler cmd_foo(BootInfo *Boot, CHAR16 *Args)
* 2. Add a forward declaration above the table
* 3. Append an entry to commands[] (before the sentinel)
```
---
## Command execution pipeline
The central function that processes a line of user input is `execute_command`:
```493:557:/home/lochlan/Documents/Coding/c/os/commands.c
Task *execute_command(BootInfo *Boot, CHAR16 *Input, TaskPrivilege caller_priv)
{
CHAR16 *cmd_start = NULL;
CHAR16 *args_start = NULL;
UINTN i = 0;
CommandTaskContext *ctx;
Task *t;
if (Boot == NULL || Input == NULL) {
return NULL;
}
trim_spaces_inplace(Input);
if (Input[0] == L'\0') {
return NULL;
}
/* Split input into command and argument strings */
cmd_start = Input;
args_start = Input;
/* Advance past the command keyword */
while (*args_start != L'\0' && !is_space16(*args_start)) {
args_start++;
}
/* NUL-terminate the command and skip leading whitespace in args */
if (*args_start != L'\0') {
*args_start = L'\0';
args_start++;
/* skip leading whitespace in args */
while (*args_start != L'\0' && is_space16(*args_start)) {
args_start++;
}
}
/* Look up and dispatch the command */
for (i = 0; commands[i].name != NULL; i++) {
if (ascii_streq_ci(cmd_start, commands[i].name)) {
/* Allocate a context block for the command task. */
ctx = (CommandTaskContext *)kmalloc(sizeof(CommandTaskContext));
if (ctx == NULL) {
SAFE_PRINT(Boot, L"Failed to allocate command context; running in core thread.\n\r");
commands[i].handler(Boot, args_start);
return NULL;
}
ctx->Boot = Boot;
ctx->handler = commands[i].handler;
wstrcpy16_local(ctx->args, args_start, sizeof(ctx->args) / sizeof(ctx->args[0]));
t = task_create_with_priv(commands[i].name,
command_task_entry,
ctx,
caller_priv);
if (t == NULL) {
SAFE_PRINT(Boot, L"Failed to create task for command '%s'; running in core thread.\n\r",
commands[i].name);
kfree(ctx);
commands[i].handler(Boot, args_start);
return NULL;
}
SAFE_PRINT(Boot, L"[starling] spawned '%s' as PID %d\n\r", t->name, t->pid);
return t;
}
}
/* Command not found */
SAFE_PRINT(Boot, L"Unknown command: %s\n\r", cmd_start);
SAFE_PRINT(Boot, L"Type 'help' for a list of available commands.\n\r");
return NULL;
}
```
Pipeline stages:
1. **Normalisation**:
- `trim_spaces_inplace` removes leading/trailing spaces.
- Empty lines are ignored.
2. **Tokenisation**:
- `cmd_start` points to the command token.
- `args_start` is advanced past the command; the first whitespace is replaced with `L'\0'`, splitting the string in-place.
- Leading whitespace in `args_start` is skipped.
3. **Lookup**:
- `commands[]` is scanned for a name that matches `cmd_start` using case-insensitive `ascii_streq_ci`.
4. **Dispatch**:
- On match, a `CommandTaskContext` is allocated via `kmalloc` and filled with:
- `Boot` pointer.
- Handler function.
- A bounded copy of the argument string.
- A new task is created via `task_create_with_priv` with:
- Task name = command name.
- Entry = `command_task_entry`.
- Argument = pointer to the context.
- Privilege = `caller_priv` (inherited from the calling shell).
- If task creation fails, the command handler is run synchronously in the current thread as a fallback.
The terminal then optionally `task_wait`s on the returned `Task *`, serialising command execution from the user's perspective while still letting the scheduler run other tasks (e.g., background demos).
---
## Command task entry and context
Command handlers are executed in a dedicated task, whose entry function is `command_task_entry`:
```44:50:/home/lochlan/Documents/Coding/c/os/commands.c
typedef struct {
BootInfo *Boot;
CommandHandlerFn handler;
CHAR16 args[128];
} CommandTaskContext;
static void command_task_entry(void *arg);
```
Implementation:
```570:587:/home/lochlan/Documents/Coding/c/os/commands.c
static void command_task_entry(void *arg)
{
CommandTaskContext *ctx = (CommandTaskContext *)arg;
BootInfo *Boot = NULL;
if (ctx == NULL) {
return;
}
Boot = ctx->Boot;
if (ctx->handler != NULL) {
ctx->handler(Boot, ctx->args);
}
/* Context was heap-allocated in execute_command. */
kfree(ctx);
}
```
This design gives each command:
- Its own stack, independent of the terminal.
- Its own argument buffer, isolated from the terminal's input buffer.
- Automatic cleanup of the context when the command finishes.
---
## Built-in commands
Some notable built-in handlers:
- **System control**:
- `shutdown` (KERNEL) → `cmd_shutdown` calls `Boot->shutdown` via `request_shutdown` to power off the machine. `request_shutdown` enforces kernel privilege.
- `clear` (USER) → `cmd_clear` uses `Boot->clear_screen` to wipe the console.
- **Information and help**:
- `help` (USER) → `cmd_help` calls `show_help` to print a formatted table of available commands.
- `man` (USER) → `cmd_man` prints the `usage` field for a specific command.
- `about` (USER) → `cmd_about` prints OS information and feature list.
- **Diagnostics**:
- `mem` (KERNEL) → `cmd_mem` calls `memory_print_stats` to show PMM and heap state. The callee enforces kernel privilege.
- `ps` (DRIVER) → `cmd_ps` calls `task_print_list` to show current tasks. The callee enforces driver privilege.
- `memtest` (KERNEL) → `cmd_memtest` exercises heap and PMM allocations. The handler enforces kernel privilege.
- `tasktest` (DRIVER) → `cmd_tasktest` spawns multiple worker tasks to demonstrate cooperative scheduling.
- **Tasking demo**:
- `spawn` (DRIVER) → `cmd_spawn` creates a demonstration task using `demo_task_fn`, which yields in a loop and reports progress.
- **Privilege escalation**:
- `kusr` (USER) → `cmd_kusr` temporarily elevates the calling task to `TASK_PRIV_KERNEL`, dispatches the given sub-command, then restores the original privilege level.
Examples:
```248:252:/home/lochlan/Documents/Coding/c/os/commands.c
static void cmd_mem(BootInfo *Boot, CHAR16 *Args)
{
(void)Args;
memory_print_stats(Boot);
}
```
```275:279:/home/lochlan/Documents/Coding/c/os/commands.c
static void cmd_ps(BootInfo *Boot, CHAR16 *Args)
{
(void)Args;
task_print_list(Boot);
}
```
From a user's perspective:
- Commands are discoverable via `help` and `man`.
- Many commands provide deep insight into internal subsystems (memory, tasks) without requiring external tooling.
---
## Adding new commands
To add a new command `foo`:
1. **Declare the handler** near the top of `commands.c`:
```32:42:/home/lochlan/Documents/Coding/c/os/commands.c
static void cmd_shutdown(BootInfo *Boot, CHAR16 *Args);
static void cmd_help(BootInfo *Boot, CHAR16 *Args);
...
static void cmd_tasktest(BootInfo *Boot, CHAR16 *Args);
static void cmd_kusr(BootInfo *Boot, CHAR16 *Args);
/* Add: */
static void cmd_foo(BootInfo *Boot, CHAR16 *Args);
```
2. **Implement the handler**:
```200:207:/home/lochlan/Documents/Coding/c/os/commands.c
static void cmd_shutdown(BootInfo *Boot, CHAR16 *Args)
{
(void)Args;
SAFE_PRINT(Boot, L"Shutting down...\n\r");
request_shutdown(Boot);
}
```
Use this as a template for `cmd_foo`, replacing the body with your logic and using `SAFE_PRINT` for output.
3. **Register the command** in `commands[]` before the sentinel:
```140:164:/home/lochlan/Documents/Coding/c/os/commands.c
{
L"kusr",
...
TASK_PRIV_USER,
cmd_kusr
},
{NULL, NULL, NULL, 0, NULL} /* sentinel */
```
Insert a new block above the sentinel:
```c
{
L"foo",
L"One-line description",
L"Usage: foo [args]\n\r Detailed explanation...",
TASK_PRIV_USER, /* minimum privilege required */
cmd_foo
},
```
4. Rebuild and run. Typing `foo` at the `starling>` prompt will now execute your handler in its own task.
This extensible design makes it straightforward to grow the OS with additional diagnostics, demos, or experimental subsystems, all accessible from the interactive shell.

View File

@@ -0,0 +1,269 @@
## Interrupt and exception handling overview
The kernel's interrupt/exception handling is implemented in `idt.c` and a companion assembly file `isr.S` (not shown here). The design goals are:
- Preserve the firmware's existing hardware interrupt handlers where possible.
- Override CPU **exception vectors 031** with kernel stubs that route into a C dispatcher.
- Print detailed diagnostics for exceptions and halt on unrecoverable faults.
---
## IDT representation
The x86-64 Interrupt Descriptor Table (IDT) is represented as an array of packed 16-byte entries:
```22:37:/home/lochlan/Documents/Coding/c/os/idt.c
typedef struct {
UINT16 offset_low; /* bits 0-15 of handler address */
UINT16 selector; /* code segment selector */
UINT8 ist; /* interrupt stack table index */
UINT8 type_attr; /* type and attributes */
UINT16 offset_mid; /* bits 16-31 of handler address */
UINT32 offset_high; /* bits 32-63 of handler address */
UINT32 zero; /* reserved, must be zero */
} __attribute__((packed)) IdtEntry;
typedef struct {
UINT16 limit;
UINT64 base;
} __attribute__((packed)) IdtPtr;
```
Module state:
```42:47:/home/lochlan/Documents/Coding/c/os/idt.c
static IdtEntry idt[IDT_SIZE];
static BootInfo *gBoot = NULL;
/* Defined in isr.S one stub function per vector (0-255). */
extern void (*isr_stub_table[])(void);
```
Each element of `isr_stub_table` is a low-level assembly stub that:
- Saves CPU state into an `ISRFrame`.
- Calls the common C dispatcher `isr_handler`.
- Restores state and performs `iretq` (for IRQs) or loops into a halt (for fatal faults).
---
## Installing IDT entries
`idt_set_gate` encodes a handler function pointer into an IDT entry:
```53:67:/home/lochlan/Documents/Coding/c/os/idt.c
static void idt_set_gate(UINTN index, void (*handler)(void))
{
UINT64 addr = (UINT64)(UINTN)handler;
UINT16 selector = 0;
__asm__ __volatile__("mov %%cs, %0" : "=r"(selector));
idt[index].offset_low = (UINT16)(addr & 0xFFFF);
idt[index].selector = selector;
idt[index].ist = 0;
idt[index].type_attr = IDT_TYPE_INTERRUPT;
idt[index].offset_mid = (UINT16)((addr >> 16) & 0xFFFF);
idt[index].offset_high = (UINT32)((addr >> 32) & 0xFFFFFFFF);
idt[index].zero = 0;
}
```
`lidt` loads a new IDTR:
```69:73:/home/lochlan/Documents/Coding/c/os/idt.c
static void lidt(const IdtPtr *idtr)
{
__asm__ __volatile__("lidt (%0)" :: "r"(idtr));
}
```
---
## Exception names
For better diagnostics, the kernel maps exception vectors to human-readable names:
```80:116:/home/lochlan/Documents/Coding/c/os/idt.c
static const CHAR16 *exception_name(UINTN vector)
{
switch (vector) {
case 0: return L"Divide Error";
case 1: return L"Debug";
...
case 14: return L"Page Fault";
...
case 30: return L"Security";
case 31: return L"Reserved";
default: return L"Unknown";
}
}
```
These strings are used by `isr_handler` when printing exception banners.
---
## PIC helpers and halting
The kernel provides minimal support for acknowledging legacy PIC interrupts and halting the CPU in fatal cases:
```124:136:/home/lochlan/Documents/Coding/c/os/idt.c
static inline void outb(UINT16 port, UINT8 value)
{
__asm__ __volatile__("outb %0, %1" :: "a"(value), "Nd"(port));
}
static void pic_eoi(UINTN vector)
{
if (vector >= 40) {
outb(0xA0, 0x20); /* EOI to slave PIC */
}
outb(0x20, 0x20); /* EOI to master PIC */
}
```
`halt_forever` disables interrupts and executes `hlt` in an infinite loop:
```138:144:/home/lochlan/Documents/Coding/c/os/idt.c
static void halt_forever(void)
{
for (;;) {
__asm__ __volatile__("cli; hlt");
}
}
```
---
## ISR dispatcher (`isr_handler`)
`isr_handler` is the central C function that receives control from all exception and interrupt stubs:
```150:177:/home/lochlan/Documents/Coding/c/os/idt.c
void isr_handler(ISRFrame *frame)
{
UINT64 cr2 = 0;
/* Hardware IRQs (vectors 32-47): send EOI and return */
if (frame->vector >= 32 && frame->vector <= 47) {
pic_eoi(frame->vector);
return;
}
/* CPU exceptions (vectors 0-31): print diagnostics and halt */
if (gBoot != NULL && gBoot->print != NULL) {
gBoot->print(L"\n\rEXCEPTION: %d (%s)\n\r", frame->vector,
exception_name(frame->vector));
gBoot->print(L" Error Code: 0x%lx\n\r", frame->error_code);
gBoot->print(L" RIP: 0x%lx CS: 0x%lx RFLAGS: 0x%lx\n\r",
frame->rip, frame->cs, frame->rflags);
}
if (frame->vector == 14) {
__asm__ __volatile__("mov %%cr2, %0" : "=r"(cr2));
if (gBoot != NULL && gBoot->print != NULL) {
gBoot->print(L" CR2: 0x%lx\n\r", cr2);
}
}
halt_forever();
}
```
The `ISRFrame` structure (defined in `idt.h`) contains the state saved by the assembly stubs, including:
- Exception/interrupt vector number.
- Error code (if applicable).
- `RIP`, `CS`, and `RFLAGS` at the time of the fault.
The handler's behaviour is:
- For **hardware IRQs** (vectors 3247):
- Send an **End Of Interrupt** (EOI) to the PIC.
- Return to the interrupted context.
- For **CPU exceptions** (vectors 031):
- Print a diagnostic header with the vector number and name.
- Show error code and execution context (`RIP`, `CS`, `RFLAGS`).
- For page faults (vector 14), read and print CR2 (faulting virtual address).
- Halt the system via `halt_forever`.
This makes debugging faults significantly easier when running under QEMU or on real hardware.
---
## IDT initialisation (`idt_init`)
`idt_init` is called from `kmain` early during boot:
```183:214:/home/lochlan/Documents/Coding/c/os/idt.c
void idt_init(BootInfo *Boot)
{
IdtPtr old_idtr;
IdtPtr idtr;
IdtEntry *old_idt = NULL;
UINTN i = 0;
gBoot = Boot;
/* Read the firmware's existing IDT so we can preserve its entries */
__asm__ __volatile__("sidt %0" : "=m"(old_idtr));
old_idt = (IdtEntry *)(UINTN)old_idtr.base;
/* Copy the entire existing IDT first (preserves firmware IRQ handlers) */
for (i = 0; i < IDT_SIZE; i++) {
if (old_idt != NULL && (i * sizeof(IdtEntry)) < (UINTN)(old_idtr.limit + 1)) {
idt[i] = old_idt[i];
} else {
idt_set_gate(i, isr_stub_table[i]);
}
}
/* Override only CPU exception vectors (0-31) with our handlers */
for (i = 0; i < 32; i++) {
idt_set_gate(i, isr_stub_table[i]);
}
idtr.limit = (UINT16)(sizeof(idt) - 1);
idtr.base = (UINT64)(UINTN)idt;
lidt(&idtr);
}
```
The process is:
1. **Capture firmware IDT**:
- `sidt` reads the current IDTR into `old_idtr`.
- `old_idt` is set to the base of the firmware's IDT.
2. **Copy firmware entries**:
- For all indices `i` where the address lies within the firmware's IDT limit, copy the existing entry into the kernel's `idt` array.
- For indices beyond the firmware's limit, install the kernel's own stub from `isr_stub_table`.
3. **Override CPU exceptions**:
- For vectors 031, call `idt_set_gate` with the kernel's stubs, ensuring that exceptions are always handled by `isr_handler`.
4. **Activate new IDT**:
- Populate `idtr` with the address and size of the kernel's `idt`.
- Call `lidt` to load the new IDT.
This approach preserves any firmware-installed handlers for higher interrupt vectors (e.g., hardware IRQs or system-specific events), while guaranteeing full control over CPU exception handling.
---
## Interaction with the rest of the kernel
The IDT/ISR subsystem interacts with other parts of the kernel in the following ways:
- **BootInfo access**:
- `idt_init` stores `Boot` in `gBoot` so that `isr_handler` can safely use `Boot->print` for diagnostics.
- **Memory subsystem**:
- `isr_handler` reads CR2 for page faults; combined with `paging_get_phys` from `memory.c`, this can be used to inspect paging state.
- **Tasks and scheduler**:
- The current implementation is **non-preemptive**: context switches happen only through explicit calls to `task_yield`, not timer interrupts. Exceptions still interrupt tasks asynchronously, but there is no timer tick driving the scheduler.
- **User-level diagnostics**:
- When an exception occurs, the on-screen diagnostics provide enough context to identify the type of fault and its location in the kernel, especially when used alongside a symbol-enabled build and external debugger.
Future extensions might include:
- Installing a periodic timer IRQ handler that calls `task_yield` to add preemptive scheduling.
- Extending `ISRFrame` and `isr_handler` with richer diagnostics or a kernel debugger stub.

View File

@@ -0,0 +1,638 @@
## Memory management overview
The kernel's memory subsystem is implemented in `memory.c` and exposes three layers:
- **Physical Memory Manager (PMM)** a bitmap-based page-frame allocator over a fixed-size pool obtained from the loader at boot.
- **Paging helpers** routines to walk and extend the live 4-level x86-64 page tables, map/unmap virtual addresses, and translate virtual to physical.
- **Heap allocator** a first-fit free-list allocator with block splitting and bidirectional coalescing, backed by pages from the PMM.
All three layers are wired together and brought up by `memory_init`:
```515:522:/home/lochlan/Documents/Coding/c/os/memory.c
void memory_init(BootInfo *Boot)
{
SAFE_PRINT(Boot, L"Initializing memory management...\n\r");
pmm_init(Boot);
paging_init(Boot);
heap_init(Boot);
SAFE_PRINT(Boot, L"Memory management ready.\n\r\n\r");
}
```
---
## Physical Memory Manager (PMM)
### Design
The PMM manages a pool of 4 KiB physical page frames acquired from the loader via `BootInfo->alloc_pages`. It uses a simple **bitmap** to track free vs. allocated pages:
```27:33:/home/lochlan/Documents/Coding/c/os/memory.c
static UINT64 pmm_pool_base = 0;
static UINTN pmm_total_pages = 0;
static UINTN pmm_free_count = 0;
static UINT8 pmm_bitmap[PMM_POOL_PAGES / 8];
static BOOLEAN pmm_ready = FALSE;
```
Each bit in `pmm_bitmap` corresponds to a single page in the pool:
- **0** page is free.
- **1** page is allocated.
Helper functions manipulate these bits:
```37:53:/home/lochlan/Documents/Coding/c/os/memory.c
static void pmm_set_bit(UINTN idx)
{
pmm_bitmap[idx / 8] |= (UINT8)(1U << (idx % 8));
}
static void pmm_clear_bit(UINTN idx)
{
pmm_bitmap[idx / 8] &= (UINT8)~(1U << (idx % 8));
}
static BOOLEAN pmm_test_bit(UINTN idx)
{
return (pmm_bitmap[idx / 8] & (1U << (idx % 8))) != 0;
}
```
### Initialisation
`pmm_init` obtains the underlying page pool from the loader and prepares the bitmap:
```64:96:/home/lochlan/Documents/Coding/c/os/memory.c
void pmm_init(BootInfo *Boot)
{
KSTATUS Status;
UINT64 pool_addr = 0;
UINTN i;
/* Zero the bitmap all pages start free */
for (i = 0; i < sizeof(pmm_bitmap); i++) {
pmm_bitmap[i] = 0;
}
if (Boot == NULL || Boot->alloc_pages == NULL) {
SAFE_PRINT(Boot, L"PMM: page allocator unavailable\n\r");
return;
}
Status = Boot->alloc_pages(PMM_POOL_PAGES, &pool_addr);
...
pmm_pool_base = (UINT64)pool_addr;
pmm_total_pages = PMM_POOL_PAGES;
pmm_free_count = PMM_POOL_PAGES;
pmm_ready = TRUE;
SAFE_PRINT(Boot, L" PMM : %d pages (%d KB) at 0x%lx\n\r",
pmm_total_pages,
(pmm_total_pages * PAGE_SIZE) / 1024,
pmm_pool_base);
}
```
Instead of parsing the firmware's memory map, this OS delegates low-level page allocation to the loader via `BootInfo->alloc_pages`. The PMM then **sub-allocates** from this contiguous pool using its own bitmap.
### Single-page allocation
`pmm_alloc_page` scans the bitmap for the first free page, marks it allocated, and returns the physical address:
```98:116:/home/lochlan/Documents/Coding/c/os/memory.c
UINT64 pmm_alloc_page(void)
{
UINTN i;
if (!pmm_ready || pmm_free_count == 0) {
return 0;
}
for (i = 0; i < pmm_total_pages; i++) {
if (!pmm_test_bit(i)) {
pmm_set_bit(i);
pmm_free_count--;
return pmm_pool_base + ((UINT64)i * PAGE_SIZE);
}
}
return 0;
}
```
The corresponding free operation validates the address and clears the bit:
```119:132:/home/lochlan/Documents/Coding/c/os/memory.c
void pmm_free_page(UINT64 phys_addr)
{
UINTN idx;
if (!pmm_ready) return;
if (phys_addr < pmm_pool_base) return;
idx = (UINTN)((phys_addr - pmm_pool_base) / PAGE_SIZE);
if (idx >= pmm_total_pages) return;
if (!pmm_test_bit(idx)) return; /* already free */
pmm_clear_bit(idx);
pmm_free_count++;
}
```
### Contiguous allocation
For multi-page allocations, `pmm_alloc_pages` performs a **first-fit** search for a run of `count` consecutive free bits:
```134:163:/home/lochlan/Documents/Coding/c/os/memory.c
UINT64 pmm_alloc_pages(UINTN count)
{
UINTN i, j;
BOOLEAN found;
if (!pmm_ready || count == 0 || count > pmm_total_pages
|| pmm_free_count < count) {
return 0;
}
for (i = 0; i + count <= pmm_total_pages; i++) {
found = TRUE;
for (j = 0; j < count; j++) {
if (pmm_test_bit(i + j)) {
found = FALSE;
i += j; /* skip past the used page */
break;
}
}
if (found) {
for (j = 0; j < count; j++) {
pmm_set_bit(i + j);
}
pmm_free_count -= count;
return pmm_pool_base + ((UINT64)i * PAGE_SIZE);
}
}
return 0;
}
```
`pmm_free_pages` simply calls `pmm_free_page` for each page in the range.
---
## Paging helpers
The paging layer operates directly on the current CR3 page table hierarchy and uses the PMM to allocate new page-table pages on demand.
### Reading CR3 and locating the PML4
```186:204:/home/lochlan/Documents/Coding/c/os/memory.c
static UINT64 read_cr3(void)
{
UINT64 cr3;
__asm__ __volatile__("mov %%cr3, %0" : "=r"(cr3));
return cr3;
}
static void invlpg(UINT64 addr)
{
__asm__ __volatile__("invlpg (%0)" :: "r"(addr) : "memory");
}
static UINT64 *get_pml4(void)
{
return (UINT64 *)(UINTN)(read_cr3() & PTE_ADDR_MASK);
}
```
- `read_cr3` returns the physical address of the current PML4.
- `get_pml4` masks off flag bits using `PTE_ADDR_MASK` and casts the result to a pointer, assuming identity mapping of low physical memory (as set up by the loader).
`paging_init` logs the initial CR3 value for diagnostic purposes:
```244:249:/home/lochlan/Documents/Coding/c/os/memory.c
void paging_init(BootInfo *Boot)
{
SAFE_PRINT(Boot, L" Page: CR3 = 0x%lx (identity-mapped by loader)\n\r",
read_cr3());
}
```
### Walking page-table levels
`paging_walk_level` abstracts a single step down the PML4 → PDPT → PD → PT hierarchy:
```211:238:/home/lochlan/Documents/Coding/c/os/memory.c
static UINT64 *paging_walk_level(UINT64 *table, UINTN index, BOOLEAN create)
{
UINT64 *next;
UINTN i;
UINT64 page;
if (table[index] & PTE_PRESENT) {
return (UINT64 *)(UINTN)(table[index] & PTE_ADDR_MASK);
}
if (!create) {
return NULL;
}
page = pmm_alloc_page();
if (page == 0) {
return NULL;
}
/* Zero the freshly-allocated page table */
next = (UINT64 *)(UINTN)page;
for (i = 0; i < PAGE_SIZE / sizeof(UINT64); i++) {
next[i] = 0;
}
table[index] = page | PTE_PRESENT | PTE_WRITABLE;
return next;
}
```
If `create` is true and the entry is missing, it:
- Allocates a fresh page with `pmm_alloc_page`.
- Clears it.
- Installs it as the next-level table with base address + default flags (`PTE_PRESENT | PTE_WRITABLE`).
### Mapping and unmapping pages
To map a single 4 KiB page, the kernel:
1. Decomposes the virtual address into PML4/PDPT/PD/PT indices.
2. Walks or creates intermediate tables.
3. Installs a PTE with the desired flags.
4. Invalidates the TLB entry with `invlpg`.
```256:285:/home/lochlan/Documents/Coding/c/os/memory.c
BOOLEAN paging_map_page(UINT64 virt, UINT64 phys, UINT64 flags)
{
UINT64 *pml4, *pdpt, *pd, *pt;
UINTN pml4i, pdpti, pdi, pti;
pml4i = (virt >> 39) & 0x1FF;
pdpti = (virt >> 30) & 0x1FF;
pdi = (virt >> 21) & 0x1FF;
pti = (virt >> 12) & 0x1FF;
pml4 = get_pml4();
pdpt = paging_walk_level(pml4, pml4i, TRUE);
if (pdpt == NULL) return FALSE;
/* 1 GB huge page cannot carve a 4 KB mapping inside it */
if (pdpt[pdpti] & PTE_HUGE) return FALSE;
pd = paging_walk_level(pdpt, pdpti, TRUE);
if (pd == NULL) return FALSE;
/* 2 MB huge page cannot carve a 4 KB mapping inside it */
if (pd[pdi] & PTE_HUGE) return FALSE;
pt = paging_walk_level(pd, pdi, TRUE);
if (pt == NULL) return FALSE;
pt[pti] = (phys & PTE_ADDR_MASK) | flags | PTE_PRESENT;
invlpg(virt);
return TRUE;
}
```
Unmapping follows the same index computation but stops early if an intermediate table or mapping is missing or a huge-page mapping is in place:
```288:314:/home/lochlan/Documents/Coding/c/os/memory.c
void paging_unmap_page(UINT64 virt)
{
UINT64 *pml4, *pdpt, *pd, *pt;
UINTN pml4i, pdpti, pdi, pti;
pml4i = (virt >> 39) & 0x1FF;
pdpti = (virt >> 30) & 0x1FF;
pdi = (virt >> 21) & 0x1FF;
pti = (virt >> 12) & 0x1FF;
pml4 = get_pml4();
pdpt = paging_walk_level(pml4, pml4i, FALSE);
if (pdpt == NULL) return;
if (pdpt[pdpti] & PTE_HUGE) return;
pd = paging_walk_level(pdpt, pdpti, FALSE);
if (pd == NULL) return;
if (pd[pdi] & PTE_HUGE) return;
pt = paging_walk_level(pd, pdi, FALSE);
if (pt == NULL) return;
pt[pti] = 0;
invlpg(virt);
}
```
### Virtual-to-physical translation
`paging_get_phys` walks the existing hierarchy without allocating anything, and supports 4 KiB, 2 MiB, and 1 GiB mappings:
```320:351:/home/lochlan/Documents/Coding/c/os/memory.c
UINT64 paging_get_phys(UINT64 virt)
{
UINT64 *pml4, *pdpt, *pd, *pt;
UINTN pml4i, pdpti, pdi, pti;
pml4i = (virt >> 39) & 0x1FF;
pdpti = (virt >> 30) & 0x1FF;
pdi = (virt >> 21) & 0x1FF;
pti = (virt >> 12) & 0x1FF;
pml4 = get_pml4();
if (!(pml4[pml4i] & PTE_PRESENT)) return 0;
pdpt = (UINT64 *)(UINTN)(pml4[pml4i] & PTE_ADDR_MASK);
if (!(pdpt[pdpti] & PTE_PRESENT)) return 0;
if (pdpt[pdpti] & PTE_HUGE) {
/* 1 GB page */
return (pdpt[pdpti] & 0x000FFFFFC0000000ULL) | (virt & 0x3FFFFFFFULL);
}
pd = (UINT64 *)(UINTN)(pdpt[pdpti] & PTE_ADDR_MASK);
if (!(pd[pdi] & PTE_PRESENT)) return 0;
if (pd[pdi] & PTE_HUGE) {
/* 2 MB page */
return (pd[pdi] & 0x000FFFFFFFE00000ULL) | (virt & 0x1FFFFFULL);
}
pt = (UINT64 *)(UINTN)(pd[pdi] & PTE_ADDR_MASK);
if (!(pt[pti] & PTE_PRESENT)) return 0;
return (pt[pti] & PTE_ADDR_MASK) | (virt & 0xFFFULL);
}
```
This function is useful for diagnostics and for checking assumptions about how the firmware identity-mapped memory before entering the kernel.
---
## Heap allocator
The heap allocator builds on top of the PMM to provide `kmalloc`/`kfree` semantics. It uses a singly linked list of **heap blocks** (`HeapBlock`), each containing metadata and a `size` field describing the payload.
### Initialisation
`heap_init` obtains an initial contiguous region of heap memory and seeds the free list with a single large free block:
```370:394:/home/lochlan/Documents/Coding/c/os/memory.c
void heap_init(BootInfo *Boot)
{
UINT64 phys;
UINTN heap_size;
phys = pmm_alloc_pages(HEAP_INITIAL_PAGES);
if (phys == 0) {
SAFE_PRINT(Boot, L" Heap: failed to allocate pages\n\r");
return;
}
heap_size = HEAP_INITIAL_PAGES * PAGE_SIZE;
heap_start = (HeapBlock *)(UINTN)phys;
heap_start->magic = HEAP_BLOCK_MAGIC;
heap_start->state = HEAP_BLOCK_FREE;
heap_start->size = heap_size - sizeof(HeapBlock);
heap_start->next = NULL;
heap_start->prev = NULL;
heap_ready = TRUE;
SAFE_PRINT(Boot, L" Heap: %d KB at 0x%lx\n\r",
heap_size / 1024, phys);
}
```
The allocator assumes that the physical address returned by `pmm_alloc_pages` is accessible via identity mapping, so it can cast it directly to a `HeapBlock *`.
### Alignment helper
Allocations are rounded up to a fixed alignment (e.g., 16 bytes) using `align_up`:
```361:364:/home/lochlan/Documents/Coding/c/os/memory.c
static UINTN align_up(UINTN val, UINTN align)
{
return (val + align - 1) & ~(align - 1);
}
```
### Allocation (`kmalloc`)
`kmalloc` performs a **first-fit** search of the free list:
```401:440:/home/lochlan/Documents/Coding/c/os/memory.c
void *kmalloc(UINTN size)
{
HeapBlock *block, *split;
UINTN aligned;
if (!heap_ready || size == 0) {
return NULL;
}
aligned = align_up(size, HEAP_ALIGN);
for (block = heap_start; block != NULL; block = block->next) {
if (block->magic != HEAP_BLOCK_MAGIC) {
return NULL; /* heap corruption */
}
if (block->state != HEAP_BLOCK_FREE || block->size < aligned) {
continue;
}
/* Try to split if there is room for another header + 16 bytes */
if (block->size >= aligned + sizeof(HeapBlock) + HEAP_ALIGN) {
split = (HeapBlock *)((UINT8 *)block + sizeof(HeapBlock) + aligned);
split->magic = HEAP_BLOCK_MAGIC;
split->state = HEAP_BLOCK_FREE;
split->size = block->size - aligned - sizeof(HeapBlock);
split->next = block->next;
split->prev = block;
if (block->next != NULL) {
block->next->prev = split;
}
block->next = split;
block->size = aligned;
}
block->state = HEAP_BLOCK_USED;
return (void *)((UINT8 *)block + sizeof(HeapBlock));
}
return NULL; /* out of heap memory */
}
```
Notable details:
- **Corruption detection** checks `HEAP_BLOCK_MAGIC` for each block; any mismatch aborts with `NULL`.
- **Splitting** if the free block is large enough, it is split into:
- An allocated block of exactly `aligned` bytes.
- A new trailing free block (`split`) with its own header.
- **Alignment** the returned pointer is `sizeof(HeapBlock)` bytes after the header and aligned according to `HEAP_ALIGN`.
### Freeing (`kfree`) and coalescing
`kfree` marks a block as free and then attempts to coalesce with neighboring free blocks to combat fragmentation:
```449:486:/home/lochlan/Documents/Coding/c/os/memory.c
void kfree(void *ptr)
{
HeapBlock *block;
if (ptr == NULL || !heap_ready) {
return;
}
block = (HeapBlock *)((UINT8 *)ptr - sizeof(HeapBlock));
if (block->magic != HEAP_BLOCK_MAGIC || block->state != HEAP_BLOCK_USED) {
return; /* bad pointer or double-free */
}
block->state = HEAP_BLOCK_FREE;
/* Coalesce with next neighbour */
if (block->next != NULL
&& block->next->magic == HEAP_BLOCK_MAGIC
&& block->next->state == HEAP_BLOCK_FREE) {
block->size += sizeof(HeapBlock) + block->next->size;
block->next = block->next->next;
if (block->next != NULL) {
block->next->prev = block;
}
}
/* Coalesce with previous neighbour */
if (block->prev != NULL
&& block->prev->magic == HEAP_BLOCK_MAGIC
&& block->prev->state == HEAP_BLOCK_FREE) {
block->prev->size += sizeof(HeapBlock) + block->size;
block->prev->next = block->next;
if (block->next != NULL) {
block->next->prev = block->prev;
}
}
}
```
The allocator never returns memory to the PMM; all heap pages remain reserved for heap use for the lifetime of the kernel.
### Heap statistics
`heap_get_stats` walks the free list and aggregates total, used, and free bytes as well as block count:
```488:508:/home/lochlan/Documents/Coding/c/os/memory.c
void heap_get_stats(UINTN *total, UINTN *used, UINTN *free_mem,
UINTN *num_blocks)
{
HeapBlock *b;
*total = 0; *used = 0; *free_mem = 0; *num_blocks = 0;
if (!heap_ready) return;
for (b = heap_start; b != NULL && b->magic == HEAP_BLOCK_MAGIC;
b = b->next) {
(*num_blocks)++;
*total += b->size;
if (b->state == HEAP_BLOCK_USED) {
*used += b->size;
} else {
*free_mem += b->size;
}
}
}
```
These statistics are surfaced to the user via the `mem` and `memtest` commands.
---
## Runtime memory diagnostics (`mem` and `memtest`)
The `mem` command (in `commands.c`) prints a snapshot of PMM and heap state by calling `memory_print_stats`. Access requires `TASK_PRIV_KERNEL`:
```525:572:/home/lochlan/Documents/Coding/c/os/memory.c
void memory_print_stats(BootInfo *Boot)
{
UINTN h_total, h_used, h_free, h_blocks;
UINTN p_total, p_free, p_used;
Task *caller;
/* Subsystem-level privilege enforcement: memory stats require KERNEL. */
caller = task_current();
if (caller != NULL && task_get_privilege(caller) < TASK_PRIV_KERNEL) {
SAFE_PRINT(Boot, L"Permission denied: memory stats require kernel privilege.\n\r");
return;
}
p_total = pmm_get_total_pages();
p_free = pmm_get_free_pages();
p_used = p_total - p_free;
heap_get_stats(&h_total, &h_used, &h_free, &h_blocks);
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Memory Statistics\n\r");
SAFE_PRINT(Boot, L"================================================\n\r");
...
SAFE_PRINT(Boot, L"Paging:\n\r");
SAFE_PRINT(Boot, L" CR3: 0x%lx\n\r", read_cr3());
SAFE_PRINT(Boot, L" Mode: 4-level (PML4)\n\r");
SAFE_PRINT(Boot, L"\n\r");
}
```
The `memtest` command runs a scripted set of tests that exercise heap allocation, heap free/coalescing, and PMM single- and multi-page allocation. It also enforces `TASK_PRIV_KERNEL`:
```306:379:/home/lochlan/Documents/Coding/c/os/commands.c
static void cmd_memtest(BootInfo *Boot, CHAR16 *Args)
{
void *ptrs[8];
UINTN sizes[] = { 16, 64, 128, 256, 512, 1024, 2048, 4096 };
UINTN i;
UINT64 page;
UINTN h_total, h_used, h_free, h_blocks;
Task *caller;
(void)Args;
/* Subsystem-level privilege enforcement: memtest requires KERNEL. */
caller = task_current();
if (caller != NULL && task_get_privilege(caller) < TASK_PRIV_KERNEL) {
SAFE_PRINT(Boot, L"Permission denied: memtest requires kernel privilege.\n\r");
return;
}
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Memory Test\n\r");
SAFE_PRINT(Boot, L"================================================\n\r");
...
/* --- Heap allocation test --- */
...
/* --- Heap free test --- */
...
/* --- PMM page allocation test --- */
...
/* --- Multi-page allocation test --- */
...
SAFE_PRINT(Boot, L"\n\rAll memory tests completed.\n\r\n\r");
}
```
These commands provide a convenient way to validate memory subsystem behaviour from the Starling Terminal without needing an external debugger.

308
docs/overview.md Normal file
View File

@@ -0,0 +1,308 @@
## Overview
This document explains the high-level control flow of the operating system from firmware entry through to the interactive shell and introduces the major subsystems that the rest of the documentation explores in depth.
- **Boot loader**: a UEFI application implemented in `main.c` that reads `kernel.elf`, maps its segments, prepares the `BootInfo` interface, and jumps into the kernel.
- **Kernel entry**: `kmain` in `kernel.c`, which initialises the IDT, memory, and tasking subsystems, then spawns the Starling Terminal task.
- **Subsystems**: memory management (`memory.c`), cooperative multitasking (`task.c`), interrupt/exception handling (`idt.c`), and the command/terminal layer (`commands.c` + `kernel.c`).
The remaining sections walk through this path step by step and show how these modules interact.
---
## Firmware and boot loader (`main.c`)
The system starts execution inside the UEFI firmware, which invokes the PE32+ entry point of the loader. GNU-EFI arranges for this to be the `efi_main` function in `main.c`:
```298:345:/home/lochlan/Documents/Coding/c/os/main.c
EFI_STATUS
EFIAPI
efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
EFI_STATUS Status;
VOID *KernelImage = NULL;
UINTN KernelSize = 0;
UINT64 KernelEntry = 0;
BootInfo Boot;
KernelEntryFn EntryFn = NULL;
/* Initialise the GNU-EFI library */
InitializeLib(ImageHandle, SystemTable);
Print(L"Loading kernel...\n\r");
Status = read_file_to_buffer(ImageHandle, L"\\kernel.elf", &KernelImage, &KernelSize);
...
Status = load_elf_kernel(KernelImage, KernelSize, &KernelEntry);
...
/* Populate the BootInfo struct with generic UEFI-backed services */
Boot.print = Print;
Boot.clear_screen = loader_clear_screen;
...
Boot.free_pages = loader_free_pages;
Boot.firmware_vendor = SystemTable->FirmwareVendor;
...
/* Jump to the kernel this should not return */
EntryFn = (KernelEntryFn)(UINTN)KernelEntry;
EntryFn(&Boot);
Print(L"Kernel returned. Halting.\n\r");
return EFI_SUCCESS;
}
```
Key steps performed by `efi_main`:
- **Load kernel image**: `read_file_to_buffer` opens `\\kernel.elf` from the EFI System Partition and reads it into a pool buffer.
- **Parse ELF64**: `load_elf_kernel` verifies ELF headers and iterates PT\_LOAD segments, mapping each to its requested virtual/physical address via UEFI `AllocatePages`, then zero-fills `.bss`:
```168:223:/home/lochlan/Documents/Coding/c/os/main.c
static EFI_STATUS load_elf_kernel(VOID *Image, UINTN Size, UINT64 *EntryOut)
{
Elf64_Ehdr *Ehdr = (Elf64_Ehdr *)Image;
...
for (Index = 0; Index < Ehdr->e_phnum; Index++) {
Elf64_Phdr *Segment = (Elf64_Phdr *)((UINT8 *)Phdr + (Index * Ehdr->e_phentsize));
...
Address = (EFI_PHYSICAL_ADDRESS)SegmentStart;
if (EFI_ERROR(uefi_call_wrapper(BS->AllocatePages, 4, AllocateAddress,
EfiLoaderData, SegmentPages, &Address))) {
return EFI_OUT_OF_RESOURCES;
}
CopyMem((VOID *)(UINTN)Segment->p_vaddr,
(UINT8 *)Image + Segment->p_offset,
(UINTN)Segment->p_filesz);
if (Segment->p_memsz > Segment->p_filesz) {
SetMem((VOID *)(UINTN)(Segment->p_vaddr + Segment->p_filesz),
(UINTN)(Segment->p_memsz - Segment->p_filesz), 0);
}
}
*EntryOut = Ehdr->e_entry;
return EFI_SUCCESS;
}
```
- **Prepare the kernel ABI**: `BootInfo` is a compact struct of function pointers and metadata shared between loader and kernel:
```21:62:/home/lochlan/Documents/Coding/c/os/boot_info.h
typedef struct {
/* Console I/O */
KernelPrintFn print;
ConsoleClearFn clear_screen;
ConsoleSetAttrFn set_attribute;
KeyReadFn read_key;
KeyReadFn try_read_key;
/* System control */
void (*shutdown)(void);
/* Physical memory */
KSTATUS (*alloc_pages)(UINTN pages, UINT64 *addr);
KSTATUS (*free_pages)(UINT64 addr, UINTN pages);
/* Firmware metadata (for informational commands only). */
const CHAR16 *firmware_vendor;
UINT32 firmware_major;
UINT32 firmware_minor;
} BootInfo;
```
UEFI-specific calls (console I/O, page allocation, shutdown) are wrapped in small adapter functions (`loader_clear_screen`, `loader_alloc_pages`, etc.) and stored in this struct. The kernel never calls firmware entry points directly; instead it depends only on `BootInfo`.
Finally, the loader:
- Casts the ELF entry point to `KernelEntryFn`.
- Invokes `EntryFn(&Boot)`, transferring control to the kernel.
---
## Kernel entry and subsystem initialisation (`kernel.c`)
The C-level kernel entry point is `kmain` in `kernel.c`. It receives a single `BootInfo *` argument from the loader:
```169:254:/home/lochlan/Documents/Coding/c/os/kernel.c
void kmain(BootInfo *Boot)
{
KSTATUS Status;
Task *terminal_task = NULL;
StarlingContext *ctx = NULL;
if (Boot == NULL) {
return;
}
if (Boot->clear_screen != NULL) {
Status = Boot->clear_screen();
...
}
if (Boot->set_attribute != NULL) {
Status = Boot->set_attribute(TEXT_ATTR(COLOR_LIGHTGREEN, COLOR_BLACK));
...
}
/* ---- Subsystem initialisation ---- */
idt_init(Boot);
memory_init(Boot);
task_init(Boot);
/* ---- Welcome banner ---- */
SAFE_PRINT(Boot, L" Welcome to Simple 64-bit Operating System!\n\r");
...
SAFE_PRINT(Boot, L"Type 'help' for a list of commands.\n\r\n\r");
/* ---- Spawn Starling Terminal as its own task ---- */
ctx = (StarlingContext *)kmalloc(sizeof(StarlingContext));
...
ctx->Boot = Boot;
ctx->depth = 0;
ctx->shell_priv = TASK_PRIV_USER;
terminal_task = task_create_with_priv(L"starling-term",
starling_terminal_task,
ctx,
TASK_PRIV_USER);
if (terminal_task == NULL) {
...
starling_terminal_task(Boot);
return;
}
SAFE_PRINT(Boot, L"[core] Started Starling Terminal (PID %d).\n\r", terminal_task->pid);
/* Core thread becomes an idle loop, yielding to the terminal and others. */
while (TRUE) {
task_yield();
}
}
```
`kmain` performs three major duties:
1. **Console setup** clears the screen and sets a green-on-black colour scheme using firmware-backed services from `BootInfo`.
2. **Subsystem initialisation** calls:
- `idt_init(Boot)` to install the kernel's Interrupt Descriptor Table and exception handlers.
- `memory_init(Boot)` to bring up the physical allocator, paging helpers, and heap.
- `task_init(Boot)` to bootstrap the cooperative scheduler and register the current thread as task 0.
3. **User interface** prints a banner and spawns the Starling Terminal as a separate task via `task_create_with_priv` with `TASK_PRIV_USER` privilege, then turns the core thread into an idle loop that continuously `task_yield`s to allow other tasks to run.
At this point, the system has:
- A working IDT for CPU exceptions and IRQs.
- A memory stack providing page allocation, virtual mappings, and heap.
- A cooperative scheduler with at least two tasks: the core thread (task 0) and the terminal.
---
## Starling Terminal and command dispatch
Interactive user input is handled by the Starling Terminal task in `kernel.c`. It runs a readevalprint loop that delegates command execution to `commands.c`:
```47:163:/home/lochlan/Documents/Coding/c/os/kernel.c
static void starling_terminal_task(void *arg)
{
StarlingContext *ctx = (StarlingContext *)arg;
BootInfo *Boot = NULL;
KeyEvent Key;
...
if (ctx == NULL || ctx->Boot == NULL) {
return;
}
Boot = ctx->Boot;
depth = ctx->depth;
shell_priv = ctx->shell_priv;
SAFE_PRINT(Boot, L"\n\r[Starling Terminal depth %d, priv %d] ready.\n\r\n\r",
depth, (INT32)shell_priv);
SAFE_PRINT(Boot, L"starling> ");
while (TRUE) {
/* Try non-blocking read first; yield to other tasks while idle */
if (Boot->try_read_key != NULL) {
Status = Boot->try_read_key(&Key);
if (Status != 0) {
task_yield();
continue;
}
} else if (Boot->read_key != NULL) {
Status = Boot->read_key(&Key);
} else {
SAFE_PRINT(Boot, L"Console input unavailable.\n\r");
break;
}
...
if (Key.unicode_char == L'\r' || Key.unicode_char == L'\n') {
/* Enter pressed: execute the buffered command */
line[len] = L'\0';
SAFE_PRINT(Boot, L"\n\r");
trim_spaces_inplace(line);
...
} else {
Task *cmd_task = execute_command(Boot, line, shell_priv);
/* If a command task was spawned, wait for it to finish. */
if (cmd_task != NULL) {
task_wait(cmd_task);
}
/* Reset for next command */
len = 0;
SAFE_PRINT(Boot, L"starling> ");
}
}
...
}
/* Free our context on exit (allocated by the spawner). */
kfree(ctx);
}
```
Key points:
- **Non-blocking idle**: when `try_read_key` returns no key, the terminal calls `task_yield()` so other tasks can run while the user is idle.
- **Line editing**: handles printable ASCII and backspace to maintain a simple line buffer (`line[128]`).
- **Command execution**: on Enter, the line is trimmed and passed to `execute_command(Boot, line, shell_priv)` in `commands.c`, propagating the shell's privilege level. If that function spawns a dedicated command task, the terminal waits for it via `task_wait`.
- **Nested terminals**: entering `starling` recursively spawns another Starling Terminal task with increased `depth`, demonstrating multi-level shells.
The command registry and dispatch path are documented in detail in `commands-and-terminal.md`.
---
## Subsystem overview
The kernel is organised into focused subsystems, each in its own translation unit:
- **Type layer** (`kernel_types.h`): defines fixed-width and utility types such as `UINT8`, `UINT64`, `UINTN`, and `CHAR16`, deliberately avoiding firmware headers so that core kernel code remains decoupled from UEFI.
- **Boot ABI** (`boot_info.h`): defines `BootInfo`, `KeyEvent`, and function pointer types (`KernelPrintFn`, `ConsoleClearFn`, etc.) forming the sole contract between loader and kernel.
- **Memory management** (`memory.c` + `memory.h`):
- PMM bitmap-based page-frame allocator over a 16 MB pool obtained from the loader.
- Paging helpers to walk and extend the 4-level x86-64 page tables, map/unmap pages, and translate virtual to physical addresses.
- Heap first-fit free-list allocator with block splitting and coalescing, backed by PMM pages.
- **Tasks and scheduler** (`task.c` + `task.h`):
- Static process control block (PCB) pool.
- Cooperative round-robin scheduler.
- Software privilege levels (`TASK_PRIV_USER`, `TASK_PRIV_DRIVER`, `TASK_PRIV_KERNEL`) for access control.
- Stack management and context switch support (via an external `context_switch` assembly routine).
- **Interrupts and exceptions** (`idt.c` + `idt.h`):
- IDT mirroring of firmware entries.
- Replacement of CPU exception vectors 031 with kernel stubs.
- Central `isr_handler` that prints diagnostics and halts on unrecoverable faults.
- **Commands and shell** (`commands.c` + `commands.h`):
- Command registry with per-command minimum privilege levels and help/man system.
- System control commands (`shutdown`, `about`, `mem`, `ps`).
- Test commands (`memtest`, `tasktest`, `spawn`) that exercise memory and scheduler subsystems in isolation.
- Privilege escalation command (`kusr`) for running commands with elevated privilege.
Each of these subsystems is covered in a dedicated document:
- `memory-and-allocation.md` PMM, paging, and heap internals.
- `tasks-and-scheduler.md` task lifecycle, stacks, context switching, and scheduling.
- `interrupts-and-exceptions.md` IDT construction, ISRs, and fault handling.
- `commands-and-terminal.md` command pipeline from user input to handler execution.

508
docs/tasks-and-scheduler.md Normal file
View File

@@ -0,0 +1,508 @@
## Cooperative tasking overview
The kernel uses a **cooperative** multitasking model implemented in `task.c`. Tasks (lightweight threads) must call `task_yield` explicitly to let others run; there is no preemptive timer interrupt that forces context switches.
The key components are:
- A fixed-size **PCB pool** (`Task tasks[TASK_MAX]`).
- A per-task **stack** allocated from the PMM.
- A **scheduler** that performs round-robin selection among READY tasks.
- A `context_switch` assembly routine that saves/restores callee-saved registers and the stack pointer.
---
## Module state and initialisation
The scheduler's global state is:
```30:35:/home/lochlan/Documents/Coding/c/os/task.c
static Task tasks[TASK_MAX]; /* PCB pool (static array) */
static Task *current_task = NULL;
static UINT32 next_pid = 0;
static BootInfo *task_boot = NULL;
static BOOLEAN task_ready = FALSE;
```
`task_init` is called from `kmain` after memory and IDT initialisation:
```62:97:/home/lochlan/Documents/Coding/c/os/task.c
void task_init(BootInfo *Boot)
{
UINTN i;
task_boot = Boot;
/* Clear all PCB slots */
for (i = 0; i < TASK_MAX; i++) {
tasks[i].state = TASK_STATE_FREE;
tasks[i].pid = 0;
tasks[i].privilege = TASK_PRIV_USER;
tasks[i].saved_rsp = 0;
tasks[i].stack_base = 0;
tasks[i].stack_pages = 0;
tasks[i].entry = NULL;
tasks[i].arg = NULL;
tasks[i].switches = 0;
tasks[i].name[0] = L'\0';
}
/*
* Task 0 = the currently running kernel core thread.
* It already has a stack (the kernel's boot stack), so we don't
* allocate one. Its saved_rsp will be filled in during the
* first context_switch call in task_yield().
*/
tasks[0].pid = next_pid++;
tasks[0].state = TASK_STATE_RUNNING;
tasks[0].privilege = TASK_PRIV_KERNEL;
tasks[0].switches = 1;
wstrcpy16(tasks[0].name, L"core", TASK_NAME_LEN);
current_task = &tasks[0];
task_ready = TRUE;
SAFE_PRINT(Boot, L" Tasks: scheduler ready (max %d tasks)\n\r",
(UINTN)TASK_MAX);
}
```
Important points:
- Task 0 represents the **kernel core thread**, which uses the boot-time stack provided by the loader. It receives `TASK_PRIV_KERNEL` privilege.
- No stack is allocated for task 0; its `saved_rsp` is populated the first time a context switch occurs.
- All other PCBs begin in `TASK_STATE_FREE` with `TASK_PRIV_USER`.
---
## Task creation and stack layout
New tasks are created via `task_create_with_priv` (or its wrapper `task_create`), which:
1. Checks that the caller is not escalating privilege beyond its own level.
2. Finds a free PCB slot.
3. Allocates a stack from the PMM.
4. Sets up an initial stack frame so that `context_switch` can "return" into a C trampoline function.
```121:211:/home/lochlan/Documents/Coding/c/os/task.c
Task *task_create_with_priv(const CHAR16 *name,
TaskEntryFn entry,
void *arg,
TaskPrivilege privilege)
{
Task *t = NULL;
UINTN i;
UINT64 stack_phys;
UINT64 *sp;
...
/* Subsystem-level privilege enforcement: prevent privilege escalation. */
{
Task *caller = task_current();
if (caller != NULL && privilege > task_get_privilege(caller)) {
return NULL;
}
}
/* Find a free PCB slot */
for (i = 0; i < TASK_MAX; i++) {
if (tasks[i].state == TASK_STATE_FREE) {
t = &tasks[i];
break;
}
}
...
/* Allocate stack pages from the physical memory manager */
stack_phys = pmm_alloc_pages(TASK_STACK_PAGES);
if (stack_phys == 0) {
return NULL; /* out of memory */
}
/* Fill in the PCB */
t->pid = next_pid++;
t->state = TASK_STATE_READY;
t->privilege = privilege;
t->entry = entry;
t->arg = arg;
t->switches = 0;
t->stack_base = stack_phys;
t->stack_pages = TASK_STACK_PAGES;
wstrcpy16(t->name, name != NULL ? name : L"unnamed", TASK_NAME_LEN);
/*
* Set up the initial stack frame so that context_switch() can
* "return" into task_trampoline().
*
* context_switch saves/restores (low → high on stack):
* flags, r15, r14, r13, r12, rbx, rbp (pushes)
* then `ret` pops the return address (→ trampoline)
*
* Above the return address we place a safety-net address
* (task_exit) so that if the trampoline or entry function does
* a bare `ret`, it lands in task_exit().
*/
sp = (UINT64 *)(stack_phys + TASK_STACK_SIZE);
/* Align stack top to 16 bytes */
sp = (UINT64 *)((UINT64)sp & ~0xFULL);
/* Safety-net return address for the trampoline */
*(--sp) = (UINT64)(UINTN)task_exit;
/* Return address for context_switch's `ret` → trampoline */
*(--sp) = (UINT64)(UINTN)task_trampoline;
/* Callee-saved registers all zero for fresh task */
*(--sp) = 0; /* rbp */
*(--sp) = 0; /* rbx */
*(--sp) = 0; /* r12 */
*(--sp) = 0; /* r13 */
*(--sp) = 0; /* r14 */
*(--sp) = 0; /* r15 */
/* RFLAGS interrupts enabled (IF = bit 9) */
*(--sp) = 0x202; /* flags */
t->saved_rsp = (UINT64)(UINTN)sp;
return t;
}
```
The convenience wrapper `task_create` inherits the calling task's privilege level:
```213:220:/home/lochlan/Documents/Coding/c/os/task.c
Task *task_create(const CHAR16 *name, TaskEntryFn entry, void *arg)
{
/* Inherit privilege from the calling task (kernel if no task context). */
Task *caller = task_current();
TaskPrivilege priv = (caller != NULL) ? task_get_privilege(caller)
: TASK_PRIV_KERNEL;
return task_create_with_priv(name, entry, arg, priv);
}
```
The effective stack layout (low to high addresses) after `task_create` is:
- Saved `flags`, `r15`, `r14`, `r13`, `r12`, `rbx`, `rbp` (pushed by `context_switch` semantics).
- Return address to `task_trampoline`.
- Safety-net return address to `task_exit`.
This design guarantees that:
- The first time the scheduler chooses this task, restoring registers and issuing `ret` will jump to `task_trampoline`.
- If the trampoline or entry function ever returns normally, execution will fall into `task_exit` rather than running off the end of the stack.
---
## Trampoline and task entry
The trampoline is a small C function that calls the user-supplied entry point and then terminates the task cleanly:
```105:116:/home/lochlan/Documents/Coding/c/os/task.c
static void task_trampoline(void)
{
Task *t = task_current();
if (t != NULL && t->entry != NULL) {
t->entry(t->arg);
}
task_exit();
/* Should never reach here, but just in case: */
for (;;) {
__asm__ __volatile__("hlt");
}
}
```
The entry function signature is:
```12:17:/home/lochlan/Documents/Coding/c/os/task.h
typedef void (*TaskEntryFn)(void *arg);
```
This makes a task analogous to a `pthread`:
- It receives an opaque `void *arg`.
- It runs arbitrary kernel code.
- On completion it returns to `task_trampoline`, which calls `task_exit`.
---
## Scheduling and `task_yield`
The scheduler is purely cooperative and uses a simple **round-robin** algorithm implemented by `schedule_next`:
```203:230:/home/lochlan/Documents/Coding/c/os/task.c
static Task *schedule_next(void)
{
UINTN start, idx, i;
if (current_task == NULL) {
return &tasks[0];
}
/* Find current task's index in the array */
start = (UINTN)(current_task - tasks);
/* Round-robin: scan from (current+1) wrapping around */
for (i = 1; i <= TASK_MAX; i++) {
idx = (start + i) % TASK_MAX;
if (tasks[idx].state == TASK_STATE_READY) {
return &tasks[idx];
}
}
/* No other ready task stay with current if still runnable */
if (current_task->state == TASK_STATE_RUNNING ||
current_task->state == TASK_STATE_READY) {
return current_task;
}
/* Fallback to task 0 (kernel / shell) */
return &tasks[0];
}
```
`task_yield` is the public API that tasks call to give up the CPU:
```236:266:/home/lochlan/Documents/Coding/c/os/task.c
void task_yield(void)
{
Task *prev, *next;
if (!task_ready) {
return;
}
prev = current_task;
next = schedule_next();
if (next == prev) {
return; /* nothing else to switch to */
}
/* Mark the previous task as READY (still runnable) */
if (prev->state == TASK_STATE_RUNNING) {
prev->state = TASK_STATE_READY;
}
next->state = TASK_STATE_RUNNING;
next->switches++;
current_task = next;
/*
* context_switch saves callee-saved regs + flags on prev's stack,
* stores prev's RSP into prev->saved_rsp, loads next->saved_rsp
* into RSP, restores regs + flags, and `ret`s into next's code.
*/
context_switch(&prev->saved_rsp, next->saved_rsp);
}
```
The actual register-level state transition is performed by an external assembly function:
```18:22:/home/lochlan/Documents/Coding/c/os/task.h
void context_switch(UINT64 *prev_rsp, UINT64 next_rsp);
```
Conceptually, `context_switch`:
- Pushes callee-saved registers and FLAGS on the current stack.
- Stores the resulting stack pointer in `*prev_rsp`.
- Loads `next_rsp` into RSP.
- Pops registers and FLAGS from the new stack.
- Issues `ret`, returning into the next task's code.
---
## Task termination (`task_exit`)
Tasks terminate by calling `task_exit`, typically via the trampoline:
```272:305:/home/lochlan/Documents/Coding/c/os/task.c
void task_exit(void)
{
Task *prev, *next;
if (!task_ready) {
return;
}
prev = current_task;
prev->state = TASK_STATE_TERMINATED;
/* Free the stack memory back to the PMM */
if (prev->stack_base != 0 && prev->stack_pages != 0) {
pmm_free_pages(prev->stack_base, prev->stack_pages);
prev->stack_base = 0;
prev->stack_pages = 0;
}
/* Mark the PCB slot as free for reuse */
prev->state = TASK_STATE_FREE;
next = schedule_next();
if (next == prev) {
/* Shouldn't happen if task 0 (kernel) is always alive */
next = &tasks[0];
}
next->state = TASK_STATE_RUNNING;
next->switches++;
current_task = next;
/* One-way switch: we never return to the exited task */
context_switch(&prev->saved_rsp, next->saved_rsp);
/* Should never reach here */
for (;;) {
__asm__ __volatile__("hlt");
}
}
```
Key behaviours:
- The task's stack pages are returned to the PMM via `pmm_free_pages`.
- The PCB slot is recycled back to `TASK_STATE_FREE`.
- The subsequent `context_switch` is **one-way**: control never returns to the exited task.
---
## Waiting for tasks
Certain parts of the kernel (e.g., the Starling Terminal and some commands) need to wait for a worker task to finish. This is done cooperatively via `task_wait`:
```336:348:/home/lochlan/Documents/Coding/c/os/task.c
void task_wait(Task *t)
{
if (!task_ready || t == NULL) {
return;
}
/*
* Busy-wait cooperatively until the target task's PCB slot has
* been recycled back to FREE by task_exit().
*/
while (t->state != TASK_STATE_FREE) {
task_yield();
}
}
```
Because the scheduler is cooperative, this **busy-wait** loop is benign: it yields on each iteration, allowing the waited-on task to make progress and eventually call `task_exit`.
Example usage from the Starling Terminal:
```135:140:/home/lochlan/Documents/Coding/c/os/kernel.c
Task *cmd_task = execute_command(Boot, line, shell_priv);
/* If a command task was spawned, wait for it to finish. */
if (cmd_task != NULL) {
task_wait(cmd_task);
}
```
---
## Task inspection (`ps` and `tasktest`)
The `ps` command uses `task_print_list` to show current tasks. Access requires at least `TASK_PRIV_DRIVER`:
```407:439:/home/lochlan/Documents/Coding/c/os/task.c
void task_print_list(BootInfo *Boot)
{
UINTN i;
Task *caller;
/* Subsystem-level privilege enforcement: task list requires DRIVER. */
caller = task_current();
if (caller != NULL && task_get_privilege(caller) < TASK_PRIV_DRIVER) {
SAFE_PRINT(Boot, L"Permission denied: task list requires driver privilege.\n\r");
return;
}
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L" PID STATE PRIV SWITCHES NAME\n\r");
SAFE_PRINT(Boot, L" --- ---------- ---- -------- ----\n\r");
for (i = 0; i < TASK_MAX; i++) {
if (tasks[i].state == TASK_STATE_FREE) {
continue;
}
SAFE_PRINT(Boot, L" %3d %-10s %4d %8d %s\n\r",
tasks[i].pid,
state_str(tasks[i].state),
(INT32)tasks[i].privilege,
tasks[i].switches,
tasks[i].name);
}
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L" Active tasks: %d / %d\n\r",
task_count(), (UINTN)TASK_MAX);
SAFE_PRINT(Boot, L"\n\r");
}
```
The `tasktest` command in `commands.c` programmatically exercises the scheduler:
```400:435:/home/lochlan/Documents/Coding/c/os/commands.c
static void cmd_tasktest(BootInfo *Boot, CHAR16 *Args)
{
Task *t1, *t2, *t3;
UINTN i;
(void)Args;
...
t1 = task_create(L"worker-A", worker_task_fn, Boot);
t2 = task_create(L"worker-B", worker_task_fn, Boot);
t3 = task_create(L"worker-C", worker_task_fn, Boot);
...
SAFE_PRINT(Boot, L"\n\rYielding to let workers run:\n\r\n\r");
/* Yield enough times for all workers to complete (3 tasks x 3 steps) */
for (i = 0; i < 12; i++) {
task_yield();
}
SAFE_PRINT(Boot, L"\n\rTask list after test:\n\r");
task_print_list(Boot);
SAFE_PRINT(Boot, L"Task scheduler test completed.\n\r\n\r");
}
```
Each worker task:
- Prints a progress message.
- Calls `task_yield`.
- Repeats three times, then finishes.
This demonstrates how cooperative tasks interleave output and how `task_yield` drives scheduling.
---
## Privilege system
Each task carries a `TaskPrivilege` level defined in `task.h`:
```47:52:/home/lochlan/Documents/Coding/c/os/task.h
typedef enum {
TASK_PRIV_USER = 0,
TASK_PRIV_DRIVER = 1,
TASK_PRIV_KERNEL = 2,
} TaskPrivilege;
```
All tasks still execute in CPU ring 0; this is a **software-only** hierarchy used for access control decisions:
- `task_create_with_priv` prevents a caller from creating a task with a higher privilege than its own.
- Subsystem functions like `memory_print_stats`, `task_print_list`, and `request_shutdown` check the calling task's privilege before proceeding.
- The `kusr` command (`commands.c`) temporarily elevates a task to `TASK_PRIV_KERNEL` to run a privileged sub-command, then restores the original level.
Accessors:
- `task_get_privilege(Task *t)` returns the task's current privilege level.
- `task_set_privilege(Task *t, TaskPrivilege p)` changes it (no enforcement; callers are responsible).

214
idt.c Normal file
View File

@@ -0,0 +1,214 @@
/*
* idt.c Interrupt Descriptor Table setup and exception handling.
*
* On initialisation the kernel copies the firmware's IDT (preserving
* its hardware IRQ handlers for vectors 32-47), then overrides CPU
* exception vectors 0-31 with our own stubs from isr.S.
*
* Hardware IRQs receive an EOI and return. CPU exceptions print
* diagnostic information (including CR2 for page faults) and halt.
*/
#include "idt.h"
/* ================================================================
* Constants and internal types
* ================================================================ */
#define IDT_SIZE 256
#define IDT_TYPE_INTERRUPT 0x8E /* P=1, DPL=0, 64-bit interrupt gate */
/* Single 16-byte IDT entry (x86-64 long mode). */
typedef struct {
UINT16 offset_low; /* bits 0-15 of handler address */
UINT16 selector; /* code segment selector */
UINT8 ist; /* interrupt stack table index */
UINT8 type_attr; /* type and attributes */
UINT16 offset_mid; /* bits 16-31 of handler address */
UINT32 offset_high; /* bits 32-63 of handler address */
UINT32 zero; /* reserved, must be zero */
} __attribute__((packed)) IdtEntry;
/* IDTR register layout for the LIDT instruction. */
typedef struct {
UINT16 limit;
UINT64 base;
} __attribute__((packed)) IdtPtr;
/* ================================================================
* Module state
* ================================================================ */
static IdtEntry idt[IDT_SIZE];
static BootInfo *gBoot = NULL;
/* Defined in isr.S one stub function per vector (0-255). */
extern void (*isr_stub_table[])(void);
/* ================================================================
* IDT gate helpers
* ================================================================ */
/* Install a single IDT gate pointing at `handler`. */
static void idt_set_gate(UINTN index, void (*handler)(void))
{
UINT64 addr = (UINT64)(UINTN)handler;
UINT16 selector = 0;
__asm__ __volatile__("mov %%cs, %0" : "=r"(selector));
idt[index].offset_low = (UINT16)(addr & 0xFFFF);
idt[index].selector = selector;
idt[index].ist = 0;
idt[index].type_attr = IDT_TYPE_INTERRUPT;
idt[index].offset_mid = (UINT16)((addr >> 16) & 0xFFFF);
idt[index].offset_high = (UINT32)((addr >> 32) & 0xFFFFFFFF);
idt[index].zero = 0;
}
/* Load a new IDT register value. */
static void lidt(const IdtPtr *idtr)
{
__asm__ __volatile__("lidt (%0)" :: "r"(idtr));
}
/* ================================================================
* Exception name lookup
* ================================================================ */
/* Return a human-readable name for CPU exception vectors 0-31. */
static const CHAR16 *exception_name(UINTN vector)
{
switch (vector) {
case 0: return L"Divide Error";
case 1: return L"Debug";
case 2: return L"Non-Maskable Interrupt";
case 3: return L"Breakpoint";
case 4: return L"Overflow";
case 5: return L"Bound Range Exceeded";
case 6: return L"Invalid Opcode";
case 7: return L"Device Not Available";
case 8: return L"Double Fault";
case 9: return L"Coprocessor Segment Overrun";
case 10: return L"Invalid TSS";
case 11: return L"Segment Not Present";
case 12: return L"Stack-Segment Fault";
case 13: return L"General Protection Fault";
case 14: return L"Page Fault";
case 15: return L"Reserved";
case 16: return L"x87 Floating-Point";
case 17: return L"Alignment Check";
case 18: return L"Machine Check";
case 19: return L"SIMD Floating-Point";
case 20: return L"Virtualization";
case 21: return L"Control Protection";
case 22: return L"Reserved";
case 23: return L"Reserved";
case 24: return L"Reserved";
case 25: return L"Reserved";
case 26: return L"Reserved";
case 27: return L"Reserved";
case 28: return L"Hypervisor Injection";
case 29: return L"VMM Communication";
case 30: return L"Security";
case 31: return L"Reserved";
default: return L"Unknown";
}
}
/* ================================================================
* PIC and low-level helpers
* ================================================================ */
/* Write a byte to an I/O port. */
static inline void outb(UINT16 port, UINT8 value)
{
__asm__ __volatile__("outb %0, %1" :: "a"(value), "Nd"(port));
}
/* Send End-Of-Interrupt to the 8259 PIC(s). */
static void pic_eoi(UINTN vector)
{
if (vector >= 40) {
outb(0xA0, 0x20); /* EOI to slave PIC */
}
outb(0x20, 0x20); /* EOI to master PIC */
}
/* Disable interrupts and halt forever (unrecoverable fault). */
static void halt_forever(void)
{
for (;;) {
__asm__ __volatile__("cli; hlt");
}
}
/* ================================================================
* ISR dispatcher (called from isr_common in isr.S)
* ================================================================ */
void isr_handler(ISRFrame *frame)
{
UINT64 cr2 = 0;
/* Hardware IRQs (vectors 32-47): send EOI and return */
if (frame->vector >= 32 && frame->vector <= 47) {
pic_eoi(frame->vector);
return;
}
/* CPU exceptions (vectors 0-31): print diagnostics and halt */
if (gBoot != NULL && gBoot->print != NULL) {
gBoot->print(L"\n\rEXCEPTION: %d (%s)\n\r", frame->vector,
exception_name(frame->vector));
gBoot->print(L" Error Code: 0x%lx\n\r", frame->error_code);
gBoot->print(L" RIP: 0x%lx CS: 0x%lx RFLAGS: 0x%lx\n\r",
frame->rip, frame->cs, frame->rflags);
}
if (frame->vector == 14) {
__asm__ __volatile__("mov %%cr2, %0" : "=r"(cr2));
if (gBoot != NULL && gBoot->print != NULL) {
gBoot->print(L" CR2: 0x%lx\n\r", cr2);
}
}
halt_forever();
}
/* ================================================================
* IDT initialisation
* ================================================================ */
void idt_init(BootInfo *Boot)
{
IdtPtr old_idtr;
IdtPtr idtr;
IdtEntry *old_idt = NULL;
UINTN i = 0;
gBoot = Boot;
/* Read the firmware's existing IDT so we can preserve its entries */
__asm__ __volatile__("sidt %0" : "=m"(old_idtr));
old_idt = (IdtEntry *)(UINTN)old_idtr.base;
/* Copy the entire existing IDT first (preserves firmware IRQ handlers) */
for (i = 0; i < IDT_SIZE; i++) {
if (old_idt != NULL && (i * sizeof(IdtEntry)) < (UINTN)(old_idtr.limit + 1)) {
idt[i] = old_idt[i];
} else {
idt_set_gate(i, isr_stub_table[i]);
}
}
/* Override only CPU exception vectors (0-31) with our handlers */
for (i = 0; i < 32; i++) {
idt_set_gate(i, isr_stub_table[i]);
}
idtr.limit = (UINT16)(sizeof(idt) - 1);
idtr.base = (UINT64)(UINTN)idt;
lidt(&idtr);
}

52
idt.h Normal file
View File

@@ -0,0 +1,52 @@
/*
* idt.h Interrupt Descriptor Table interface.
*
* Declares the ISR stack frame layout (pushed by isr_common in isr.S)
* and the IDT initialisation function.
*/
#ifndef IDT_H
#define IDT_H
#include "kernel_types.h"
#include "boot_info.h"
/*
* ISRFrame register state saved by isr_common before calling
* isr_handler(). The order must match the push sequence in isr.S.
*/
typedef struct {
/* General-purpose registers (pushed by isr_common) */
UINT64 r15;
UINT64 r14;
UINT64 r13;
UINT64 r12;
UINT64 r11;
UINT64 r10;
UINT64 r9;
UINT64 r8;
UINT64 rbp;
UINT64 rdi;
UINT64 rsi;
UINT64 rdx;
UINT64 rcx;
UINT64 rbx;
UINT64 rax;
/* Pushed by the ISR stub macros */
UINT64 vector;
UINT64 error_code;
/* Pushed by the CPU on interrupt entry */
UINT64 rip;
UINT64 cs;
UINT64 rflags;
} ISRFrame;
/*
* Install our IDT: copies firmware entries for vectors 32+,
* overrides vectors 0-31 with kernel exception handlers.
*/
void idt_init(BootInfo *Boot);
#endif /* IDT_H */

615
isr.S Normal file
View File

@@ -0,0 +1,615 @@
/*
* isr.S Interrupt Service Routine stubs for x86-64.
*
* Provides 256 thin stub functions (isr0 isr255) and a common body
* (isr_common) that saves all general-purpose registers, calls the C
* handler isr_handler(ISRFrame *), restores registers, and returns
* via IRETQ.
*
* Two macros generate the stubs:
* ISR_NOERR pushes a dummy error code (0) for vectors that don't
* ISR_ERR the CPU already pushed an error code
*
* A pointer table (isr_stub_table) is emitted at the end so that
* idt.c can look up the stub address for each vector by index.
*/
.text
.global isr_handler
.global isr_stub_table
/* ----------------------------------------------------------------
* Stub macros
* ---------------------------------------------------------------- */
.macro ISR_NOERR num
.global isr\num
isr\num:
pushq $0
pushq $\num
jmp isr_common
.endm
.macro ISR_ERR num
.global isr\num
isr\num:
pushq $\num
jmp isr_common
.endm
/* ----------------------------------------------------------------
* Common ISR body saves state, calls C handler, restores state
* ---------------------------------------------------------------- */
isr_common:
cld
pushq %rax
pushq %rbx
pushq %rcx
pushq %rdx
pushq %rsi
pushq %rdi
pushq %rbp
pushq %r8
pushq %r9
pushq %r10
pushq %r11
pushq %r12
pushq %r13
pushq %r14
pushq %r15
movq %rsp, %rdi
call isr_handler
popq %r15
popq %r14
popq %r13
popq %r12
popq %r11
popq %r10
popq %r9
popq %r8
popq %rbp
popq %rdi
popq %rsi
popq %rdx
popq %rcx
popq %rbx
popq %rax
addq $16, %rsp
iretq
/* ----------------------------------------------------------------
* Stub instantiation (vectors 0-255)
*
* Vectors 0-31: CPU exceptions (some push error codes)
* Vectors 32-47: legacy 8259 PIC hardware IRQs
* Vectors 48+: available for software / APIC use
* ---------------------------------------------------------------- */
/* CPU exceptions */
ISR_NOERR 0
ISR_NOERR 1
ISR_NOERR 2
ISR_NOERR 3
ISR_NOERR 4
ISR_NOERR 5
ISR_NOERR 6
ISR_NOERR 7
ISR_ERR 8
ISR_NOERR 9
ISR_ERR 10
ISR_ERR 11
ISR_ERR 12
ISR_ERR 13
ISR_ERR 14
ISR_NOERR 15
ISR_NOERR 16
ISR_ERR 17
ISR_NOERR 18
ISR_NOERR 19
ISR_NOERR 20
ISR_NOERR 21
ISR_NOERR 22
ISR_NOERR 23
ISR_NOERR 24
ISR_NOERR 25
ISR_NOERR 26
ISR_NOERR 27
ISR_NOERR 28
ISR_NOERR 29
ISR_ERR 30
ISR_NOERR 31
ISR_NOERR 32
ISR_NOERR 33
ISR_NOERR 34
ISR_NOERR 35
ISR_NOERR 36
ISR_NOERR 37
ISR_NOERR 38
ISR_NOERR 39
ISR_NOERR 40
ISR_NOERR 41
ISR_NOERR 42
ISR_NOERR 43
ISR_NOERR 44
ISR_NOERR 45
ISR_NOERR 46
ISR_NOERR 47
ISR_NOERR 48
ISR_NOERR 49
ISR_NOERR 50
ISR_NOERR 51
ISR_NOERR 52
ISR_NOERR 53
ISR_NOERR 54
ISR_NOERR 55
ISR_NOERR 56
ISR_NOERR 57
ISR_NOERR 58
ISR_NOERR 59
ISR_NOERR 60
ISR_NOERR 61
ISR_NOERR 62
ISR_NOERR 63
ISR_NOERR 64
ISR_NOERR 65
ISR_NOERR 66
ISR_NOERR 67
ISR_NOERR 68
ISR_NOERR 69
ISR_NOERR 70
ISR_NOERR 71
ISR_NOERR 72
ISR_NOERR 73
ISR_NOERR 74
ISR_NOERR 75
ISR_NOERR 76
ISR_NOERR 77
ISR_NOERR 78
ISR_NOERR 79
ISR_NOERR 80
ISR_NOERR 81
ISR_NOERR 82
ISR_NOERR 83
ISR_NOERR 84
ISR_NOERR 85
ISR_NOERR 86
ISR_NOERR 87
ISR_NOERR 88
ISR_NOERR 89
ISR_NOERR 90
ISR_NOERR 91
ISR_NOERR 92
ISR_NOERR 93
ISR_NOERR 94
ISR_NOERR 95
ISR_NOERR 96
ISR_NOERR 97
ISR_NOERR 98
ISR_NOERR 99
ISR_NOERR 100
ISR_NOERR 101
ISR_NOERR 102
ISR_NOERR 103
ISR_NOERR 104
ISR_NOERR 105
ISR_NOERR 106
ISR_NOERR 107
ISR_NOERR 108
ISR_NOERR 109
ISR_NOERR 110
ISR_NOERR 111
ISR_NOERR 112
ISR_NOERR 113
ISR_NOERR 114
ISR_NOERR 115
ISR_NOERR 116
ISR_NOERR 117
ISR_NOERR 118
ISR_NOERR 119
ISR_NOERR 120
ISR_NOERR 121
ISR_NOERR 122
ISR_NOERR 123
ISR_NOERR 124
ISR_NOERR 125
ISR_NOERR 126
ISR_NOERR 127
ISR_NOERR 128
ISR_NOERR 129
ISR_NOERR 130
ISR_NOERR 131
ISR_NOERR 132
ISR_NOERR 133
ISR_NOERR 134
ISR_NOERR 135
ISR_NOERR 136
ISR_NOERR 137
ISR_NOERR 138
ISR_NOERR 139
ISR_NOERR 140
ISR_NOERR 141
ISR_NOERR 142
ISR_NOERR 143
ISR_NOERR 144
ISR_NOERR 145
ISR_NOERR 146
ISR_NOERR 147
ISR_NOERR 148
ISR_NOERR 149
ISR_NOERR 150
ISR_NOERR 151
ISR_NOERR 152
ISR_NOERR 153
ISR_NOERR 154
ISR_NOERR 155
ISR_NOERR 156
ISR_NOERR 157
ISR_NOERR 158
ISR_NOERR 159
ISR_NOERR 160
ISR_NOERR 161
ISR_NOERR 162
ISR_NOERR 163
ISR_NOERR 164
ISR_NOERR 165
ISR_NOERR 166
ISR_NOERR 167
ISR_NOERR 168
ISR_NOERR 169
ISR_NOERR 170
ISR_NOERR 171
ISR_NOERR 172
ISR_NOERR 173
ISR_NOERR 174
ISR_NOERR 175
ISR_NOERR 176
ISR_NOERR 177
ISR_NOERR 178
ISR_NOERR 179
ISR_NOERR 180
ISR_NOERR 181
ISR_NOERR 182
ISR_NOERR 183
ISR_NOERR 184
ISR_NOERR 185
ISR_NOERR 186
ISR_NOERR 187
ISR_NOERR 188
ISR_NOERR 189
ISR_NOERR 190
ISR_NOERR 191
ISR_NOERR 192
ISR_NOERR 193
ISR_NOERR 194
ISR_NOERR 195
ISR_NOERR 196
ISR_NOERR 197
ISR_NOERR 198
ISR_NOERR 199
ISR_NOERR 200
ISR_NOERR 201
ISR_NOERR 202
ISR_NOERR 203
ISR_NOERR 204
ISR_NOERR 205
ISR_NOERR 206
ISR_NOERR 207
ISR_NOERR 208
ISR_NOERR 209
ISR_NOERR 210
ISR_NOERR 211
ISR_NOERR 212
ISR_NOERR 213
ISR_NOERR 214
ISR_NOERR 215
ISR_NOERR 216
ISR_NOERR 217
ISR_NOERR 218
ISR_NOERR 219
ISR_NOERR 220
ISR_NOERR 221
ISR_NOERR 222
ISR_NOERR 223
ISR_NOERR 224
ISR_NOERR 225
ISR_NOERR 226
ISR_NOERR 227
ISR_NOERR 228
ISR_NOERR 229
ISR_NOERR 230
ISR_NOERR 231
ISR_NOERR 232
ISR_NOERR 233
ISR_NOERR 234
ISR_NOERR 235
ISR_NOERR 236
ISR_NOERR 237
ISR_NOERR 238
ISR_NOERR 239
ISR_NOERR 240
ISR_NOERR 241
ISR_NOERR 242
ISR_NOERR 243
ISR_NOERR 244
ISR_NOERR 245
ISR_NOERR 246
ISR_NOERR 247
ISR_NOERR 248
ISR_NOERR 249
ISR_NOERR 250
ISR_NOERR 251
ISR_NOERR 252
ISR_NOERR 253
ISR_NOERR 254
ISR_NOERR 255
/* ----------------------------------------------------------------
* Stub pointer table indexed by vector number (0-255)
* Used by idt_set_gate() in idt.c to populate the IDT.
* ---------------------------------------------------------------- */
isr_stub_table:
.quad isr0
.quad isr1
.quad isr2
.quad isr3
.quad isr4
.quad isr5
.quad isr6
.quad isr7
.quad isr8
.quad isr9
.quad isr10
.quad isr11
.quad isr12
.quad isr13
.quad isr14
.quad isr15
.quad isr16
.quad isr17
.quad isr18
.quad isr19
.quad isr20
.quad isr21
.quad isr22
.quad isr23
.quad isr24
.quad isr25
.quad isr26
.quad isr27
.quad isr28
.quad isr29
.quad isr30
.quad isr31
.quad isr32
.quad isr33
.quad isr34
.quad isr35
.quad isr36
.quad isr37
.quad isr38
.quad isr39
.quad isr40
.quad isr41
.quad isr42
.quad isr43
.quad isr44
.quad isr45
.quad isr46
.quad isr47
.quad isr48
.quad isr49
.quad isr50
.quad isr51
.quad isr52
.quad isr53
.quad isr54
.quad isr55
.quad isr56
.quad isr57
.quad isr58
.quad isr59
.quad isr60
.quad isr61
.quad isr62
.quad isr63
.quad isr64
.quad isr65
.quad isr66
.quad isr67
.quad isr68
.quad isr69
.quad isr70
.quad isr71
.quad isr72
.quad isr73
.quad isr74
.quad isr75
.quad isr76
.quad isr77
.quad isr78
.quad isr79
.quad isr80
.quad isr81
.quad isr82
.quad isr83
.quad isr84
.quad isr85
.quad isr86
.quad isr87
.quad isr88
.quad isr89
.quad isr90
.quad isr91
.quad isr92
.quad isr93
.quad isr94
.quad isr95
.quad isr96
.quad isr97
.quad isr98
.quad isr99
.quad isr100
.quad isr101
.quad isr102
.quad isr103
.quad isr104
.quad isr105
.quad isr106
.quad isr107
.quad isr108
.quad isr109
.quad isr110
.quad isr111
.quad isr112
.quad isr113
.quad isr114
.quad isr115
.quad isr116
.quad isr117
.quad isr118
.quad isr119
.quad isr120
.quad isr121
.quad isr122
.quad isr123
.quad isr124
.quad isr125
.quad isr126
.quad isr127
.quad isr128
.quad isr129
.quad isr130
.quad isr131
.quad isr132
.quad isr133
.quad isr134
.quad isr135
.quad isr136
.quad isr137
.quad isr138
.quad isr139
.quad isr140
.quad isr141
.quad isr142
.quad isr143
.quad isr144
.quad isr145
.quad isr146
.quad isr147
.quad isr148
.quad isr149
.quad isr150
.quad isr151
.quad isr152
.quad isr153
.quad isr154
.quad isr155
.quad isr156
.quad isr157
.quad isr158
.quad isr159
.quad isr160
.quad isr161
.quad isr162
.quad isr163
.quad isr164
.quad isr165
.quad isr166
.quad isr167
.quad isr168
.quad isr169
.quad isr170
.quad isr171
.quad isr172
.quad isr173
.quad isr174
.quad isr175
.quad isr176
.quad isr177
.quad isr178
.quad isr179
.quad isr180
.quad isr181
.quad isr182
.quad isr183
.quad isr184
.quad isr185
.quad isr186
.quad isr187
.quad isr188
.quad isr189
.quad isr190
.quad isr191
.quad isr192
.quad isr193
.quad isr194
.quad isr195
.quad isr196
.quad isr197
.quad isr198
.quad isr199
.quad isr200
.quad isr201
.quad isr202
.quad isr203
.quad isr204
.quad isr205
.quad isr206
.quad isr207
.quad isr208
.quad isr209
.quad isr210
.quad isr211
.quad isr212
.quad isr213
.quad isr214
.quad isr215
.quad isr216
.quad isr217
.quad isr218
.quad isr219
.quad isr220
.quad isr221
.quad isr222
.quad isr223
.quad isr224
.quad isr225
.quad isr226
.quad isr227
.quad isr228
.quad isr229
.quad isr230
.quad isr231
.quad isr232
.quad isr233
.quad isr234
.quad isr235
.quad isr236
.quad isr237
.quad isr238
.quad isr239
.quad isr240
.quad isr241
.quad isr242
.quad isr243
.quad isr244
.quad isr245
.quad isr246
.quad isr247
.quad isr248
.quad isr249
.quad isr250
.quad isr251
.quad isr252
.quad isr253
.quad isr254
.quad isr255
.section .note.GNU-stack,"",@progbits

286
kernel.c
View File

@@ -1,8 +1,25 @@
#include <efi.h>
/*
* kernel.c Kernel entry point and Starling Terminal task.
*
* kmain() is called by the loader (main.c) after the ELF kernel has been
* mapped into memory. It initialises subsystems (IDT, memory, tasks),
* prints a welcome banner, and then spawns the Starling Terminal as a
* dedicated task. The terminal task runs the interactive read-eval-print
* loop that dispatches typed commands via commands.c.
*
* While the terminal waits for keyboard input it yields to the cooperative
* scheduler so that background tasks can make progress.
*/
#include "kernel_types.h"
#include "boot_info.h"
#include "commands.h"
#include "idt.h"
#include "memory.h"
#include "task.h"
#include "string_utils.h"
/* Null-safe print helper used throughout the kernel. */
#define SAFE_PRINT(Boot, ...) \
do { \
if ((Boot) != NULL && (Boot)->print != NULL) { \
@@ -10,11 +27,158 @@
} \
} while (0)
/* Simple text attribute helper (modelled after UEFI TEXT_ATTR). */
#define TEXT_ATTR(fg, bg) (UINTN)(((fg) & 0x0F) | (((bg) & 0x0F) << 4))
/* Basic colour constants (compatible with UEFI's palette). */
#define COLOR_BLACK 0x0
#define COLOR_LIGHTGREEN 0xA
/* Simple context passed to each Starling Terminal instance. */
typedef struct {
BootInfo *Boot;
UINTN depth; /* 0 = top-level, 1+ = nested shells */
TaskPrivilege shell_priv; /* logical privilege for this shell */
} StarlingContext;
/* ================================================================
* Starling Terminal task interactive command loop
* ================================================================ */
static void starling_terminal_task(void *arg)
{
StarlingContext *ctx = (StarlingContext *)arg;
BootInfo *Boot = NULL;
KeyEvent Key;
KSTATUS Status;
UINTN read_errors = 0;
CHAR16 line[128];
UINTN len = 0;
UINTN depth = 0;
TaskPrivilege shell_priv;
if (ctx == NULL || ctx->Boot == NULL) {
return;
}
Boot = ctx->Boot;
depth = ctx->depth;
shell_priv = ctx->shell_priv;
SAFE_PRINT(Boot, L"\n\r[Starling Terminal depth %d, priv %d] ready.\n\r\n\r",
depth, (INT32)shell_priv);
SAFE_PRINT(Boot, L"starling> ");
while (TRUE) {
/* Try non-blocking read first; yield to other tasks while idle */
if (Boot->try_read_key != NULL) {
Status = Boot->try_read_key(&Key);
if (Status != 0) {
task_yield();
continue;
}
} else if (Boot->read_key != NULL) {
Status = Boot->read_key(&Key);
} else {
SAFE_PRINT(Boot, L"Console input unavailable.\n\r");
break;
}
if (Status != 0) {
read_errors++;
if (read_errors == 1 || (read_errors % 64) == 0) {
SAFE_PRINT(Boot, L"read_key failed (status=%ld)\n\r",
(UINT64)Status);
}
continue;
}
read_errors = 0;
if (Key.unicode_char == L'\r' || Key.unicode_char == L'\n') {
/* Enter pressed: execute the buffered command */
line[len] = L'\0';
SAFE_PRINT(Boot, L"\n\r");
trim_spaces_inplace(line);
/* Built-in terminal controls: exit / spawn nested Starling. */
if (ascii_streq_ci(line, L"exit")) {
if (depth == 0) {
SAFE_PRINT(Boot, L"Starling: cannot exit top-level terminal.\n\r");
SAFE_PRINT(Boot, L"starling> ");
} else {
SAFE_PRINT(Boot, L"Exiting Starling Terminal depth %d...\n\r", depth);
break;
}
} else if (ascii_streq_ci(line, L"starling")) {
StarlingContext *child_ctx;
Task *child_task;
child_ctx = (StarlingContext *)kmalloc(sizeof(StarlingContext));
if (child_ctx == NULL) {
SAFE_PRINT(Boot, L"Starling: failed to allocate nested terminal context.\n\r");
SAFE_PRINT(Boot, L"starling> ");
} else {
child_ctx->Boot = Boot;
child_ctx->depth = depth + 1;
child_ctx->shell_priv = shell_priv;
child_task = task_create_with_priv(L"starling-term",
starling_terminal_task,
child_ctx,
shell_priv);
if (child_task == NULL) {
SAFE_PRINT(Boot, L"Starling: failed to spawn nested terminal.\n\r");
kfree(child_ctx);
SAFE_PRINT(Boot, L"starling> ");
} else {
SAFE_PRINT(Boot, L"[starling] spawned nested terminal (PID %d, depth %d)\n\r",
child_task->pid, child_ctx->depth);
/* Block this shell until the child terminal exits. */
task_wait(child_task);
SAFE_PRINT(Boot, L"[starling] returned from nested terminal (depth %d)\n\r", depth);
SAFE_PRINT(Boot, L"starling> ");
}
}
} else {
Task *cmd_task = execute_command(Boot, line, shell_priv);
/* If a command task was spawned, wait for it to finish. */
if (cmd_task != NULL) {
task_wait(cmd_task);
}
/* Reset for next command */
len = 0;
SAFE_PRINT(Boot, L"starling> ");
}
} else if (Key.scan_code == 0x08 || Key.unicode_char == L'\b' || Key.unicode_char == 0x7F) {
/* Backspace */
if (len > 0) {
len--;
SAFE_PRINT(Boot, L"\b \b");
}
} else if (Key.unicode_char >= 32 && Key.unicode_char < 127) {
/* Printable ASCII */
if (len < (sizeof(line) / sizeof(line[0]) - 1)) {
line[len++] = Key.unicode_char;
SAFE_PRINT(Boot, L"%c", Key.unicode_char);
}
}
}
/* Free our context on exit (allocated by the spawner). */
kfree(ctx);
}
/* ================================================================
* Kernel entry point
* ================================================================ */
void kmain(BootInfo *Boot)
{
EFI_INPUT_KEY Key;
EFI_STATUS Status;
UINTN read_errors = 0;
KSTATUS Status;
Task *terminal_task = NULL;
StarlingContext *ctx = NULL;
if (Boot == NULL) {
return;
@@ -22,88 +186,82 @@ void kmain(BootInfo *Boot)
if (Boot->clear_screen != NULL) {
Status = Boot->clear_screen();
if (EFI_ERROR(Status)) {
SAFE_PRINT(Boot, L"clear_screen failed: %r\n\r", Status);
if (Status != 0) {
SAFE_PRINT(Boot, L"clear_screen failed (status=%ld)\n\r",
(UINT64)Status);
}
}
if (Boot->set_attribute != NULL) {
Status = Boot->set_attribute(EFI_TEXT_ATTR(EFI_LIGHTGREEN, EFI_BLACK));
if (EFI_ERROR(Status)) {
SAFE_PRINT(Boot, L"set_attribute failed: %r\n\r", Status);
Status = Boot->set_attribute(TEXT_ATTR(COLOR_LIGHTGREEN, COLOR_BLACK));
if (Status != 0) {
SAFE_PRINT(Boot, L"set_attribute failed (status=%ld)\n\r",
(UINT64)Status);
}
}
SAFE_PRINT(Boot, L"================================================\n\r");
SAFE_PRINT(Boot, L" Welcome to Simple UEFI Operating System!\n\r");
/* ---- Subsystem initialisation ---- */
idt_init(Boot);
memory_init(Boot);
task_init(Boot);
/* ---- Welcome banner ---- */
SAFE_PRINT(Boot, L" Welcome to Simple 64-bit Operating System!\n\r");
SAFE_PRINT(Boot, L"================================================\n\r");
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"System Information:\n\r");
if (Boot->SystemTable != NULL) {
SAFE_PRINT(Boot, L" UEFI Firmware Vendor: %s\n\r",
Boot->SystemTable->FirmwareVendor);
SAFE_PRINT(Boot, L" UEFI Firmware Revision: %d.%d\n\r",
Boot->SystemTable->FirmwareRevision >> 16,
Boot->SystemTable->FirmwareRevision & 0xFFFF);
if (Boot->firmware_vendor != NULL) {
SAFE_PRINT(Boot, L" Firmware Vendor: %s\n\r", Boot->firmware_vendor);
SAFE_PRINT(Boot, L" Firmware Revision: %d.%d\n\r",
Boot->firmware_major, Boot->firmware_minor);
} else {
SAFE_PRINT(Boot, L" UEFI System Table: Unavailable\n\r");
SAFE_PRINT(Boot, L" Firmware information: Unavailable\n\r");
}
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Available Services:\n\r");
SAFE_PRINT(Boot, L" - Console Input/Output: %s\n\r",
(Boot->read_key != NULL && Boot->print != NULL) ? L"Active" : L"Unavailable");
SAFE_PRINT(Boot, L" - Runtime Services: %s\n\r",
(Boot->SystemTable != NULL &&
Boot->SystemTable->RuntimeServices != NULL) ? L"Active" : L"Unavailable");
SAFE_PRINT(Boot, L" - Boot Services: %s\n\r",
(Boot->SystemTable != NULL &&
Boot->SystemTable->BootServices != NULL) ? L"Active" : L"Unavailable");
SAFE_PRINT(Boot, L" - Terminal: Starling Terminal\n\r");
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Type 'help' for a list of commands.\n\r\n\r");
// Simple line buffer
CHAR16 line[128];
UINTN len = 0;
/* ---- Spawn Starling Terminal as its own task ---- */
ctx = (StarlingContext *)kmalloc(sizeof(StarlingContext));
if (ctx == NULL) {
SAFE_PRINT(Boot, L"Failed to allocate Starling Terminal context; starting inline.\n\r");
StarlingContext inline_ctx;
inline_ctx.Boot = Boot;
inline_ctx.depth = 0;
inline_ctx.shell_priv = TASK_PRIV_USER;
starling_terminal_task(&inline_ctx);
return;
}
SAFE_PRINT(Boot, L"-> ");
ctx->Boot = Boot;
ctx->depth = 0;
ctx->shell_priv = TASK_PRIV_USER;
terminal_task = task_create_with_priv(L"starling-term",
starling_terminal_task,
ctx,
TASK_PRIV_USER);
if (terminal_task == NULL) {
SAFE_PRINT(Boot, L"Failed to start Starling Terminal task; falling back to kernel loop.\n\r");
/*
* Fall back to running the terminal loop directly in the core
* thread so the system remains usable even if task creation
* fails for some reason.
*/
starling_terminal_task(Boot);
return;
}
SAFE_PRINT(Boot, L"[core] Started Starling Terminal (PID %d).\n\r", terminal_task->pid);
/* Core thread becomes an idle loop, yielding to the terminal and others. */
while (TRUE) {
if (Boot->read_key == NULL) {
SAFE_PRINT(Boot, L"Console input unavailable.\n\r");
break;
}
Status = Boot->read_key(&Key);
if (EFI_ERROR(Status)) {
read_errors++;
if (read_errors == 1 || (read_errors % 64) == 0) {
SAFE_PRINT(Boot, L"read_key failed: %r\n\r", Status);
}
continue;
}
read_errors = 0;
if (Key.UnicodeChar == L'\r' || Key.UnicodeChar == L'\n') {
// Enter pressed: terminate string and handle
line[len] = L'\0';
SAFE_PRINT(Boot, L"\n\r");
execute_command(Boot, line);
// Reset for next command
len = 0;
SAFE_PRINT(Boot, L"-> ");
} else if (Key.ScanCode == 0x08 || Key.UnicodeChar == L'\b' || Key.UnicodeChar == 0x7F) {
// Backspace (ScanCode 0x08 is backspace in UEFI)
if (len > 0) {
len--;
SAFE_PRINT(Boot, L"\b \b");
}
} else if (Key.UnicodeChar >= 32 && Key.UnicodeChar < 127) {
if (len < (sizeof(line) / sizeof(line[0]) - 1)) {
line[len++] = Key.UnicodeChar;
SAFE_PRINT(Boot, L"%c", Key.UnicodeChar);
}
}
task_yield();
}
}

View File

@@ -1,25 +1,33 @@
/*
* kernel.ld Linker script for the ELF64 kernel.
*
* The kernel is loaded at physical address 0x100000 (1 MB) by the
* UEFI loader. Each section is page-aligned (4 KB) so that page-
* level permissions can be applied later if desired.
*/
ENTRY(kmain)
SECTIONS
{
. = 0x100000;
. = 0x100000; /* load address: 1 MB */
.text : ALIGN(0x1000)
.text : ALIGN(0x1000) /* executable code */
{
*(.text*)
}
.rodata : ALIGN(0x1000)
.rodata : ALIGN(0x1000) /* read-only data / string literals */
{
*(.rodata*)
}
.data : ALIGN(0x1000)
.data : ALIGN(0x1000) /* initialised read-write data */
{
*(.data*)
}
.bss : ALIGN(0x1000)
.bss : ALIGN(0x1000) /* zero-initialised data */
{
*(COMMON)
*(.bss*)

40
kernel_types.h Normal file
View File

@@ -0,0 +1,40 @@
/*
* kernel_types.h Common fixed-width and utility types for the kernel.
*
* This header deliberately avoids pulling in any firmware- or
* platform-specific headers (such as UEFI's <efi.h>). All core kernel
* code should include this instead of <efi.h>.
*/
#ifndef KERNEL_TYPES_H
#define KERNEL_TYPES_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef uint8_t UINT8;
typedef uint16_t UINT16;
typedef uint32_t UINT32;
typedef uint64_t UINT64;
typedef int32_t INT32;
typedef size_t UINTN;
#ifndef BOOLEAN
typedef bool BOOLEAN;
#endif
typedef uint16_t CHAR16;
#ifndef TRUE
#define TRUE ((BOOLEAN)true)
#endif
#ifndef FALSE
#define FALSE ((BOOLEAN)false)
#endif
#endif /* KERNEL_TYPES_H */

171
main.c
View File

@@ -1,45 +1,70 @@
/*
* main.c UEFI boot loader.
*
* This is the first code that runs. efi_main() is called by UEFI
* firmware via the PE32+ entry point. It:
* 1. Reads kernel.elf from the EFI System Partition
* 2. Parses the ELF64 headers and maps PT_LOAD segments into memory
* 3. Populates a BootInfo struct with generic service wrappers
* 4. Jumps to the kernel entry point (kmain)
*/
#include <efi.h>
#include <efilib.h>
#include "boot_info.h"
#define ELF_MAGIC 0x464c457f
#define PT_LOAD 1
/* ================================================================
* ELF64 constants and types
* ================================================================ */
#define ELF_MAGIC 0x464c457f /* "\x7fELF" as a little-endian UINT32 */
#define PT_LOAD 1 /* loadable program segment */
/* Minimal ELF64 file header (enough to locate program headers). */
typedef struct {
UINT32 e_magic;
UINT8 e_class;
UINT8 e_data;
UINT8 e_version;
UINT8 e_osabi;
UINT8 e_abiversion;
UINT8 e_pad[7];
UINT16 e_type;
UINT16 e_machine;
UINT32 e_magic; /* must be ELF_MAGIC */
UINT8 e_class; /* 2 = 64-bit */
UINT8 e_data; /* 1 = little-endian */
UINT8 e_version;
UINT8 e_osabi;
UINT8 e_abiversion;
UINT8 e_pad[7];
UINT16 e_type; /* ET_EXEC = 2 */
UINT16 e_machine; /* EM_X86_64 = 62 */
UINT32 e_version2;
UINT64 e_entry;
UINT64 e_phoff;
UINT64 e_entry; /* virtual address of entry point */
UINT64 e_phoff; /* file offset to program header table */
UINT64 e_shoff;
UINT32 e_flags;
UINT16 e_ehsize;
UINT16 e_phentsize;
UINT16 e_phnum;
UINT16 e_phentsize; /* size of one program header entry */
UINT16 e_phnum; /* number of program header entries */
UINT16 e_shentsize;
UINT16 e_shnum;
UINT16 e_shstrndx;
} Elf64_Ehdr;
/* ELF64 program header (describes one segment). */
typedef struct {
UINT32 p_type;
UINT32 p_flags;
UINT64 p_offset;
UINT64 p_vaddr;
UINT64 p_paddr;
UINT64 p_filesz;
UINT64 p_memsz;
UINT64 p_align;
UINT32 p_type; /* segment type (PT_LOAD, etc.) */
UINT32 p_flags; /* segment flags (R/W/X) */
UINT64 p_offset; /* file offset of segment data */
UINT64 p_vaddr; /* virtual address to map at */
UINT64 p_paddr; /* physical address (usually == vaddr) */
UINT64 p_filesz; /* bytes of segment data in file */
UINT64 p_memsz; /* total bytes in memory (>= filesz) */
UINT64 p_align; /* alignment requirement */
} Elf64_Phdr;
/* ================================================================
* EFI file-system helpers
* ================================================================ */
/*
* Open the root directory of the volume from which this loader was
* loaded (i.e. the EFI System Partition).
*/
static EFI_STATUS open_root_volume(EFI_HANDLE ImageHandle, EFI_FILE_PROTOCOL **Root)
{
EFI_STATUS Status;
@@ -61,6 +86,10 @@ static EFI_STATUS open_root_volume(EFI_HANDLE ImageHandle, EFI_FILE_PROTOCOL **R
return uefi_call_wrapper(FileSystem->OpenVolume, 2, FileSystem, Root);
}
/*
* Read an entire file from the EFI System Partition into a pool buffer.
* Caller must free *Buffer with BS->FreePool() when done.
*/
static EFI_STATUS read_file_to_buffer(EFI_HANDLE ImageHandle, CHAR16 *Path,
VOID **Buffer, UINTN *Size)
{
@@ -127,6 +156,15 @@ static EFI_STATUS read_file_to_buffer(EFI_HANDLE ImageHandle, CHAR16 *Path,
return EFI_SUCCESS;
}
/* ================================================================
* ELF64 kernel loader
* ================================================================ */
/*
* Parse an ELF64 image in memory and load all PT_LOAD segments to
* their specified virtual (== physical) addresses. On success,
* *EntryOut is set to the kernel entry point.
*/
static EFI_STATUS load_elf_kernel(VOID *Image, UINTN Size, UINT64 *EntryOut)
{
Elf64_Ehdr *Ehdr = (Elf64_Ehdr *)Image;
@@ -185,21 +223,48 @@ static EFI_STATUS load_elf_kernel(VOID *Image, UINTN Size, UINT64 *EntryOut)
return EFI_SUCCESS;
}
static EFI_STATUS loader_clear_screen(void)
/* ================================================================
* UEFI service wrappers (passed to the kernel via BootInfo)
* ================================================================ */
static KSTATUS loader_clear_screen(void)
{
return uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut);
EFI_STATUS Status = uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut);
return (KSTATUS)Status;
}
static EFI_STATUS loader_set_attribute(UINTN Attribute)
static KSTATUS loader_set_attribute(UINTN Attribute)
{
return uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, Attribute);
EFI_STATUS Status = uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, Attribute);
return (KSTATUS)Status;
}
static EFI_STATUS loader_read_key(EFI_INPUT_KEY *Key)
static KSTATUS loader_read_key(KeyEvent *Key)
{
EFI_STATUS Status;
EFI_INPUT_KEY EfiKey;
UINTN Index = 0;
uefi_call_wrapper(BS->WaitForEvent, 3, 1, &ST->ConIn->WaitForKey, &Index);
return uefi_call_wrapper(ST->ConIn->ReadKeyStroke, 2, ST->ConIn, Key);
Status = uefi_call_wrapper(ST->ConIn->ReadKeyStroke, 2, ST->ConIn, &EfiKey);
if (!EFI_ERROR(Status) && Key != NULL) {
Key->scan_code = EfiKey.ScanCode;
Key->unicode_char = EfiKey.UnicodeChar;
}
return (KSTATUS)Status;
}
static KSTATUS loader_try_read_key(KeyEvent *Key)
{
EFI_STATUS Status;
EFI_INPUT_KEY EfiKey;
Status = uefi_call_wrapper(ST->ConIn->ReadKeyStroke, 2, ST->ConIn, &EfiKey);
if (!EFI_ERROR(Status) && Key != NULL) {
Key->scan_code = EfiKey.ScanCode;
Key->unicode_char = EfiKey.UnicodeChar;
}
return (KSTATUS)Status;
}
static void loader_shutdown(void)
@@ -207,6 +272,29 @@ static void loader_shutdown(void)
uefi_call_wrapper(RT->ResetSystem, 4, EfiResetShutdown, EFI_SUCCESS, 0, NULL);
}
static KSTATUS loader_alloc_pages(UINTN pages, UINT64 *addr)
{
EFI_PHYSICAL_ADDRESS Phys = 0;
EFI_STATUS Status = uefi_call_wrapper(BS->AllocatePages, 4,
AllocateAnyPages, EfiLoaderData,
pages, &Phys);
if (!EFI_ERROR(Status) && addr != NULL) {
*addr = (UINT64)Phys;
}
return (KSTATUS)Status;
}
static KSTATUS loader_free_pages(UINT64 addr, UINTN pages)
{
EFI_STATUS Status = uefi_call_wrapper(BS->FreePages, 2,
(EFI_PHYSICAL_ADDRESS)addr, pages);
return (KSTATUS)Status;
}
/* ================================================================
* UEFI application entry point
* ================================================================ */
EFI_STATUS
EFIAPI
efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
@@ -217,10 +305,10 @@ efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
UINT64 KernelEntry = 0;
BootInfo Boot;
KernelEntryFn EntryFn = NULL;
// Initialize the GNU-EFI library
/* Initialise the GNU-EFI library */
InitializeLib(ImageHandle, SystemTable);
Print(L"Loading kernel...\n\r");
Status = read_file_to_buffer(ImageHandle, L"\\kernel.elf", &KernelImage, &KernelSize);
@@ -235,13 +323,20 @@ efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
return Status;
}
Boot.SystemTable = ST;
Boot.print = Print;
Boot.clear_screen = loader_clear_screen;
Boot.set_attribute = loader_set_attribute;
Boot.read_key = loader_read_key;
Boot.shutdown = loader_shutdown;
/* Populate the BootInfo struct with generic UEFI-backed services */
Boot.print = Print;
Boot.clear_screen = loader_clear_screen;
Boot.set_attribute = loader_set_attribute;
Boot.read_key = loader_read_key;
Boot.try_read_key = loader_try_read_key;
Boot.shutdown = loader_shutdown;
Boot.alloc_pages = loader_alloc_pages;
Boot.free_pages = loader_free_pages;
Boot.firmware_vendor = SystemTable->FirmwareVendor;
Boot.firmware_major = (SystemTable->FirmwareRevision >> 16) & 0xFFFF;
Boot.firmware_minor = SystemTable->FirmwareRevision & 0xFFFF;
/* Jump to the kernel this should not return */
EntryFn = (KernelEntryFn)(UINTN)KernelEntry;
EntryFn(&Boot);

571
memory.c Normal file
View File

@@ -0,0 +1,571 @@
/*
* memory.c Kernel memory management.
*
* Implements three layers:
* PMM bitmap-based physical page-frame allocator backed by a
* 16 MB pool obtained from the loader at boot.
* Paging walks and creates 4-level x86-64 page tables; supports
* map, unmap, and virtual-to-physical translation.
* Heap first-fit free-list allocator with block splitting and
* bidirectional coalescing; 16-byte aligned.
*/
#include "memory.h"
#include "task.h"
/* Null-safe print helper used throughout the kernel. */
#define SAFE_PRINT(Boot, ...) \
do { \
if ((Boot) != NULL && (Boot)->print != NULL) { \
(Boot)->print(__VA_ARGS__); \
} \
} while (0)
/* ================================================================
* Physical Memory Manager bitmap-based page-frame allocator
* ================================================================ */
static UINT64 pmm_pool_base = 0;
static UINTN pmm_total_pages = 0;
static UINTN pmm_free_count = 0;
static UINT8 pmm_bitmap[PMM_POOL_PAGES / 8];
static BOOLEAN pmm_ready = FALSE;
/* ================================================================
* PMM bitmap helpers
* ================================================================ */
/* Mark page `idx` as allocated. */
static void pmm_set_bit(UINTN idx)
{
pmm_bitmap[idx / 8] |= (UINT8)(1U << (idx % 8));
}
/* Mark page `idx` as free. */
static void pmm_clear_bit(UINTN idx)
{
pmm_bitmap[idx / 8] &= (UINT8)~(1U << (idx % 8));
}
/* Return TRUE if page `idx` is currently allocated. */
static BOOLEAN pmm_test_bit(UINTN idx)
{
return (pmm_bitmap[idx / 8] & (1U << (idx % 8))) != 0;
}
/* ----------------------------------------------------------------
* PMM public interface
* ---------------------------------------------------------------- */
/*
* Initialise the PMM: request PMM_POOL_PAGES from the loader via
* BootInfo->alloc_pages() and set up the bitmap with all pages
* marked free.
*/
void pmm_init(BootInfo *Boot)
{
KSTATUS Status;
UINT64 pool_addr = 0;
UINTN i;
/* Zero the bitmap all pages start free */
for (i = 0; i < sizeof(pmm_bitmap); i++) {
pmm_bitmap[i] = 0;
}
if (Boot == NULL || Boot->alloc_pages == NULL) {
SAFE_PRINT(Boot, L"PMM: page allocator unavailable\n\r");
return;
}
Status = Boot->alloc_pages(PMM_POOL_PAGES, &pool_addr);
if (Status != 0) {
SAFE_PRINT(Boot, L"PMM: failed to allocate pool (%d pages), status=%ld\n\r",
(UINTN)PMM_POOL_PAGES, (UINT64)Status);
return;
}
pmm_pool_base = (UINT64)pool_addr;
pmm_total_pages = PMM_POOL_PAGES;
pmm_free_count = PMM_POOL_PAGES;
pmm_ready = TRUE;
SAFE_PRINT(Boot, L" PMM : %d pages (%d KB) at 0x%lx\n\r",
pmm_total_pages,
(pmm_total_pages * PAGE_SIZE) / 1024,
pmm_pool_base);
}
/* Allocate a single 4 KB page. Returns physical address or 0. */
UINT64 pmm_alloc_page(void)
{
UINTN i;
if (!pmm_ready || pmm_free_count == 0) {
return 0;
}
for (i = 0; i < pmm_total_pages; i++) {
if (!pmm_test_bit(i)) {
pmm_set_bit(i);
pmm_free_count--;
return pmm_pool_base + ((UINT64)i * PAGE_SIZE);
}
}
return 0;
}
/* Free a single page previously returned by pmm_alloc_page(). */
void pmm_free_page(UINT64 phys_addr)
{
UINTN idx;
if (!pmm_ready) return;
if (phys_addr < pmm_pool_base) return;
idx = (UINTN)((phys_addr - pmm_pool_base) / PAGE_SIZE);
if (idx >= pmm_total_pages) return;
if (!pmm_test_bit(idx)) return; /* already free */
pmm_clear_bit(idx);
pmm_free_count++;
}
/* Allocate `count` physically contiguous pages (first-fit). */
UINT64 pmm_alloc_pages(UINTN count)
{
UINTN i, j;
BOOLEAN found;
if (!pmm_ready || count == 0 || count > pmm_total_pages
|| pmm_free_count < count) {
return 0;
}
for (i = 0; i + count <= pmm_total_pages; i++) {
found = TRUE;
for (j = 0; j < count; j++) {
if (pmm_test_bit(i + j)) {
found = FALSE;
i += j; /* skip past the used page */
break;
}
}
if (found) {
for (j = 0; j < count; j++) {
pmm_set_bit(i + j);
}
pmm_free_count -= count;
return pmm_pool_base + ((UINT64)i * PAGE_SIZE);
}
}
return 0;
}
/* Free `count` contiguous pages starting at phys_addr. */
void pmm_free_pages(UINT64 phys_addr, UINTN count)
{
UINTN i;
for (i = 0; i < count; i++) {
pmm_free_page(phys_addr + ((UINT64)i * PAGE_SIZE));
}
}
UINTN pmm_get_free_pages(void) { return pmm_free_count; }
UINTN pmm_get_total_pages(void) { return pmm_total_pages; }
/* ================================================================
* Paging manipulate the live 4-level x86-64 page tables
* ================================================================ */
/* ================================================================
* Paging low-level helpers
* ================================================================ */
/* Read the CR3 register (physical address of PML4). */
static UINT64 read_cr3(void)
{
UINT64 cr3;
__asm__ __volatile__("mov %%cr3, %0" : "=r"(cr3));
return cr3;
}
/* Invalidate the TLB entry for virtual address `addr`. */
static void invlpg(UINT64 addr)
{
__asm__ __volatile__("invlpg (%0)" :: "r"(addr) : "memory");
}
/* Return a pointer to the current PML4 table. */
static UINT64 *get_pml4(void)
{
return (UINT64 *)(UINTN)(read_cr3() & PTE_ADDR_MASK);
}
/*
* Walk one level of the page table hierarchy.
* If `create` is TRUE and the entry is missing, a fresh zeroed page is
* allocated from the PMM and installed.
*/
static UINT64 *paging_walk_level(UINT64 *table, UINTN index, BOOLEAN create)
{
UINT64 *next;
UINTN i;
UINT64 page;
if (table[index] & PTE_PRESENT) {
return (UINT64 *)(UINTN)(table[index] & PTE_ADDR_MASK);
}
if (!create) {
return NULL;
}
page = pmm_alloc_page();
if (page == 0) {
return NULL;
}
/* Zero the freshly-allocated page table */
next = (UINT64 *)(UINTN)page;
for (i = 0; i < PAGE_SIZE / sizeof(UINT64); i++) {
next[i] = 0;
}
table[index] = page | PTE_PRESENT | PTE_WRITABLE;
return next;
}
/* ----------------------------------------------------------------
* Paging public interface
* ---------------------------------------------------------------- */
/* Log the current CR3 value (identity-mapped by UEFI). */
void paging_init(BootInfo *Boot)
{
SAFE_PRINT(Boot, L" Page: CR3 = 0x%lx (identity-mapped by loader)\n\r",
read_cr3());
}
/*
* Map a single 4 KB page: virt → phys with the given flags.
* Returns TRUE on success, FALSE if a huge page is in the way or
* page-table allocation failed.
*/
BOOLEAN paging_map_page(UINT64 virt, UINT64 phys, UINT64 flags)
{
UINT64 *pml4, *pdpt, *pd, *pt;
UINTN pml4i, pdpti, pdi, pti;
pml4i = (virt >> 39) & 0x1FF;
pdpti = (virt >> 30) & 0x1FF;
pdi = (virt >> 21) & 0x1FF;
pti = (virt >> 12) & 0x1FF;
pml4 = get_pml4();
pdpt = paging_walk_level(pml4, pml4i, TRUE);
if (pdpt == NULL) return FALSE;
/* 1 GB huge page cannot carve a 4 KB mapping inside it */
if (pdpt[pdpti] & PTE_HUGE) return FALSE;
pd = paging_walk_level(pdpt, pdpti, TRUE);
if (pd == NULL) return FALSE;
/* 2 MB huge page cannot carve a 4 KB mapping inside it */
if (pd[pdi] & PTE_HUGE) return FALSE;
pt = paging_walk_level(pd, pdi, TRUE);
if (pt == NULL) return FALSE;
pt[pti] = (phys & PTE_ADDR_MASK) | flags | PTE_PRESENT;
invlpg(virt);
return TRUE;
}
/* Remove the mapping for a single 4 KB page and flush the TLB. */
void paging_unmap_page(UINT64 virt)
{
UINT64 *pml4, *pdpt, *pd, *pt;
UINTN pml4i, pdpti, pdi, pti;
pml4i = (virt >> 39) & 0x1FF;
pdpti = (virt >> 30) & 0x1FF;
pdi = (virt >> 21) & 0x1FF;
pti = (virt >> 12) & 0x1FF;
pml4 = get_pml4();
pdpt = paging_walk_level(pml4, pml4i, FALSE);
if (pdpt == NULL) return;
if (pdpt[pdpti] & PTE_HUGE) return;
pd = paging_walk_level(pdpt, pdpti, FALSE);
if (pd == NULL) return;
if (pd[pdi] & PTE_HUGE) return;
pt = paging_walk_level(pd, pdi, FALSE);
if (pt == NULL) return;
pt[pti] = 0;
invlpg(virt);
}
/*
* Translate a virtual address to its physical counterpart.
* Handles 4 KB, 2 MB, and 1 GB page sizes. Returns 0 if unmapped.
*/
UINT64 paging_get_phys(UINT64 virt)
{
UINT64 *pml4, *pdpt, *pd, *pt;
UINTN pml4i, pdpti, pdi, pti;
pml4i = (virt >> 39) & 0x1FF;
pdpti = (virt >> 30) & 0x1FF;
pdi = (virt >> 21) & 0x1FF;
pti = (virt >> 12) & 0x1FF;
pml4 = get_pml4();
if (!(pml4[pml4i] & PTE_PRESENT)) return 0;
pdpt = (UINT64 *)(UINTN)(pml4[pml4i] & PTE_ADDR_MASK);
if (!(pdpt[pdpti] & PTE_PRESENT)) return 0;
if (pdpt[pdpti] & PTE_HUGE) {
/* 1 GB page */
return (pdpt[pdpti] & 0x000FFFFFC0000000ULL) | (virt & 0x3FFFFFFFULL);
}
pd = (UINT64 *)(UINTN)(pdpt[pdpti] & PTE_ADDR_MASK);
if (!(pd[pdi] & PTE_PRESENT)) return 0;
if (pd[pdi] & PTE_HUGE) {
/* 2 MB page */
return (pd[pdi] & 0x000FFFFFFFE00000ULL) | (virt & 0x1FFFFFULL);
}
pt = (UINT64 *)(UINTN)(pd[pdi] & PTE_ADDR_MASK);
if (!(pt[pti] & PTE_PRESENT)) return 0;
return (pt[pti] & PTE_ADDR_MASK) | (virt & 0xFFFULL);
}
/* ================================================================
* Heap Allocator first-fit free-list with coalescing
* ================================================================ */
static HeapBlock *heap_start = NULL;
static BOOLEAN heap_ready = FALSE;
/* Round `val` up to the next multiple of `align`. */
static UINTN align_up(UINTN val, UINTN align)
{
return (val + align - 1) & ~(align - 1);
}
/*
* Initialise the heap: allocate HEAP_INITIAL_PAGES from the PMM
* and set up a single free block spanning the entire region.
*/
void heap_init(BootInfo *Boot)
{
UINT64 phys;
UINTN heap_size;
phys = pmm_alloc_pages(HEAP_INITIAL_PAGES);
if (phys == 0) {
SAFE_PRINT(Boot, L" Heap: failed to allocate pages\n\r");
return;
}
heap_size = HEAP_INITIAL_PAGES * PAGE_SIZE;
heap_start = (HeapBlock *)(UINTN)phys;
heap_start->magic = HEAP_BLOCK_MAGIC;
heap_start->state = HEAP_BLOCK_FREE;
heap_start->size = heap_size - sizeof(HeapBlock);
heap_start->next = NULL;
heap_start->prev = NULL;
heap_ready = TRUE;
SAFE_PRINT(Boot, L" Heap: %d KB at 0x%lx\n\r",
heap_size / 1024, phys);
}
/*
* Allocate `size` bytes from the heap (first-fit).
* The returned pointer is aligned to HEAP_ALIGN. Returns NULL on
* failure or heap corruption.
*/
void *kmalloc(UINTN size)
{
HeapBlock *block, *split;
UINTN aligned;
if (!heap_ready || size == 0) {
return NULL;
}
aligned = align_up(size, HEAP_ALIGN);
for (block = heap_start; block != NULL; block = block->next) {
if (block->magic != HEAP_BLOCK_MAGIC) {
return NULL; /* heap corruption */
}
if (block->state != HEAP_BLOCK_FREE || block->size < aligned) {
continue;
}
/* Try to split if there is room for another header + 16 bytes */
if (block->size >= aligned + sizeof(HeapBlock) + HEAP_ALIGN) {
split = (HeapBlock *)((UINT8 *)block + sizeof(HeapBlock) + aligned);
split->magic = HEAP_BLOCK_MAGIC;
split->state = HEAP_BLOCK_FREE;
split->size = block->size - aligned - sizeof(HeapBlock);
split->next = block->next;
split->prev = block;
if (block->next != NULL) {
block->next->prev = split;
}
block->next = split;
block->size = aligned;
}
block->state = HEAP_BLOCK_USED;
return (void *)((UINT8 *)block + sizeof(HeapBlock));
}
return NULL; /* out of heap memory */
}
/*
* Free a previously kmalloc'd pointer. Coalesces adjacent free
* blocks to reduce fragmentation.
*/
void kfree(void *ptr)
{
HeapBlock *block;
if (ptr == NULL || !heap_ready) {
return;
}
block = (HeapBlock *)((UINT8 *)ptr - sizeof(HeapBlock));
if (block->magic != HEAP_BLOCK_MAGIC || block->state != HEAP_BLOCK_USED) {
return; /* bad pointer or double-free */
}
block->state = HEAP_BLOCK_FREE;
/* Coalesce with next neighbour */
if (block->next != NULL
&& block->next->magic == HEAP_BLOCK_MAGIC
&& block->next->state == HEAP_BLOCK_FREE) {
block->size += sizeof(HeapBlock) + block->next->size;
block->next = block->next->next;
if (block->next != NULL) {
block->next->prev = block;
}
}
/* Coalesce with previous neighbour */
if (block->prev != NULL
&& block->prev->magic == HEAP_BLOCK_MAGIC
&& block->prev->state == HEAP_BLOCK_FREE) {
block->prev->size += sizeof(HeapBlock) + block->size;
block->prev->next = block->next;
if (block->next != NULL) {
block->next->prev = block->prev;
}
}
}
/* Gather aggregate heap statistics. */
void heap_get_stats(UINTN *total, UINTN *used, UINTN *free_mem,
UINTN *num_blocks)
{
HeapBlock *b;
*total = 0; *used = 0; *free_mem = 0; *num_blocks = 0;
if (!heap_ready) return;
for (b = heap_start; b != NULL && b->magic == HEAP_BLOCK_MAGIC;
b = b->next) {
(*num_blocks)++;
*total += b->size;
if (b->state == HEAP_BLOCK_USED) {
*used += b->size;
} else {
*free_mem += b->size;
}
}
}
/* ================================================================
* Top-level helpers
* ================================================================ */
/* Initialise all memory subsystems in order. */
void memory_init(BootInfo *Boot)
{
SAFE_PRINT(Boot, L"Initializing memory management...\n\r");
pmm_init(Boot);
paging_init(Boot);
heap_init(Boot);
SAFE_PRINT(Boot, L"Memory management ready.\n\r\n\r");
}
/* Print a summary of PMM, heap, and paging state to the console. */
void memory_print_stats(BootInfo *Boot)
{
UINTN h_total, h_used, h_free, h_blocks;
UINTN p_total, p_free, p_used;
Task *caller;
/* Subsystem-level privilege enforcement: memory stats require KERNEL. */
caller = task_current();
if (caller != NULL && task_get_privilege(caller) < TASK_PRIV_KERNEL) {
SAFE_PRINT(Boot, L"Permission denied: memory stats require kernel privilege.\n\r");
return;
}
p_total = pmm_get_total_pages();
p_free = pmm_get_free_pages();
p_used = p_total - p_free;
heap_get_stats(&h_total, &h_used, &h_free, &h_blocks);
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Memory Statistics\n\r");
SAFE_PRINT(Boot, L"================================================\n\r");
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Physical Memory Manager:\n\r");
SAFE_PRINT(Boot, L" Pool Base: 0x%lx\n\r", pmm_pool_base);
SAFE_PRINT(Boot, L" Total Pages: %d (%d KB)\n\r",
p_total, (p_total * PAGE_SIZE) / 1024);
SAFE_PRINT(Boot, L" Used Pages: %d (%d KB)\n\r",
p_used, (p_used * PAGE_SIZE) / 1024);
SAFE_PRINT(Boot, L" Free Pages: %d (%d KB)\n\r",
p_free, (p_free * PAGE_SIZE) / 1024);
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Heap Allocator:\n\r");
SAFE_PRINT(Boot, L" Total: %d bytes\n\r", h_total);
SAFE_PRINT(Boot, L" Used: %d bytes\n\r", h_used);
SAFE_PRINT(Boot, L" Free: %d bytes\n\r", h_free);
SAFE_PRINT(Boot, L" Blocks: %d\n\r", h_blocks);
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L"Paging:\n\r");
SAFE_PRINT(Boot, L" CR3: 0x%lx\n\r", read_cr3());
SAFE_PRINT(Boot, L" Mode: 4-level (PML4)\n\r");
SAFE_PRINT(Boot, L"\n\r");
}

104
memory.h Normal file
View File

@@ -0,0 +1,104 @@
/*
* memory.h Memory management subsystem declarations.
*
* Three layers:
* PMM bitmap-based physical page-frame allocator (16 MB pool)
* Paging walks / creates 4-level x86-64 page tables
* Heap first-fit free-list allocator with block coalescing
*/
#ifndef MEMORY_H
#define MEMORY_H
#include "kernel_types.h"
#include "boot_info.h"
/* ================================================================
* Page-level constants
* ================================================================ */
#define PAGE_SIZE 4096
#define PAGE_SHIFT 12
#define PAGE_MASK (~(UINT64)(PAGE_SIZE - 1))
/* ================================================================
* Page-table entry flags (x86-64 4-level paging)
* ================================================================ */
#define PTE_PRESENT (1ULL << 0)
#define PTE_WRITABLE (1ULL << 1)
#define PTE_USER (1ULL << 2)
#define PTE_WRITETHROUGH (1ULL << 3)
#define PTE_NOCACHE (1ULL << 4)
#define PTE_ACCESSED (1ULL << 5)
#define PTE_DIRTY (1ULL << 6)
#define PTE_HUGE (1ULL << 7) /* 2 MB or 1 GB page */
#define PTE_GLOBAL (1ULL << 8)
#define PTE_NX (1ULL << 63) /* No-Execute */
#define PTE_ADDR_MASK 0x000FFFFFFFFFF000ULL
/* ================================================================
* Physical Memory Manager (PMM)
* ================================================================ */
/* Size of the PMM-managed pool (allocated from UEFI at boot). */
#define PMM_POOL_SIZE (16ULL * 1024 * 1024) /* 16 MB */
#define PMM_POOL_PAGES (PMM_POOL_SIZE / PAGE_SIZE)
void pmm_init(BootInfo *Boot);
UINT64 pmm_alloc_page(void);
void pmm_free_page(UINT64 phys_addr);
UINT64 pmm_alloc_pages(UINTN count);
void pmm_free_pages(UINT64 phys_addr, UINTN count);
UINTN pmm_get_free_pages(void);
UINTN pmm_get_total_pages(void);
/* ================================================================
* Paging
* ================================================================ */
void paging_init(BootInfo *Boot);
BOOLEAN paging_map_page(UINT64 virt, UINT64 phys, UINT64 flags);
void paging_unmap_page(UINT64 virt);
UINT64 paging_get_phys(UINT64 virt);
/* ================================================================
* Heap allocator
* ================================================================ */
#define HEAP_INITIAL_PAGES 64 /* 256 KB initial heap */
#define HEAP_BLOCK_MAGIC 0xDEADBEEFU /* corruption-detection canary */
#define HEAP_BLOCK_FREE 0
#define HEAP_BLOCK_USED 1
#define HEAP_ALIGN 16 /* minimum allocation alignment */
/*
* HeapBlock header placed immediately before each allocation.
* Blocks form a doubly-linked free-list.
*/
typedef struct HeapBlock {
UINT32 magic; /* must be HEAP_BLOCK_MAGIC */
UINT32 state; /* HEAP_BLOCK_FREE / _USED */
UINTN size; /* usable bytes (excludes hdr) */
struct HeapBlock *next;
struct HeapBlock *prev;
} HeapBlock;
void heap_init(BootInfo *Boot);
void *kmalloc(UINTN size);
void kfree(void *ptr);
void heap_get_stats(UINTN *total, UINTN *used, UINTN *free_mem,
UINTN *num_blocks);
/* ================================================================
* Top-level helpers
* ================================================================ */
/* Initialise all memory subsystems (PMM → paging → heap). */
void memory_init(BootInfo *Boot);
/* Print PMM, heap, and paging statistics to the console. */
void memory_print_stats(BootInfo *Boot);
#endif /* MEMORY_H */

View File

@@ -1,5 +1,16 @@
/*
* string_utils.c Wide-character (CHAR16) string helpers.
*
* Provides basic string operations used throughout the kernel,
* particularly by the command parser in commands.c.
*/
#include "string_utils.h"
/* ----------------------------------------------------------------
* Character classification / conversion
* ---------------------------------------------------------------- */
BOOLEAN is_space16(CHAR16 Ch)
{
return (Ch == L' ' || Ch == L'\t');
@@ -13,6 +24,10 @@ CHAR16 ascii_lower16(CHAR16 Ch)
return Ch;
}
/* ----------------------------------------------------------------
* String comparison
* ---------------------------------------------------------------- */
BOOLEAN ascii_streq_ci(const CHAR16 *A, const CHAR16 *B)
{
if (A == NULL || B == NULL) {
@@ -30,6 +45,10 @@ BOOLEAN ascii_streq_ci(const CHAR16 *A, const CHAR16 *B)
return (*A == L'\0' && *B == L'\0');
}
/* ----------------------------------------------------------------
* In-place trimming
* ---------------------------------------------------------------- */
void trim_spaces_inplace(CHAR16 *Cmd)
{
UINTN start = 0;
@@ -40,19 +59,23 @@ void trim_spaces_inplace(CHAR16 *Cmd)
return;
}
/* Find first non-space character */
while (Cmd[start] != L'\0' && is_space16(Cmd[start])) {
start++;
}
/* Find end of string */
end = start;
while (Cmd[end] != L'\0') {
end++;
}
/* Trim trailing spaces */
while (end > start && is_space16(Cmd[end - 1])) {
end--;
}
/* Shift trimmed content to the beginning */
while (i < (end - start)) {
Cmd[i] = Cmd[start + i];
i++;

View File

@@ -1,11 +1,25 @@
/*
* string_utils.h Wide-character (CHAR16) string helpers.
*
* Utility functions for the kernel's interactive shell: whitespace
* detection, case-insensitive comparison, and in-place trimming.
*/
#ifndef STRING_UTILS_H
#define STRING_UTILS_H
#include <efi.h>
#include "kernel_types.h"
/* Return TRUE if Ch is a space or horizontal tab. */
BOOLEAN is_space16(CHAR16 Ch);
/* Return the ASCII lower-case equivalent of Ch (A-Z only). */
CHAR16 ascii_lower16(CHAR16 Ch);
/* Case-insensitive equality test for two NUL-terminated CHAR16 strings. */
BOOLEAN ascii_streq_ci(const CHAR16 *A, const CHAR16 *B);
/* Strip leading and trailing whitespace from Cmd in place. */
void trim_spaces_inplace(CHAR16 *Cmd);
#endif
#endif /* STRING_UTILS_H */

438
task.c Normal file
View File

@@ -0,0 +1,438 @@
/*
* task.c Cooperative multitasking: PCB pool, scheduler, yield/exit.
*
* Task 0 is the always-present kernel core thread (uses the boot stack).
* Additional tasks are created with task_create(), which allocates a
* stack from the PMM and sets up a fake context-switch frame so that
* context_switch() can "return" into a trampoline that calls the real
* entry function.
*
* Scheduling is purely cooperative: tasks must call task_yield() to
* let others run. The scheduler uses round-robin selection among
* READY tasks.
*/
#include "task.h"
#include "memory.h"
/* Null-safe print helper used throughout the kernel. */
#define SAFE_PRINT(Boot, ...) \
do { \
if ((Boot) != NULL && (Boot)->print != NULL) { \
(Boot)->print(__VA_ARGS__); \
} \
} while (0)
/* ================================================================
* Module state
* ================================================================ */
static Task tasks[TASK_MAX]; /* PCB pool (static array) */
static Task *current_task = NULL;
static UINT32 next_pid = 0;
static BootInfo *task_boot = NULL;
static BOOLEAN task_ready = FALSE;
/* Forward declaration */
static void task_trampoline(void);
/* ----------------------------------------------------------------
* Helpers
* ---------------------------------------------------------------- */
/* Copy a wide string with a maximum length (including NUL). */
static void wstrcpy16(CHAR16 *dst, const CHAR16 *src, UINTN max)
{
UINTN i = 0;
while (i < max - 1 && src != NULL && src[i] != L'\0') {
dst[i] = src[i];
i++;
}
dst[i] = L'\0';
}
/* ----------------------------------------------------------------
* Initialisation make the current (kernel) thread task 0
* ---------------------------------------------------------------- */
/*
* Initialise the scheduler: clear all PCB slots and register the
* currently running kernel core thread as task 0.
*/
void task_init(BootInfo *Boot)
{
UINTN i;
task_boot = Boot;
/* Clear all PCB slots */
for (i = 0; i < TASK_MAX; i++) {
tasks[i].state = TASK_STATE_FREE;
tasks[i].pid = 0;
tasks[i].privilege = TASK_PRIV_USER;
tasks[i].saved_rsp = 0;
tasks[i].stack_base = 0;
tasks[i].stack_pages = 0;
tasks[i].entry = NULL;
tasks[i].arg = NULL;
tasks[i].switches = 0;
tasks[i].name[0] = L'\0';
}
/*
* Task 0 = the currently running kernel core thread.
* It already has a stack (the kernel's boot stack), so we don't
* allocate one. Its saved_rsp will be filled in during the
* first context_switch call in task_yield().
*/
tasks[0].pid = next_pid++;
tasks[0].state = TASK_STATE_RUNNING;
tasks[0].privilege = TASK_PRIV_KERNEL;
tasks[0].switches = 1;
wstrcpy16(tasks[0].name, L"core", TASK_NAME_LEN);
current_task = &tasks[0];
task_ready = TRUE;
SAFE_PRINT(Boot, L" Tasks: scheduler ready (max %d tasks)\n\r",
(UINTN)TASK_MAX);
}
/* ----------------------------------------------------------------
* Trampoline first code a new task executes after context_switch
* returns into it. It calls the real entry function and then
* performs a clean task_exit().
* ---------------------------------------------------------------- */
static void task_trampoline(void)
{
Task *t = task_current();
if (t != NULL && t->entry != NULL) {
t->entry(t->arg);
}
task_exit();
/* Should never reach here, but just in case: */
for (;;) {
__asm__ __volatile__("hlt");
}
}
/* ----------------------------------------------------------------
* Create a new task
* ---------------------------------------------------------------- */
Task *task_create_with_priv(const CHAR16 *name,
TaskEntryFn entry,
void *arg,
TaskPrivilege privilege)
{
Task *t = NULL;
UINTN i;
UINT64 stack_phys;
UINT64 *sp;
if (!task_ready || entry == NULL) {
return NULL;
}
/* Subsystem-level privilege enforcement: prevent privilege escalation. */
{
Task *caller = task_current();
if (caller != NULL && privilege > task_get_privilege(caller)) {
return NULL;
}
}
/* Find a free PCB slot */
for (i = 0; i < TASK_MAX; i++) {
if (tasks[i].state == TASK_STATE_FREE) {
t = &tasks[i];
break;
}
}
if (t == NULL) {
return NULL; /* out of slots */
}
/* Allocate stack pages from the physical memory manager */
stack_phys = pmm_alloc_pages(TASK_STACK_PAGES);
if (stack_phys == 0) {
return NULL; /* out of memory */
}
/* Fill in the PCB */
t->pid = next_pid++;
t->state = TASK_STATE_READY;
t->privilege = privilege;
t->entry = entry;
t->arg = arg;
t->switches = 0;
t->stack_base = stack_phys;
t->stack_pages = TASK_STACK_PAGES;
wstrcpy16(t->name, name != NULL ? name : L"unnamed", TASK_NAME_LEN);
/*
* Set up the initial stack frame so that context_switch() can
* "return" into task_trampoline().
*
* context_switch saves/restores (low → high on stack):
* flags, r15, r14, r13, r12, rbx, rbp (pushes)
* then `ret` pops the return address (→ trampoline)
*
* Above the return address we place a safety-net address
* (task_exit) so that if the trampoline or entry function does
* a bare `ret`, it lands in task_exit().
*/
sp = (UINT64 *)(stack_phys + TASK_STACK_SIZE);
/* Align stack top to 16 bytes */
sp = (UINT64 *)((UINT64)sp & ~0xFULL);
/* Safety-net return address for the trampoline */
*(--sp) = (UINT64)(UINTN)task_exit;
/* Return address for context_switch's `ret` → trampoline */
*(--sp) = (UINT64)(UINTN)task_trampoline;
/* Callee-saved registers all zero for fresh task */
*(--sp) = 0; /* rbp */
*(--sp) = 0; /* rbx */
*(--sp) = 0; /* r12 */
*(--sp) = 0; /* r13 */
*(--sp) = 0; /* r14 */
*(--sp) = 0; /* r15 */
/* RFLAGS interrupts enabled (IF = bit 9) */
*(--sp) = 0x202; /* flags */
t->saved_rsp = (UINT64)(UINTN)sp;
return t;
}
Task *task_create(const CHAR16 *name, TaskEntryFn entry, void *arg)
{
/* Inherit privilege from the calling task (kernel if no task context). */
Task *caller = task_current();
TaskPrivilege priv = (caller != NULL) ? task_get_privilege(caller)
: TASK_PRIV_KERNEL;
return task_create_with_priv(name, entry, arg, priv);
}
/* ----------------------------------------------------------------
* Schedule pick the next READY task (round-robin)
* ---------------------------------------------------------------- */
static Task *schedule_next(void)
{
UINTN start, idx, i;
if (current_task == NULL) {
return &tasks[0];
}
/* Find current task's index in the array */
start = (UINTN)(current_task - tasks);
/* Round-robin: scan from (current+1) wrapping around */
for (i = 1; i <= TASK_MAX; i++) {
idx = (start + i) % TASK_MAX;
if (tasks[idx].state == TASK_STATE_READY) {
return &tasks[idx];
}
}
/* No other ready task stay with current if still runnable */
if (current_task->state == TASK_STATE_RUNNING ||
current_task->state == TASK_STATE_READY) {
return current_task;
}
/* Fallback to task 0 (kernel / shell) */
return &tasks[0];
}
/* ----------------------------------------------------------------
* Yield voluntarily give up the CPU to the next ready task
* ---------------------------------------------------------------- */
void task_yield(void)
{
Task *prev, *next;
if (!task_ready) {
return;
}
prev = current_task;
next = schedule_next();
if (next == prev) {
return; /* nothing else to switch to */
}
/* Mark the previous task as READY (still runnable) */
if (prev->state == TASK_STATE_RUNNING) {
prev->state = TASK_STATE_READY;
}
next->state = TASK_STATE_RUNNING;
next->switches++;
current_task = next;
/*
* context_switch saves callee-saved regs + flags on prev's stack,
* stores prev's RSP into prev->saved_rsp, loads next->saved_rsp
* into RSP, restores regs + flags, and `ret`s into next's code.
*/
context_switch(&prev->saved_rsp, next->saved_rsp);
}
/* ----------------------------------------------------------------
* Exit terminate the current task and switch away
* ---------------------------------------------------------------- */
void task_exit(void)
{
Task *prev, *next;
if (!task_ready) {
return;
}
prev = current_task;
prev->state = TASK_STATE_TERMINATED;
/* Free the stack memory back to the PMM */
if (prev->stack_base != 0 && prev->stack_pages != 0) {
pmm_free_pages(prev->stack_base, prev->stack_pages);
prev->stack_base = 0;
prev->stack_pages = 0;
}
/* Mark the PCB slot as free for reuse */
prev->state = TASK_STATE_FREE;
next = schedule_next();
if (next == prev) {
/* Shouldn't happen if task 0 (kernel) is always alive */
next = &tasks[0];
}
next->state = TASK_STATE_RUNNING;
next->switches++;
current_task = next;
/* One-way switch: we never return to the exited task */
context_switch(&prev->saved_rsp, next->saved_rsp);
/* Should never reach here */
for (;;) {
__asm__ __volatile__("hlt");
}
}
/* ----------------------------------------------------------------
* Accessors
* ---------------------------------------------------------------- */
Task *task_current(void)
{
return current_task;
}
TaskPrivilege task_get_privilege(Task *t)
{
if (t == NULL) {
return TASK_PRIV_KERNEL;
}
return t->privilege;
}
void task_set_privilege(Task *t, TaskPrivilege privilege)
{
if (t == NULL) {
return;
}
t->privilege = privilege;
}
UINTN task_count(void)
{
UINTN i, count = 0;
for (i = 0; i < TASK_MAX; i++) {
if (tasks[i].state != TASK_STATE_FREE) {
count++;
}
}
return count;
}
/* ----------------------------------------------------------------
* Wait for another task to finish
* ---------------------------------------------------------------- */
void task_wait(Task *t)
{
if (!task_ready || t == NULL) {
return;
}
/*
* Busy-wait cooperatively until the target task's PCB slot has
* been recycled back to FREE by task_exit().
*/
while (t->state != TASK_STATE_FREE) {
task_yield();
}
}
/* ----------------------------------------------------------------
* Print task list (implements the `ps` command)
* ---------------------------------------------------------------- */
static const CHAR16 *state_str(TaskState s)
{
switch (s) {
case TASK_STATE_FREE: return L"FREE";
case TASK_STATE_READY: return L"READY";
case TASK_STATE_RUNNING: return L"RUNNING";
case TASK_STATE_TERMINATED: return L"ENDED";
default: return L"???";
}
}
void task_print_list(BootInfo *Boot)
{
UINTN i;
Task *caller;
/* Subsystem-level privilege enforcement: task list requires DRIVER. */
caller = task_current();
if (caller != NULL && task_get_privilege(caller) < TASK_PRIV_DRIVER) {
SAFE_PRINT(Boot, L"Permission denied: task list requires driver privilege.\n\r");
return;
}
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L" PID STATE PRIV SWITCHES NAME\n\r");
SAFE_PRINT(Boot, L" --- ---------- ---- -------- ----\n\r");
for (i = 0; i < TASK_MAX; i++) {
if (tasks[i].state == TASK_STATE_FREE) {
continue;
}
SAFE_PRINT(Boot, L" %3d %-10s %4d %8d %s\n\r",
tasks[i].pid,
state_str(tasks[i].state),
(INT32)tasks[i].privilege,
tasks[i].switches,
tasks[i].name);
}
SAFE_PRINT(Boot, L"\n\r");
SAFE_PRINT(Boot, L" Active tasks: %d / %d\n\r",
task_count(), (UINTN)TASK_MAX);
SAFE_PRINT(Boot, L"\n\r");
}

111
task.h Normal file
View File

@@ -0,0 +1,111 @@
/*
* task.h Cooperative multitasking interface.
*
* Provides process control blocks (PCBs), a round-robin scheduler,
* and voluntary yield/exit primitives. Context switching is performed
* by the assembly routine in context_switch.S.
*/
#ifndef TASK_H
#define TASK_H
#include "kernel_types.h"
#include "boot_info.h"
/* ================================================================
* Configuration
* ================================================================ */
#define TASK_MAX 32 /* max concurrent tasks */
#define TASK_STACK_PAGES 8 /* 32 KB stack per task */
#define TASK_STACK_SIZE (TASK_STACK_PAGES * 4096)
#define TASK_NAME_LEN 32 /* max name length (CHAR16) */
/* ================================================================
* Task states
* ================================================================ */
typedef enum {
TASK_STATE_FREE = 0, /* PCB slot is unused */
TASK_STATE_READY, /* runnable, waiting to be scheduled */
TASK_STATE_RUNNING, /* currently executing on the CPU */
TASK_STATE_TERMINATED /* finished; slot will be recycled */
} TaskState;
/*
* Logical privilege level for a task.
*
* Higher numeric values are more privileged:
* USER (0) least privileged
* DRIVER (1) mid-level, can talk to hardware-facing subsystems
* KERNEL (2) most privileged, core kernel and management threads
*
* All tasks still execute in ring 0 today; this is a software
* hierarchy used for access control decisions and future paging work.
*/
typedef enum {
TASK_PRIV_USER = 0,
TASK_PRIV_DRIVER = 1,
TASK_PRIV_KERNEL = 2,
} TaskPrivilege;
/* ================================================================
* Task entry function
* ================================================================ */
/* Signature for the function a new task begins executing. */
typedef void (*TaskEntryFn)(void *arg);
/* ================================================================
* Process Control Block (PCB)
* ================================================================ */
typedef struct Task {
UINT32 pid; /* unique process ID */
TaskState state; /* current lifecycle state */
TaskPrivilege privilege; /* logical privilege level */
CHAR16 name[TASK_NAME_LEN]; /* human-readable label */
/* Context switch state */
UINT64 saved_rsp; /* RSP saved by context_switch() */
UINT64 stack_base; /* base phys addr of stack alloc */
UINTN stack_pages; /* stack size in pages */
/* Entry point */
TaskEntryFn entry; /* function pointer */
void *arg; /* argument passed to entry() */
/* Scheduling metadata */
UINTN switches; /* number of times scheduled */
} Task;
/* ================================================================
* Public API
* ================================================================ */
void task_init(BootInfo *Boot); /* initialise scheduler */
/* Spawn a new task with a specific privilege level. */
Task *task_create_with_priv(const CHAR16 *name,
TaskEntryFn entry,
void *arg,
TaskPrivilege privilege);
/* Backwards-compatible helper: creates a kernel-privileged task. */
Task *task_create(const CHAR16 *name,
TaskEntryFn entry, void *arg);
void task_yield(void); /* voluntarily give up the CPU */
void task_exit(void); /* terminate the current task */
Task *task_current(void); /* return the running task's PCB */
TaskPrivilege task_get_privilege(Task *t);
void task_set_privilege(Task *t, TaskPrivilege privilege);
UINTN task_count(void); /* number of non-FREE tasks */
void task_print_list(BootInfo *Boot); /* print task table (for `ps`) */
/* Block the current task until the target task has finished. */
void task_wait(Task *t);
/* Assembly context switch (defined in context_switch.S). */
extern void context_switch(UINT64 *old_rsp, UINT64 new_rsp);
#endif /* TASK_H */