Compare commits
4 Commits
d17daf7aac
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 08cb1db571 | |||
| de161801c4 | |||
| 9b1a70e3a5 | |||
| 7ecf26cbd9 |
15
README.md
15
README.md
@@ -104,7 +104,7 @@ The loader expects `kernel.elf` at the root of the EFI partition (next to the `E
|
||||
|
||||
### Shell Commands
|
||||
|
||||
Once the OS boots, an interactive prompt (`->`) is displayed. The following commands are available:
|
||||
Once the OS boots, the Starling Terminal displays an interactive prompt (`starling>`). The following built-in commands are available:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
@@ -115,10 +115,17 @@ Once the OS boots, an interactive prompt (`->`) is displayed. The following comm
|
||||
| `about` | Display system information |
|
||||
| `mem` | Display memory statistics (PMM, heap, paging) |
|
||||
| `ps` | List all active tasks with PID, state, and name |
|
||||
| `spawn [name]` | Create a demo background task |
|
||||
| `memtest` | Run memory allocation/deallocation tests |
|
||||
| `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
|
||||
|
||||
```
|
||||
@@ -186,7 +193,7 @@ UEFI Firmware
|
||||
├─ idt_init() install exception handlers
|
||||
├─ memory_init() PMM → paging → heap
|
||||
├─ task_init() scheduler + task 0
|
||||
└─ shell loop read-eval-print with cooperative yield
|
||||
└─ Starling Terminal task interactive read-eval-print loop
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
177
commands.c
177
commands.c
@@ -39,6 +39,33 @@ 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
|
||||
@@ -49,63 +76,91 @@ 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",
|
||||
L"Usage: memtest\n\r Allocates and frees heap and page memory to verify\n\r the memory manager is working correctly.",
|
||||
L"Usage: memtest\n\r"
|
||||
L" Run memory tests in four phases:\n\r"
|
||||
L" 1) Heap allocation of 8 blocks (16–4096 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 Spawns several concurrent tasks that run cooperatively\n\r and report their progress, demonstrating context switching.",
|
||||
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
|
||||
},
|
||||
{NULL, NULL, NULL, NULL} /* sentinel */
|
||||
{
|
||||
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 */
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
@@ -114,10 +169,19 @@ static Command commands[] = {
|
||||
|
||||
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;
|
||||
@@ -275,8 +339,16 @@ static void cmd_memtest(BootInfo *Boot, CHAR16 *Args)
|
||||
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");
|
||||
@@ -399,6 +471,40 @@ static void cmd_tasktest(BootInfo *Boot, CHAR16 *Args)
|
||||
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
|
||||
* ================================================================ */
|
||||
@@ -448,22 +554,29 @@ void show_help(BootInfo *Boot)
|
||||
|
||||
/*
|
||||
* Parse a line of user input into command + arguments and dispatch
|
||||
* to the matching handler. Unknown commands print an error.
|
||||
* 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.
|
||||
*/
|
||||
void execute_command(BootInfo *Boot, CHAR16 *Input)
|
||||
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 input into command and argument strings */
|
||||
@@ -489,12 +602,60 @@ void execute_command(BootInfo *Boot, CHAR16 *Input)
|
||||
/* 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;
|
||||
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;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------
|
||||
* 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);
|
||||
}
|
||||
|
||||
11
commands.h
11
commands.h
@@ -9,6 +9,7 @@
|
||||
#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);
|
||||
@@ -23,11 +24,17 @@ typedef struct {
|
||||
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;
|
||||
|
||||
/* Parse and dispatch a line of user input. */
|
||||
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);
|
||||
|
||||
528
docs/commands-and-terminal.md
Normal file
528
docs/commands-and-terminal.md
Normal 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 read–eval–print 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.
|
||||
|
||||
269
docs/interrupts-and-exceptions.md
Normal file
269
docs/interrupts-and-exceptions.md
Normal 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 0–31** 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 32–47):
|
||||
- Send an **End Of Interrupt** (EOI) to the PIC.
|
||||
- Return to the interrupted context.
|
||||
- For **CPU exceptions** (vectors 0–31):
|
||||
- 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 0–31, 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.
|
||||
|
||||
638
docs/memory-and-allocation.md
Normal file
638
docs/memory-and-allocation.md
Normal 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
308
docs/overview.md
Normal 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 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;
|
||||
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 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 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
508
docs/tasks-and-scheduler.md
Normal 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).
|
||||
|
||||
231
kernel.c
231
kernel.c
@@ -1,12 +1,13 @@
|
||||
/*
|
||||
* kernel.c – Kernel entry point and interactive shell loop.
|
||||
* 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 enters an interactive read-eval-print
|
||||
* 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 waiting for keyboard input, the shell yields to the cooperative
|
||||
* While the terminal waits for keyboard input it yields to the cooperative
|
||||
* scheduler so that background tasks can make progress.
|
||||
*/
|
||||
|
||||
@@ -16,6 +17,7 @@
|
||||
#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, ...) \
|
||||
@@ -32,15 +34,151 @@
|
||||
#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)
|
||||
{
|
||||
KeyEvent Key;
|
||||
KSTATUS Status;
|
||||
UINTN read_errors = 0;
|
||||
Task *terminal_task = NULL;
|
||||
StarlingContext *ctx = NULL;
|
||||
|
||||
if (Boot == NULL) {
|
||||
return;
|
||||
@@ -84,61 +222,46 @@ void kmain(BootInfo *Boot)
|
||||
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" - 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");
|
||||
|
||||
/* ---- Interactive shell loop ---- */
|
||||
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) {
|
||||
/* 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");
|
||||
execute_command(Boot, line);
|
||||
|
||||
/* Reset for next command */
|
||||
len = 0;
|
||||
SAFE_PRINT(Boot, L"-> ");
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ typedef uint16_t UINT16;
|
||||
typedef uint32_t UINT32;
|
||||
typedef uint64_t UINT64;
|
||||
|
||||
typedef int32_t INT32;
|
||||
|
||||
typedef size_t UINTN;
|
||||
|
||||
#ifndef BOOLEAN
|
||||
|
||||
9
memory.c
9
memory.c
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
#include "memory.h"
|
||||
#include "task.h"
|
||||
|
||||
/* Null-safe print helper used throughout the kernel. */
|
||||
#define SAFE_PRINT(Boot, ...) \
|
||||
@@ -526,6 +527,14 @@ 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();
|
||||
|
||||
83
task.c
83
task.c
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* task.c – Cooperative multitasking: PCB pool, scheduler, yield/exit.
|
||||
*
|
||||
* Task 0 is the kernel/shell thread (uses the boot stack).
|
||||
* 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
|
||||
@@ -57,7 +57,7 @@ static void wstrcpy16(CHAR16 *dst, const CHAR16 *src, UINTN max)
|
||||
|
||||
/*
|
||||
* Initialise the scheduler: clear all PCB slots and register the
|
||||
* currently running kernel thread as task 0.
|
||||
* currently running kernel core thread as task 0.
|
||||
*/
|
||||
void task_init(BootInfo *Boot)
|
||||
{
|
||||
@@ -69,6 +69,7 @@ void task_init(BootInfo *Boot)
|
||||
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;
|
||||
@@ -79,15 +80,16 @@ void task_init(BootInfo *Boot)
|
||||
}
|
||||
|
||||
/*
|
||||
* Task 0 = the currently running kernel thread (the shell).
|
||||
* 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"kernel", TASK_NAME_LEN);
|
||||
wstrcpy16(tasks[0].name, L"core", TASK_NAME_LEN);
|
||||
|
||||
current_task = &tasks[0];
|
||||
task_ready = TRUE;
|
||||
@@ -119,7 +121,10 @@ static void task_trampoline(void)
|
||||
* Create a new task
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
Task *task_create(const CHAR16 *name, TaskEntryFn entry, void *arg)
|
||||
Task *task_create_with_priv(const CHAR16 *name,
|
||||
TaskEntryFn entry,
|
||||
void *arg,
|
||||
TaskPrivilege privilege)
|
||||
{
|
||||
Task *t = NULL;
|
||||
UINTN i;
|
||||
@@ -130,6 +135,14 @@ Task *task_create(const CHAR16 *name, TaskEntryFn entry, void *arg)
|
||||
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) {
|
||||
@@ -150,6 +163,7 @@ Task *task_create(const CHAR16 *name, TaskEntryFn entry, void *arg)
|
||||
/* 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;
|
||||
@@ -196,6 +210,15 @@ Task *task_create(const CHAR16 *name, TaskEntryFn entry, void *arg)
|
||||
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)
|
||||
* ---------------------------------------------------------------- */
|
||||
@@ -318,6 +341,22 @@ 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;
|
||||
@@ -329,6 +368,25 @@ UINTN task_count(void)
|
||||
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)
|
||||
* ---------------------------------------------------------------- */
|
||||
@@ -347,19 +405,28 @@ static const CHAR16 *state_str(TaskState s)
|
||||
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 SWITCHES NAME\n\r");
|
||||
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 %8d %s\n\r",
|
||||
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);
|
||||
}
|
||||
|
||||
33
task.h
33
task.h
@@ -32,6 +32,23 @@ typedef enum {
|
||||
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
|
||||
* ================================================================ */
|
||||
@@ -46,6 +63,7 @@ typedef void (*TaskEntryFn)(void *arg);
|
||||
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 */
|
||||
@@ -66,14 +84,27 @@ typedef struct Task {
|
||||
* ================================================================ */
|
||||
|
||||
void task_init(BootInfo *Boot); /* initialise scheduler */
|
||||
Task *task_create(const CHAR16 *name, /* spawn a new task */
|
||||
|
||||
/* 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user