298 lines
12 KiB
Markdown
298 lines
12 KiB
Markdown
## 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));
|
||
...
|
||
terminal_task = task_create(L"starling-term", starling_terminal_task, ctx);
|
||
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`, 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 read–eval–print 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;
|
||
|
||
SAFE_PRINT(Boot, L"\n\r[Starling Terminal depth %d] ready.\n\r\n\r", depth);
|
||
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);
|
||
|
||
/* 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)` in `commands.c`. 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.
|
||
- 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 0–31 with kernel stubs.
|
||
- Central `isr_handler` that prints diagnostics and halts on unrecoverable faults.
|
||
- **Commands and shell** (`commands.c` + `commands.h`):
|
||
- Command registry and help/man system.
|
||
- System control commands (`shutdown`, `about`, `mem`, `ps`).
|
||
- Test commands (`memtest`, `tasktest`, `spawn`) that exercise memory and scheduler subsystems in isolation.
|
||
|
||
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.
|
||
|