Add complete user-mode process support: - TSS (tss.c/h): Task State Segment for Ring 3->0 transitions, installed as GDT entry 5 (selector 0x28). ESP0 updated per-process for kernel stack switching. - Process management (process.c/h): Process table with up to 64 processes. process_create() clones kernel page directory, maps user code at 0x08048000 and user stack at 0xBFFFF000, copies flat binary code. Round-robin scheduler via schedule_tick() modifies the interrupt frame in-place for zero-copy context switching. - System calls (syscall.c/h): INT 0x80 dispatcher with 8 syscalls: SYS_EXIT, SYS_WRITE (to debug port + VGA), SYS_READ, SYS_FORK, SYS_GETPID, SYS_YIELD, SYS_WAITPID, SYS_EXEC. IDT gate at 0x80 uses DPL=3 (flags 0xEE) so user code can invoke it. - Assembly stubs (interrupts.S): isr128 for INT 0x80, tss_flush for loading the Task Register, enter_usermode for initial iret to Ring 3. - Paging extensions (paging.c/h): paging_clone_directory() to create per-process page directories, paging_map_page_in() for mapping into non-active directories, paging_switch_directory() for CR3 switching. - GDT expanded from 5 to 6 entries to accommodate TSS descriptor. gdt_set_gate() exposed in header for TSS initialization. - ISR handler routes timer IRQ (32) to scheduler and INT 0x80 to syscall dispatcher. Exception handler now prints EIP/CS/ERR for debugging. - Kernel boots a test user program that writes 'Hello from Ring 3!' via SYS_WRITE and exits with code 42 via SYS_EXIT. Verified working in QEMU. Context switching approach: Timer/syscall interrupts save all registers via the ISR stub. schedule_tick() copies saved_regs between PCBs and overwrites the interrupt frame, so the existing iret restores the next process's state without separate switch assembly.
110 lines
4.1 KiB
Markdown
110 lines
4.1 KiB
Markdown
# Process Subsystem
|
||
|
||
## Overview
|
||
|
||
The process subsystem enables user-mode (Ring 3) process execution on ClaudeOS.
|
||
It provides process creation, context switching via the timer interrupt, and
|
||
system calls via `INT 0x80`.
|
||
|
||
## Architecture
|
||
|
||
### Ring Transition
|
||
|
||
x86 protected mode uses privilege rings 0–3. The kernel runs in Ring 0 (full
|
||
hardware access) and user processes run in Ring 3 (restricted). The GDT
|
||
defines segment descriptors for both:
|
||
|
||
| GDT Entry | Selector | Purpose | DPL |
|
||
|-----------|----------|-----------------|-----|
|
||
| 0 | 0x00 | Null | – |
|
||
| 1 | 0x08 | Kernel Code | 0 |
|
||
| 2 | 0x10 | Kernel Data | 0 |
|
||
| 3 | 0x18 | User Code | 3 |
|
||
| 4 | 0x20 | User Data | 3 |
|
||
| 5 | 0x28 | TSS | 0 |
|
||
|
||
User-mode selectors include RPL=3: code = 0x1B, data = 0x23.
|
||
|
||
### Task State Segment (TSS)
|
||
|
||
The TSS (`tss.c`) stores the kernel stack pointer (SS0:ESP0) used when the CPU
|
||
transitions from Ring 3 to Ring 0 on an interrupt. Before running each process,
|
||
the scheduler updates TSS.ESP0 to that process's kernel stack top.
|
||
|
||
### Memory Layout
|
||
|
||
Each process gets its own page directory, cloned from the kernel's:
|
||
|
||
```
|
||
0x00000000 – 0x07FFFFFF : Identity-mapped (kernel/device access)
|
||
0x08048000 – ... : User code (loaded from binary image)
|
||
0xBFFF7000 – 0xBFFFF000 : User stack (2 pages, grows downward)
|
||
0xD0000000 – 0xF0000000 : Kernel heap (shared across all processes)
|
||
```
|
||
|
||
### Process Control Block
|
||
|
||
```c
|
||
typedef struct process {
|
||
uint32_t pid;
|
||
process_state_t state; // UNUSED, READY, RUNNING, BLOCKED, ZOMBIE
|
||
registers_t saved_regs; // Full interrupt frame
|
||
uint32_t kernel_stack; // Base of per-process kernel stack
|
||
uint32_t kernel_stack_top; // TSS ESP0 value
|
||
uint32_t page_directory; // Physical address of page directory
|
||
uint32_t user_stack; // User stack virtual address
|
||
uint32_t entry_point; // User code entry point
|
||
int32_t exit_code; // Set on exit
|
||
uint32_t parent_pid;
|
||
char name[32];
|
||
} process_t;
|
||
```
|
||
|
||
## Context Switching
|
||
|
||
Context switching uses the interrupt frame directly:
|
||
|
||
1. Timer IRQ (or `INT 0x80` for SYS_YIELD) fires
|
||
2. CPU pushes SS/ESP/EFLAGS/CS/EIP onto the process's kernel stack
|
||
3. ISR stub pushes the rest (pusha + DS) forming a `registers_t`
|
||
4. `schedule_tick()` is called with a pointer to these registers
|
||
5. Current process's registers are saved into its PCB
|
||
6. Next READY process's saved registers are written over the interrupt frame
|
||
7. TSS.ESP0 is updated, CR3 is switched to the new page directory
|
||
8. ISR stub restores the (now different) registers and `iret` enters the new
|
||
process in user mode
|
||
|
||
This avoids separate context-switch assembly — the existing ISR stub handles
|
||
everything.
|
||
|
||
## System Calls
|
||
|
||
System calls use `INT 0x80` with the call number in EAX:
|
||
|
||
| Number | Name | Arguments |
|
||
|--------|-------------|------------------------------|
|
||
| 0 | SYS_EXIT | EBX = exit code |
|
||
| 1 | SYS_WRITE | EBX = fd, ECX = buf, EDX = len |
|
||
| 2 | SYS_READ | (not implemented) |
|
||
| 3 | SYS_FORK | (returns child PID/0) |
|
||
| 4 | SYS_GETPID | (returns PID in EAX) |
|
||
| 5 | SYS_YIELD | (voluntary preemption) |
|
||
| 6 | SYS_WAITPID | EBX = child PID |
|
||
| 7 | SYS_EXEC | (not implemented) |
|
||
|
||
The INT 0x80 IDT gate has DPL=3 (flags 0xEE) so user-mode code can invoke it.
|
||
|
||
## Initial Process Entry
|
||
|
||
`process_run_first()` performs the initial transition to user mode using an
|
||
`iret` instruction that sets up Ring 3 segment selectors, the user stack
|
||
pointer, and the entry point. This is a one-way transition — the function
|
||
does not return.
|
||
|
||
## Files
|
||
|
||
- `tss.h` / `tss.c` — TSS structure and initialization
|
||
- `process.h` / `process.c` — Process table, creation, scheduling, exit, fork
|
||
- `syscall.h` / `syscall.c` — System call dispatch and handlers
|
||
- `interrupts.S` — Assembly stubs: `isr128` (INT 0x80), `tss_flush`, `enter_usermode`
|