rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 00:27:17 -0700 |
|---|---|---|
| commit | 176e770e4a2ec63a4e05a273a139907cc241fb0d | |
| parent | a0fc1ab827ad7844fa38a7380fb140a1894ab230 | |
| tree | browse files at this commit | |
User mode (ring 3), syscalls, per-process address spaces, ELF loader, VFS + ramfs - GDT: user code/data segments in sysret order; TSS.rsp0 updated per thread - syscall/sysret entry (STAR/LSTAR/SFMASK, EFER.SCE) with kernel stack switch; Linux-numbered read/write/open/close/sched_yield/getpid/exit, -errno results, user pointer validation - per-process L4 table (kernel entries copied, user region = L4 slot 1), CR3 switched by the scheduler; process = thread + address space + fd table - user faults (page fault, GPF, invalid opcode) kill the process, not the kernel - minimal ELF64 loader; user programs (hello, evil, spin, echo) built by build.rs - VFS: Inode/FileSystem/File traits, mount table, ramfs on / and /tmp; tty line discipline for read(0) - tests: user_mode, vfs; screenshot.sh drives the echo program; README
| M | README.md | +305 -53 |
| M | boot.png | +0 -0 |
| A | build.rs | +33 -0 |
| M | rust-toolchain.toml | +2 -0 |
| M | scripts/screenshot.sh | +10 -6 |
| A | src/elf.rs | +163 -0 |
| A | src/errno.rs | +17 -0 |
| A | src/fs/file.rs | +49 -0 |
| A | src/fs/mod.rs | +162 -0 |
| A | src/fs/ramfs.rs | +98 -0 |
| M | src/gdt.rs | +75 -37 |
| M | src/interrupts.rs | +40 -1 |
| M | src/lib.rs | +17 -2 |
| M | src/main.rs | +26 -2 |
| M | src/memory.rs | +162 -3 |
| A | src/process.rs | +186 -0 |
| A | src/syscall.rs | +204 -0 |
| M | src/task/keyboard.rs | +5 -12 |
| M | src/thread.rs | +77 -11 |
| A | src/tty.rs | +95 -0 |
| A | tests/user_mode.rs | +95 -0 |
| A | tests/vfs.rs | +85 -0 |
| A | user/echo.rs | +23 -0 |
| A | user/evil.rs | +17 -0 |
| A | user/hello.rs | +57 -0 |
| A | user/link.ld | +23 -0 |
| A | user/spin.rs | +20 -0 |
| A | user/ulib.rs | +105 -0 |
28 files changed, 2151 insertions, 127 deletions
@@ -10,11 +10,16 @@ kernel heap (`Box`, `Vec`, `Rc` work) backed by a fixed-size-block allocator. It multitasks two ways: an **async/await executor** (cooperative tasks; the keyboard is one of them) and **preemptive kernel threads** switched by the timer interrupt (threads A/B/C count on the top line while you type).+It runs **user programs in ring 3** (small Rust ELF binaries) as processes+with their own address spaces, which talk to the kernel via `syscall`+(`read`, `write`, `open`, `close`, `getpid`, `sched_yield`, `exit`) and read+files from an in-memory filesystem behind a Linux-style **VFS**. It has a `cargo test` suite that runs inside QEMU. Most code follows the blog_os sources (MIT / Apache-2.0) closely, with extra comments; additions are the serial mirroring, uptime clock, demo features,-scripts, and the preemptive threads (`src/thread.rs`, not in the blog series).+scripts, and everything past chapter 12: preemptive threads, user mode,+syscalls, ELF loading and the VFS.  @@ -30,7 +35,8 @@ scripts, and the preemptive threads (`src/thread.rs`, not in the blog series). | QEMU (`qemu-system-x86`, `qemu-system-gui`) | 10.0.13 | the emulator we boot in (`-gui` adds the GTK/SDL window) | `rust-toolchain.toml` pins the project to nightly with the two components, so-`cargo` inside this directory picks them up automatically.+`cargo` inside this directory picks them up automatically. It also installs+the built-in `x86_64-unknown-none` target, used to compile the user programs. Crates: `bootloader` 0.9.35 (BIOS bootloader used by the 2nd edition), `volatile` 0.2, `spin` 0.5, `x86_64` 0.15, `uart_16550` 0.3, `lazy_static` 1.5,@@ -41,7 +47,7 @@ Fresh setup on another machine: ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # rustup-rustup toolchain install nightly -c rust-src -c llvm-tools-preview+rustup toolchain install nightly -c rust-src -c llvm-tools-preview -t x86_64-unknown-none cargo install bootimage sudo apt install qemu-system-x86 qemu-system-gui ```@@ -55,7 +61,8 @@ cargo run # build + boot in a QEMU window; serial outp **Trying the keyboard:** with `cargo run`, click into the QEMU window and type; characters appear on the bottom line of the screen *and* in your terminal (via-serial). Shift works; Enter starts a new line; arrows/F-keys show as e.g.+serial). Press Enter and the `echo` user program reads the line with the+`read` syscall and prints `[echo] you typed: ...` (type `exit` to stop it). Shift works; Enter starts a new line; arrows/F-keys show as e.g. `<ArrowUp>`; Backspace erases the previous character (on the current line). To get the mouse/keyboard back from QEMU press Ctrl+Alt+G; close the window or press Ctrl+C in the terminal to quit. (Over SSH without X, use `cargo run -- -display curses` to see the VGA screen right in your terminal;@@ -72,9 +79,10 @@ cargo run --features headless,stack-overflow -- -display none # same as the sec # (QEMU's exit status is 33 = success; cargo just reports it as non-zero) cargo run -- -display none # headless but kernel keeps running; Ctrl+C to stop ./scripts/screenshot.sh [out.png] [text] [expected]- # boot, "type" text via QEMU monitor `sendkey` ("<" = Backspace;- # default "Hello, kernex<l!"), check serial shows the expected- # result ("Hello, kernel!"), save the VGA screen to boot.png+ # boot, "type" text via QEMU monitor `sendkey` ("<" = Backspace,+ # "|" = Enter; default "Hello, kernex<l!|"), check serial shows the+ # expected result ("[echo] you typed: Hello, kernel!"), save the+ # VGA screen to boot.png; exits 1 if the text is missing ``` Cargo features (in `Cargo.toml`):@@ -101,38 +109,59 @@ Survived the breakpoint, still running. [heap] Rc count after clone = 2 [heap] Rc count after drop = 1 [thread] spawned threads A, B, C (status on the top line)-Type something! (keyboard echo, Backspace works)+.[thread A] count 1+.[thread B] count 1+..[vfs] mounted ramfs at /+[vfs] mounted ramfs at /tmp+[process] started pid 1 (hello), entry 0x8000400180+[process] started pid 2 (evil), entry 0x80004000f0+[process] started pid 3 (spin), entry 0x80004000e0+[process] started pid 4 (echo), entry 0x8000400180+Type a line + Enter: the echo program (ring 3) reads it [async] async number: 42 [async] example task step 1 (after yield) [async] example task step 2 (after yield) [async] example task step 3 (after yield)-.[thread A] count 1-.[thread B] count 1-.......[thread A] count 2+....Hello from user mode! I'm pid 1.+[hello] /hello.txt says: Hello from the ramfs! This is /hello.txt.+[hello] wrote and read back /tmp/note.txt+[hello] all syscall checks passed, exiting with 0+[process] pid 1 (hello) exited with code 0+[evil] pid 2: reading kernel memory at 0x200000...+[kernel] user page fault: address Ok(VirtAddr(0x200000)), error PageFaultErrorCode(PROTECTION_VIOLATION | USER_MODE)+[kernel] page fault in user mode at rip 0x800040017e+[process] pid 2 (evil) killed: page fault+[spin] pid 3: spinning without syscalls...+.[echo] pid 4: type a line and press Enter+..[thread A] count 2 .[thread B] count 2-.....[thread C] busy loop round 1 (count = 2000000)-.. 18 ticks-[thread A] count 3+.....[thread A] count 3+. 18 ticks+.....[thread A] count 4 .[thread B] count 3-.......[thread A] count 4-.[thread B] count 4-.....[thread C] busy loop round 2 (count = 4000000)-..[thread A] count 5-.[thread B] count 5-. 36 ticks-............[thread C] busy loop round 3 (count = 6000000)-...... 54 ticks-..........[thread C] busy loop round 4 (count = 8000000)-........ 72 ticks-....[thread C] busy loop round 5 (count = 10000000)-[thread C] done, exiting-.............. 90 ticks+.[thread C] busy loop round 1 (count = 2000000)+.[spin] round 1 done (count = 30000000)+...[thread A] count 5+....... 36 ticks+[thread B] count 4+..[spin] round 2 done (count = 60000000)+..........[thread B] count 5+.[thread C] busy loop round 2 (count = 4000000)+.[spin] round 3 done (count = 90000000)+[process] pid 3 (spin) exited with code 0+.... 54 ticks+.................. 72 ticks+..[thread C] busy loop round 3 (count = 6000000)+...............[thread C] busy loop round 4 (count = 8000000)+. 90 ticks [ok] timer works, exiting ``` Each `.` is a timer tick. Threads A and B only print their first 5 counts to serial (so the keyboard echo in your terminal stays readable); after that they-keep counting on the VGA top line.+keep counting on the VGA top line. (Exact interleaving and dot counts vary a+little from run to run -- that's preemption at work.) Spin's rounds interleave+with threads A/B/C: that's preemption of ring-3 code by the timer. ## Tests @@ -163,6 +192,8 @@ heap_allocation::simple_allocation... [ok] | `tests/should_panic.rs` | a failing `assert_eq!` reaches the panic handler (`harness = false`) | | `tests/stack_overflow.rs` | stack overflow lands in the double-fault handler on the IST stack (`harness = false`) | | `tests/executor.rs` | `SimpleExecutor` runs an async fn to completion; the waker-based `Executor` interleaves two tasks that `yield_now().await` (a1 b1 a2 b2 a3 b3) |+| `tests/user_mode.rs` | `hello` exits 0 (all its syscall checks pass) and its `/tmp/note.txt` is visible to the kernel; `evil` is `Killed("page fault")`; `spin` finishes while the test thread keeps running (preemption of ring 3); the ELF loader rejects garbage/truncated files; user pointers to kernel/unmapped/overflowing ranges give `EFAULT` |+| `tests/vfs.rs` | read `/hello.txt` to EOF, path normalisation, `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, a third mount wins by longest prefix, sparse writes zero-fill, directory listing | | `tests/threads.rs` | two busy-looping threads that never yield both make progress while the main thread busy-waits too (only possible with preemption); a thread can exit and the others keep running | How it works: `#![feature(custom_test_frameworks)]` collects every `#[test_case]`@@ -183,11 +214,13 @@ x86_64-rust_kernel.json custom target: no OS, no red zone, soft-float, abort o Cargo.toml deps, features, test settings, QEMU run/test args src/lib.rs the kernel library: module list, init(), kprintln!, test framework, exit_qemu src/main.rs the bootable binary: entry_point!(kernel_main), paging + heap demos, panic handler-src/gdt.rs GDT + TSS with a separate IST stack for the double-fault handler-src/interrupts.rs IDT; breakpoint / page fault / double fault handlers; 8259 PIC;+src/gdt.rs GDT (kernel + user segments) + TSS (double-fault IST stack, rsp0 per thread)+src/interrupts.rs IDT; breakpoint / page fault / GPF / invalid opcode / double fault handlers+ (faults from ring 3 kill the process); 8259 PIC; timer handler (uptime, ticks, calls the thread scheduler), keyboard handler (just queues the scancode)-src/memory.rs OffsetPageTable setup from CR3, example mapping, BootInfoFrameAllocator+src/memory.rs OffsetPageTable setup from CR3, example mapping, BootInfoFrameAllocator,+ global frame allocator, per-process address spaces, user pointer checks src/allocator.rs heap location/size, init_heap (maps heap pages), #[global_allocator], Locked<A> src/allocator/bump.rs bump allocator (not active, for learning) src/allocator/fixed_size_block.rs fixed-size-block allocator (ACTIVE), falls back to linked_list_allocator@@ -195,7 +228,20 @@ src/task/mod.rs async: Task (pinned boxed future + id), yield_now() src/task/simple_executor.rs first executor: polls every task in a loop (no wakers) src/task/executor.rs real executor: task queue + wakers, sleeps with hlt when idle src/task/keyboard.rs lock-free scancode queue, ScancodeStream, async keyboard echo task-src/thread.rs preemptive kernel threads: spawn, round-robin schedule(), asm context switch+src/thread.rs preemptive threads: spawn, round-robin schedule(), asm context switch,+ per-thread kernel stack (TSS.rsp0) + CR3 switching+src/syscall.rs syscall/sysret MSR setup, asm entry stub, dispatch, read/write/open/close/...+src/process.rs Process (pid, fd table, exit status), spawn from ELF, iretq into ring 3, kill+src/elf.rs minimal ELF64 loader (PT_LOAD segments -> user pages)+src/errno.rs Linux errno numbers+src/tty.rs keyboard line buffer + Console (the File behind fds 0/1/2)+src/fs/mod.rs VFS: Inode + FileSystem traits, mount table, path lookup, init (boot files)+src/fs/file.rs File trait (what an fd points to), InodeFile (inode + offset)+src/fs/ramfs.rs in-memory filesystem+build.rs compiles user/*.rs to ELF files in OUT_DIR+user/ulib.rs tiny user-space "libc": syscall wrappers, println!, _start, panic handler+user/link.ld linker script for user programs (base 0x80_0040_0000)+user/{hello,evil,spin,echo}.rs the user programs src/vga_buffer.rs VGA text-mode writer, print!/println!, print_at (status line), backspace src/serial.rs COM1 UART writer + serial_print!/serial_println! tests/ integration tests, one QEMU-booted kernel each@@ -346,9 +392,185 @@ sleep queue; finished threads' stacks are never freed; no guard page under thread stacks (an overflow silently corrupts the heap); no priorities; a single CPU, so `spin::Mutex` is enough. +## User mode, syscalls and processes++Four tiny Rust programs in `user/` run in **ring 3** as separate processes, each+with its own address space and file-descriptor table:++| Program | What it does |+|---|---|+| `hello` | prints "Hello from user mode!", `getpid`, reads `/hello.txt` from the ramfs, creates and reads back `/tmp/note.txt`, checks error cases (`ENOENT`, `EBADF`, `EFAULT`, `ENOSYS`), exits with the number of failed checks (0) |+| `evil` | reads kernel memory at `0x200000` -> page fault in ring 3 -> the kernel kills it and keeps going |+| `spin` | busy-loops ~2 s with no syscalls; it still gets preempted (threads A/B/C keep printing) |+| `echo` | `read(0)` a line from the keyboard, prints `[echo] you typed: ...`; type `exit` to end it |++### Rings in one paragraph++The CPU's *current privilege level* (CPL) is the low 2 bits of the CS register:+0 = kernel, 3 = user. In ring 3 the CPU refuses privileged instructions (`hlt`,+`cli`, writing CR3, `in`/`out`...) and refuses to touch any page whose page-table+entries lack the `USER_ACCESSIBLE` bit. User code can only get back into ring 0+through doors the kernel set up: interrupts/exceptions (via the IDT) and the+`syscall` instruction (via the LSTAR MSR). Every such door switches to a kernel+stack, so a user program can't corrupt kernel state through its own stack pointer.++### GDT and TSS (`src/gdt.rs`)++```+0x00 null+0x08 kernel code (ring 0) 0x10 kernel data (ring 0)+0x18 user data (ring 3) |3 0x20 user code (ring 3) |3+0x28 TSS (16 bytes: two slots)+```++The odd order (user *data* before user *code*) is forced by `sysret`: it loads+SS = STAR[63:48] + 8 and CS = STAR[63:48] + 16, so the two user segments must+be adjacent in that order. The TSS has two jobs: IST[0] is the double-fault+stack (stage 2), and **`rsp0`** is the stack the CPU switches to when an+interrupt arrives while in ring 3. Each thread has its own kernel stack, so the+scheduler writes the next thread's stack top into `TSS.rsp0` on every switch+(`gdt::set_kernel_stack`).++### The syscall path (`src/syscall.rs`)++`syscall` was chosen over `int 0x80`: it's what Linux uses on x86_64, it's+faster, and the setup is only four MSRs. The catch is that, unlike an+interrupt, `syscall` does **not** switch stacks -- the entry code must.++Setup (`syscall::init`): `EFER.SCE = 1` (enable), `STAR` = the selectors above,+`LSTAR` = address of `syscall_entry`, `SFMASK = IF|DF|TF` (so we enter with+interrupts off until we're on a kernel stack).++One call, step by step:++1. User code puts the number in `rax`, args in `rdi, rsi, rdx` (Linux ABI; we+ also save `r10, r8, r9`) and executes `syscall`.+2. CPU: `rcx` = user RIP, `r11` = user RFLAGS, CS/SS = kernel, RFLAGS &= ~SFMASK,+ RIP = LSTAR. RSP is still the *user* stack.+3. `syscall_entry` (asm): stash user RSP in a global, load this thread's kernel+ stack top (set by the scheduler), push user RSP, `rcx`, `r11` and the args.+4. Shuffle into the C ABI and call `dispatch(nr, a1..a5)` in Rust with+ interrupts re-enabled (so a long syscall, e.g. a blocking `read`, can be preempted).+5. `dispatch` validates user pointers (`user_slice`: range must be inside the+ user half *and* mapped with `USER_ACCESSIBLE` in the current page table),+ runs the call, and returns a value or `-errno` in `rax`.+6. `cli`, pop everything back, restore user RSP, `sysretq` (RIP = rcx,+ RFLAGS = r11, CPL = 3).++Single CPU, so the "current kernel stack" and "saved user RSP" are plain+globals; SMP kernels use per-CPU data reached with `swapgs`.++| nr | name | args | notes |+|---|---|---|---|+| 0 | `read` | fd, buf, len | fd 0 = keyboard (blocks until Enter), others = files |+| 1 | `write` | fd, buf, len | fd 1/2 = console (VGA + serial) |+| 2 | `open` | path, path_len, flags | **differs from Linux**: path is ptr+len, not a C string; `O_CREAT` = 0o100 |+| 3 | `close` | fd | |+| 24 | `sched_yield` | | |+| 39 | `getpid` | | |+| 60 | `exit` | code | |++Errors are negative errno values like Linux (`-2` ENOENT, `-9` EBADF, `-14`+EFAULT, `-38` ENOSYS...). Unknown numbers return `-ENOSYS`.++### Address spaces (`src/memory.rs`, `src/process.rs`)++Each process gets a fresh level-4 page table. The kernel's L4 entries are+**copied** into it (so the kernel, heap, physical-memory map and thread stacks+are mapped in every address space and the kernel keeps working after a CR3+switch), and L4 slot 1 is the process's own:++```+0x0000_0000_0020_0000 kernel code (bootloader 0.9 puts it here) shared, ring 0 only+0x0000_0080_0000_0000 USER_START (L4 slot 1) per process+0x0000_0080_0040_0000 program (.text R-X, .rodata R--, .data/.bss RW-)+0x0000_00ff_ffff_b000 user stack (16 KiB, RW-, grows down)+0x0000_0100_0000_0000 USER_END+0x0000_0100_0020_0000 boot stack (L4 slot 2) shared+0x0000_0180_0000_0000 all physical memory (phys_mem_offset, slot 3) shared+0x0000_4444_4444_0000 kernel heap (thread kernel stacks live here) shared+```++Real kernels put the kernel in the upper half (`0xffff_8000_...`) and give+user space the whole lower half. With bootloader 0.9 the kernel sits low, so+here user space is one 512 GiB slot that the kernel doesn't use. Only user+pages have `USER_ACCESSIBLE`, which is why `evil` faults: the page at+`0x200000` *is* mapped, just not for ring 3 (`PROTECTION_VIOLATION | USER_MODE`).+`.text` is not writable and everything else is `NO_EXECUTE` (when EFER.NXE is on).++A **process** = a kernel thread (`thread.rs`, it already has a kernel stack and+gets preempted) + a CR3 value + a `Process` struct (pid, name, fd table, exit+status). The scheduler, on each switch, sets `TSS.rsp0` and the syscall kernel+stack to the next thread's stack and writes CR3 if it differs. A new process's+thread starts in the kernel, then `iretq`s into ring 3 with a hand-built frame+(SS=user data, RSP=user stack, RFLAGS=IF, CS=user code, RIP=ELF entry).++When ring-3 code faults (page fault with `USER_MODE`, general protection fault+or invalid opcode with CS RPL 3), the handler calls `process::exit_current`+instead of panicking: the process is marked `Killed`, its thread exits, the+scheduler picks someone else. A fault in ring 0 still panics -- that's a kernel bug.++### ELF loading and building user programs (`src/elf.rs`, `build.rs`, `user/`)++`build.rs` compiles each `user/*.rs` with plain `rustc` for the built-in+`x86_64-unknown-none` target (no build-std needed: it ships a precompiled+`core` and uses soft-float, so no SSE state to save). Flags worth knowing:+`-C relocation-model=static` (no dynamic loader), `-C code-model=large`+(the program lives above 4 GiB; the default model assumes 32-bit addresses+and fails with "relocation overflow"), `-T user/link.ld` (one PT_LOAD+segment per permission, page-aligned). The large code model names its+sections `.ltext`/`.lrodata`/`.ldata`/`.lbss`, so the linker script lists+those too -- forgetting that put the code into a non-executable segment and+every program died at its first instruction. The ELFs (~15-20 KB) are embedded in the+kernel with `include_bytes!` (`rust_kernel::user_programs`).++`user/ulib.rs` is a tiny libc-equivalent included by each program: the+`syscall` inline asm, wrappers, `print!/println!`, `_start` (calls `main`,+then `exit` with its return value) and a panic handler (`exit(101)`).++The loader (~160 lines, no crate) checks the header (magic, 64-bit, little+endian, x86_64, `ET_EXEC`), and for every `PT_LOAD` segment: checks it's+inside the user range, maps zeroed frames with flags from the segment+(`W` -> writable, no `X` -> NX), copies `filesz` bytes through the+physical-memory map and leaves the rest (`.bss`) zero.++### VFS (`src/fs/`) and file descriptors++Designed like Linux's so real filesystems can plug in later:++- `trait Inode` (`fs/mod.rs`): a file or directory: `kind`, `size`,+ `read_at(offset, buf)`, `write_at`, `lookup(name)`, `create(name, kind)`, `list`.+ Methods have default impls returning an error, so a read-only fs implements+ only what it supports.+- `trait FileSystem`: `name()` and `root() -> Arc<dyn Inode>`. A disk filesystem+ will hold its block device and parsed superblock.+- **Mount table**: `mount("/tmp", fs)`; `lookup(path)` picks the longest mount+ point that prefixes the path, then walks the rest with `Inode::lookup`.+ Boot mounts a ramfs on `/` (`/hello.txt`, `/etc/motd`, `/tmp`) and a+ second, separate ramfs on `/tmp`.+- `trait File` (`fs/file.rs`): what a file descriptor points to: `read(buf)`,+ `write(buf)`. `InodeFile` = an inode + its own offset (two opens of the same+ file have separate offsets, like Linux's `struct file`). `tty::Console` is+ another `File`: writes go to VGA+serial, reads come from the keyboard line buffer.+- **fd table**: `Vec<Option<Arc<dyn File>>>` per process (max 16), fds 0-2 =+ Console. `open` = `fs::lookup` (or `fs::create` with `O_CREAT`) + `InodeFile::open`+ + first free slot.++Keyboard input for `read(0)` goes through a minimal tty line discipline+(`src/tty.rs`): the keyboard task feeds characters in, Backspace edits the+current line, Enter makes the line (with `\n`) available to readers.++Known simplifications (exercises): a process's page tables and frames are+never freed on exit; `read(0)` waits with `hlt` instead of blocking the+thread; no `fork`/`exec`/`wait` syscalls (the kernel spawns programs and+`process::wait` is kernel-only); no `lseek`, `stat`, `readdir` yet; user+pointer checks walk the page table but don't guard against another thread+unmapping the page mid-syscall (there's no `munmap` yet, so that's fine for now).+ ## Roadmap -Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads.+Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads,+user mode + syscalls, ELF loading and a VFS with a ramfs. - [x] Freestanding binary — [phil-opp](https://os.phil-opp.com/freestanding-rust-binary/) - [x] Minimal kernel booting in QEMU — [phil-opp](https://os.phil-opp.com/minimal-rust-kernel/), [OSDev: Bare Bones](https://wiki.osdev.org/Bare_Bones)@@ -367,26 +589,56 @@ Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads. - [x] Async/await — Task, simple executor, waker-based executor, lock-free async keyboard task (with Backspace). [phil-opp: Async/Await](https://os.phil-opp.com/async-await/), [OSDev: Multitasking Systems](https://wiki.osdev.org/Multitasking_Systems) - [x] Preemptive kernel threads — own stacks, timer-driven round-robin, asm context switch. [OSDev: Brendan's Multi-tasking Tutorial](https://wiki.osdev.org/Brendan%27s_Multi-tasking_Tutorial), [OSDev: Context Switching](https://wiki.osdev.org/Context_Switching) -Next stages (beyond the phil-opp series):--1. **User mode (ring 3) & syscalls** — ring 3 code/data segments in the GDT, set- `TSS.rsp0` per thread so interrupts from user mode land on the kernel stack,- enter user mode with `iretq`, then `syscall`/`sysret` (or `int 0x80` first) for a- few calls (`write`, `exit`, `yield`). Per-process page tables (a new level-4- table that shares the kernel half). Threads become processes.- [OSDev: Getting to Ring 3](https://wiki.osdev.org/Getting_to_Ring_3),- [OSDev: System Calls](https://wiki.osdev.org/System_Calls),- [OSDev: SYSENTER/SYSCALL](https://wiki.osdev.org/SYSENTER)-2. **ELF loading** — parse a static ELF (e.g. with the `xmas-elf` or `goblin` crate),- map its segments into a fresh address space, jump to the entry point in ring 3.- [OSDev: ELF](https://wiki.osdev.org/ELF), [OSDev: ELF Tutorial](https://wiki.osdev.org/ELF_Tutorial)-3. **Filesystem** — a VFS layer, a RAM disk / initrd first, then FAT on an ATA/virtio disk.- [OSDev: File Systems](https://wiki.osdev.org/File_Systems), [OSDev: VFS](https://wiki.osdev.org/VFS),- [OSDev: Initrd](https://wiki.osdev.org/Initrd), [OSDev: FAT](https://wiki.osdev.org/FAT),- [OSDev: ATA PIO Mode](https://wiki.osdev.org/ATA_PIO_Mode)-4. **Shell** — line editing on top of the keyboard task, a few built-in commands- (`help`, `mem`, `uptime`, `ls`, `cat`), later launching user programs.- [OSDev: Shell](https://wiki.osdev.org/Shell)+- [x] User mode (ring 3) & syscalls — GDT user segments, TSS.rsp0 per thread, `syscall`/`sysret`, per-process page tables, user faults kill the process. [OSDev: Getting to Ring 3](https://wiki.osdev.org/Getting_to_Ring_3), [OSDev: System Calls](https://wiki.osdev.org/System_Calls), [OSDev: SYSENTER/SYSCALL](https://wiki.osdev.org/SYSENTER)+- [x] ELF loading — hand-written PT_LOAD loader, user programs built by `build.rs`. [OSDev: ELF](https://wiki.osdev.org/ELF), [OSDev: ELF Tutorial](https://wiki.osdev.org/ELF_Tutorial)+- [x] VFS skeleton + ramfs — Inode/FileSystem/File traits, mount table, fd table, `open/read/write/close`. [OSDev: VFS](https://wiki.osdev.org/VFS), [Linux VFS overview](https://www.kernel.org/doc/html/latest/filesystems/vfs.html)++### Filesystem roadmap (Tyler's plan)++Everything below plugs into the VFS above: a filesystem is a `FileSystem`+whose `root()` returns `Inode`s; mounting it is one `fs::mount` call.++1. **ramfs / tmpfs** — done (`src/fs/ramfs.rs`).+2. **Block device driver** — a `BlockDevice` trait (`read_blocks(lba, buf)`,+ `write_blocks`, `block_size`) with a first driver: **virtio-blk** (modern,+ fast, what QEMU/KVM use; needs PCI enumeration + virtqueues) or **ATA PIO**+ (dead simple port I/O, good first step). Then a small block cache.+ [OSDev: ATA PIO Mode](https://wiki.osdev.org/ATA_PIO_Mode),+ [OSDev: Virtio](https://wiki.osdev.org/Virtio), [OSDev: PCI](https://wiki.osdev.org/PCI),+ [virtio spec](https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html)+3. **Partition tables** — MBR and GPT parsing so a disk can hold several filesystems.+ [OSDev: GPT](https://wiki.osdev.org/GPT)+4. **FAT32** — the easiest real on-disk format, using the `fatfs` crate (`no_std`+ with `alloc`) behind an adapter that implements `FileSystem`/`Inode`. Test+ images are easy to make with `mkfs.vfat` + `mtools`.+ [OSDev: FAT](https://wiki.osdev.org/FAT), [fatfs crate](https://crates.io/crates/fatfs)+5. **ext2** — written by hand: superblock, block groups, inode table, direct/indirect+ blocks, directory entries. The classic "learn how Unix filesystems work" step,+ and the base for understanding ext4 later. [OSDev: Ext2](https://wiki.osdev.org/Ext2),+ [The Second Extended File System (Dave Poirier)](https://www.nongnu.org/ext2-doc/ext2.html)+6. **End goals:**+ - **exFAT** — because it works on literally anything (cameras, phones,+ Windows, macOS, Linux), so a kernel that speaks exFAT can share USB sticks+ and SD cards with every other machine. Allocation bitmap + FAT-like cluster+ chains + directory entry sets with checksums.+ [Microsoft exFAT specification](https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification)+ - **btrfs** (read-only first) — because most of Tyler's Linux systems will use+ btrfs going forward, so this kernel could read their disks. Much bigger:+ copy-on-write B-trees, the chunk tree (logical -> physical mapping), the+ root/fs trees, checksums, compression (zlib/lzo/zstd). Read-only means+ parsing the superblock, chunk tree and fs tree -- no transactions or+ space allocation. [btrfs on-disk format](https://btrfs.readthedocs.io/en/latest/dev/On-disk-format.html),+ [btrfs docs](https://btrfs.readthedocs.io/)+7. **NTFS** — last, maybe never.++Other next stages:++- **Shell** — a user-mode shell using `read(0)`/`write`; needs `fork`/`exec`+ (or a simpler `spawn(path)`) + `wait` syscalls and `readdir`/`stat` for `ls`/`cat`.+ [OSDev: Shell](https://wiki.osdev.org/Shell)+- **Process cleanup** — free a process's page tables, frames and kernel stack on exit.+- **Real blocking** — a blocked thread state + wait queues so `read(0)` and+ `sleep` don't spin with `hlt`. Small exercises along the way: a *blocked* state + sleep queue for threads, free finished threads' stacks, guard pages under thread stacks, a timer-based
Binary file changed.
@@ -0,0 +1,33 @@+//! Build script: compile the user programs in user/ into ELF executables in+//! OUT_DIR. The kernel embeds them with `include_bytes!` (src/lib.rs).+//!+//! They're compiled with plain `rustc` for the built-in `x86_64-unknown-none`+//! target (prebuilt `core`, so no build-std needed), as static executables+//! (`relocation-model=static`) linked at a high user address -- which needs+//! the `large` code model (the default ones assume code in the low/high 2 GiB).++use std::{env, path::PathBuf, process::Command};++const PROGRAMS: &[&str] = &["hello", "evil", "spin", "echo"];++fn main() {+ let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());+ let user_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("user");+ let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into());+ println!("cargo:rerun-if-changed=user");++ for program in PROGRAMS {+ let status = Command::new(&rustc)+ .args(["--edition", "2024", "--target", "x86_64-unknown-none"])+ .args(["-C", "panic=abort", "-C", "opt-level=2", "-C", "debuginfo=0", "-C", "strip=debuginfo"])+ .args(["-C", "relocation-model=static", "-C", "code-model=large"])+ .arg("-C")+ .arg(format!("link-arg=-T{}", user_dir.join("link.ld").display()))+ .arg("-o")+ .arg(out_dir.join(program))+ .arg(user_dir.join(format!("{program}.rs")))+ .status()+ .expect("failed to run rustc for a user program");+ assert!(status.success(), "building user program {program} failed");+ }+}
@@ -1,4 +1,6 @@ # Kernel dev needs nightly features (custom JSON target, build-std).+# x86_64-unknown-none: the target the user programs (user/) are built for. [toolchain] channel = "nightly" components = ["rust-src", "llvm-tools-preview"]+targets = ["x86_64-unknown-none"]
@@ -1,11 +1,13 @@ #!/usr/bin/env bash # Boot the normal (non-headless) kernel with no window, "type" some keys into # it through the QEMU monitor's `sendkey` command, then dump the VGA screen to-# a PNG. Doubles as an automated keyboard-interrupt test: the typed text is-# echoed on the serial port too, so we can grep for it.+# a PNG. Doubles as an automated keyboard + user-mode test: the default types+# a line (with a typo fixed by Backspace) and presses Enter; the `echo` user+# program reads it with read(0) and prints it back, all visible on serial. # # Usage: ./scripts/screenshot.sh [out.png] [text-to-type] [expected-result]-# In text-to-type, "<" means Backspace. The default types a typo and fixes it.+# In text-to-type, "<" means Backspace and "|" means Enter.+# Exits 1 if the expected text doesn't show up on serial. set -euo pipefail cd "$(dirname "$0")/.." @@ -15,8 +17,8 @@ touch target/x86_64-rust_kernel/debug/rust_kernel cargo bootimage IMG=target/x86_64-rust_kernel/debug/bootimage-rust_kernel.bin OUT="${1:-$PWD/boot.png}"-TEXT="${2:-Hello, kernex<l!}"-EXPECT="${3:-Hello, kernel!}"+TEXT="${2:-Hello, kernex<l!|}"+EXPECT="${3:-[echo] you typed: Hello, kernel!}" SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX) SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX) @@ -31,7 +33,7 @@ import socket, sys, time sock, out, text = sys.argv[1:] # QEMU key names for the characters we might type. names = {'<': 'backspace', ' ': 'spc', ',': 'comma', '.': 'dot', '!': 'shift-1', '?': 'shift-slash',- '-': 'minus', '\n': 'ret'}+ '-': 'minus', '|': 'ret'} def key(c): if c in names: return names[c] if c.isupper(): return 'shift-' + c.lower()@@ -64,6 +66,8 @@ then echo "[screenshot] keyboard echo OK: typed '$TEXT' -> '$EXPECT'" else echo "[screenshot] keyboard echo NOT found (expected '$EXPECT')"+ rm -f "$SERIAL_LOG"+ exit 1 fi rm -f "$SERIAL_LOG" echo "[screenshot] saved $OUT"
@@ -0,0 +1,163 @@+//! A tiny ELF64 loader: just enough to load a statically linked user program.+//!+//! An ELF file starts with a header saying what it is (64-bit, little endian,+//! x86_64, executable) and where the *program headers* are. Each program+//! header of type PT_LOAD says: "map `memsz` bytes at virtual address+//! `vaddr`, fill the first `filesz` of them from file offset `offset`, zero+//! the rest (that's .bss), with permissions R/W/X". That's all a loader for a+//! static executable has to do; then jump to `entry`.+//!+//! Every number we read comes from an untrusted file, so all offsets and sizes+//! are bounds-checked. Reference: `man 5 elf`, https://wiki.osdev.org/ELF++use crate::memory::{self, USER_END, USER_START};+use x86_64::structures::paging::{Page, PageTableFlags, PhysFrame, Size4KiB};+use x86_64::VirtAddr;++#[derive(Debug, PartialEq, Eq)]+pub enum ElfError {+ TooShort,+ NotElf,+ Not64BitLittleEndian,+ NotX86_64,+ NotExecutable,+ BadProgramHeader,+ OutsideUserSpace,+ OutOfMemory,+}++const PT_LOAD: u32 = 1;+const PF_X: u32 = 1;+const PF_W: u32 = 2;++fn read_u16(data: &[u8], at: usize) -> Result<u16, ElfError> {+ let bytes = data.get(at..at + 2).ok_or(ElfError::TooShort)?;+ Ok(u16::from_le_bytes(bytes.try_into().unwrap()))+}+fn read_u32(data: &[u8], at: usize) -> Result<u32, ElfError> {+ let bytes = data.get(at..at + 4).ok_or(ElfError::TooShort)?;+ Ok(u32::from_le_bytes(bytes.try_into().unwrap()))+}+fn read_u64(data: &[u8], at: usize) -> Result<u64, ElfError> {+ let bytes = data.get(at..at + 8).ok_or(ElfError::TooShort)?;+ Ok(u64::from_le_bytes(bytes.try_into().unwrap()))+}++/// One PT_LOAD program header.+struct Segment {+ flags: u32,+ offset: u64,+ vaddr: u64,+ filesz: u64,+ memsz: u64,+}++/// Check the ELF header; return (entry point, PT_LOAD segments).+fn parse(data: &[u8]) -> Result<(u64, impl Iterator<Item = Result<Segment, ElfError>> + '_), ElfError> {+ if data.len() < 64 {+ return Err(ElfError::TooShort);+ }+ if &data[0..4] != b"\x7fELF" {+ return Err(ElfError::NotElf);+ }+ if data[4] != 2 || data[5] != 1 {+ return Err(ElfError::Not64BitLittleEndian); // EI_CLASS=ELFCLASS64, EI_DATA=LSB+ }+ if read_u16(data, 18)? != 0x3e {+ return Err(ElfError::NotX86_64); // e_machine = EM_X86_64+ }+ if read_u16(data, 16)? != 2 {+ return Err(ElfError::NotExecutable); // e_type = ET_EXEC (static, non-PIE)+ }+ let entry = read_u64(data, 24)?;+ let phoff = read_u64(data, 32)? as usize;+ let phentsize = read_u16(data, 54)? as usize;+ let phnum = read_u16(data, 56)? as usize;+ if phentsize < 56 {+ return Err(ElfError::BadProgramHeader);+ }++ let segments = (0..phnum).filter_map(move |i| {+ let ph = phoff.checked_add(i.checked_mul(phentsize)?)?;+ let segment = (|| {+ Ok(Segment {+ flags: read_u32(data, ph + 4)?,+ offset: read_u64(data, ph + 8)?,+ vaddr: read_u64(data, ph + 16)?,+ filesz: read_u64(data, ph + 32)?,+ memsz: read_u64(data, ph + 40)?,+ })+ })();+ match read_u32(data, ph) {+ Ok(PT_LOAD) => Some(segment),+ Ok(_) => None, // other kinds (GNU_STACK, ...) we ignore+ Err(e) => Some(Err(e)),+ }+ });+ Ok((entry, segments))+}++/// Load the ELF image `data` into address space `l4`. Returns the entry point.+pub fn load(data: &[u8], l4: PhysFrame) -> Result<u64, ElfError> {+ let (entry, segments) = parse(data)?;+ if !(USER_START..USER_END).contains(&entry) {+ return Err(ElfError::OutsideUserSpace);+ }+ let nx_enabled = memory::nx_enabled();++ for segment in segments {+ let seg = segment?;+ let file_end = seg.offset.checked_add(seg.filesz).ok_or(ElfError::BadProgramHeader)?;+ let mem_end = seg.vaddr.checked_add(seg.memsz).ok_or(ElfError::BadProgramHeader)?;+ if seg.filesz > seg.memsz || file_end > data.len() as u64 {+ return Err(ElfError::BadProgramHeader);+ }+ if seg.vaddr < USER_START || mem_end > USER_END {+ return Err(ElfError::OutsideUserSpace);+ }+ if seg.memsz == 0 {+ continue;+ }++ let mut flags = PageTableFlags::empty();+ if seg.flags & PF_W != 0 {+ flags |= PageTableFlags::WRITABLE;+ }+ if seg.flags & PF_X == 0 && nx_enabled {+ flags |= PageTableFlags::NO_EXECUTE; // data can't be run as code+ }++ // Map every page the segment touches, then copy its file bytes in.+ // We write through the kernel's physical-memory window (the new+ // address space isn't active, so its user addresses mean nothing here).+ let first = Page::<Size4KiB>::containing_address(VirtAddr::new(seg.vaddr));+ let last = Page::<Size4KiB>::containing_address(VirtAddr::new(mem_end - 1));+ for page in Page::range_inclusive(first, last) {+ let frame = memory::map_user_page(l4, page, flags).ok_or(ElfError::OutOfMemory)?;++ // Which part of this page is covered by file bytes?+ let page_start = page.start_address().as_u64();+ let copy_start = page_start.max(seg.vaddr);+ let copy_end = (page_start + 4096).min(seg.vaddr + seg.filesz);+ if copy_start < copy_end {+ let src = &data[(seg.offset + (copy_start - seg.vaddr)) as usize..]+ [..(copy_end - copy_start) as usize];+ let dst = memory::phys_to_virt(frame.start_address()) + (copy_start - page_start);+ unsafe {+ core::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), src.len());+ }+ }+ // Bytes past filesz (.bss) stay zero: map_user_page zeroes new pages.+ }+ }+ Ok(entry)+}++/// Just validate the headers (used by tests).+pub fn check(data: &[u8]) -> Result<u64, ElfError> {+ let (entry, segments) = parse(data)?;+ for s in segments {+ s?;+ }+ Ok(entry)+}
@@ -0,0 +1,17 @@+//! Error numbers, same values as Linux (`man 3 errno`). Syscalls return+//! `-errno` in rax on failure, like Linux does.++#[derive(Debug, Clone, Copy, PartialEq, Eq)]+#[repr(i64)]+pub enum Errno {+ ENOENT = 2, // no such file or directory+ EBADF = 9, // bad file descriptor+ EFAULT = 14, // bad address (pointer not in user space / not mapped)+ EEXIST = 17, // file exists+ ENOTDIR = 20, // a path component isn't a directory+ EISDIR = 21, // is a directory+ EINVAL = 22, // invalid argument+ EMFILE = 24, // too many open files+ ENAMETOOLONG = 36, // path too long+ ENOSYS = 38, // no such system call+}
@@ -0,0 +1,49 @@+//! Open files: what a file descriptor refers to.+//!+//! `File` is the interface the read/write syscalls use. It's separate from+//! `Inode` because the same inode can be open several times with different+//! offsets, and because some files aren't in any file system at all (the+//! console). Linux calls this `struct file` + `file_operations`.++use super::{Inode, InodeKind};+use crate::errno::Errno;+use alloc::sync::Arc;+use spin::Mutex;++pub trait File: Send + Sync {+ /// Read up to `buf.len()` bytes; 0 means end of file.+ fn read(&self, buf: &mut [u8]) -> Result<usize, Errno>;+ /// Write bytes; returns how many were written.+ fn write(&self, buf: &[u8]) -> Result<usize, Errno>;+}++/// A regular file opened through the VFS: inode + current position.+pub struct InodeFile {+ inode: Arc<dyn Inode>,+ offset: Mutex<usize>,+}++impl InodeFile {+ pub fn open(inode: Arc<dyn Inode>) -> Result<Self, Errno> {+ if inode.kind() == InodeKind::Directory {+ return Err(Errno::EISDIR); // no getdents yet+ }+ Ok(InodeFile { inode, offset: Mutex::new(0) })+ }+}++impl File for InodeFile {+ fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+ let mut offset = self.offset.lock();+ let n = self.inode.read_at(*offset, buf)?;+ *offset += n;+ Ok(n)+ }++ fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+ let mut offset = self.offset.lock();+ let n = self.inode.write_at(*offset, buf)?;+ *offset += n;+ Ok(n)+ }+}
@@ -0,0 +1,162 @@+//! VFS (virtual file system): the layer every concrete file system plugs into.+//!+//! Modelled (very loosely) on Linux:+//!+//! * [`Inode`] -- one file or directory *inside some file system*. Knows how to+//! read/write its bytes at an offset and (for directories) look up / create+//! children. A FAT32, ext2, exFAT or btrfs driver implements this trait.+//! * [`FileSystem`] -- a mounted instance (e.g. "the FAT32 volume on disk 0");+//! gives you its root inode.+//! * the **mount table** -- which file system is attached at which path ("/",+//! "/tmp", ...). `lookup("/tmp/x")` finds the longest matching mount point+//! and walks the rest of the path from that file system's root.+//! * [`file::File`] -- an *open* file (what a file descriptor points to): an+//! inode plus a current offset, or a device like the console. The per-process+//! fd table holds `Arc<dyn File>`s, so syscalls don't care what's behind it.+//!+//! Today there is one implementation, [`ramfs`] (everything in RAM). Later: a+//! block device layer (virtio-blk / ATA PIO), then FAT32, ext2, exFAT and+//! btrfs (read-only first) as further `FileSystem`s.++pub mod file;+pub mod ramfs;++use crate::errno::Errno;+use alloc::{string::String, sync::Arc, vec::Vec};+use spin::Mutex;++#[derive(Debug, Clone, Copy, PartialEq, Eq)]+pub enum InodeKind {+ File,+ Directory,+}++/// A file or directory inside a file system.+///+/// Default method bodies return the error a "wrong kind" of inode would give+/// (e.g. `lookup` on a file -> ENOTDIR), so implementations only override+/// what makes sense for them.+pub trait Inode: Send + Sync {+ fn kind(&self) -> InodeKind;+ /// Size in bytes (files) or number of entries (directories).+ fn size(&self) -> usize;+ /// Read bytes starting at `offset`; returns how many were read (0 = EOF).+ fn read_at(&self, _offset: usize, _buf: &mut [u8]) -> Result<usize, Errno> {+ Err(Errno::EISDIR)+ }+ /// Write bytes at `offset` (growing the file if needed).+ fn write_at(&self, _offset: usize, _buf: &[u8]) -> Result<usize, Errno> {+ Err(Errno::EISDIR)+ }+ /// Find a child of this directory by name.+ fn lookup(&self, _name: &str) -> Result<Arc<dyn Inode>, Errno> {+ Err(Errno::ENOTDIR)+ }+ /// Create a child of this directory.+ fn create(&self, _name: &str, _kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {+ Err(Errno::ENOTDIR)+ }+ /// Names of all children of this directory.+ fn list(&self) -> Result<Vec<String>, Errno> {+ Err(Errno::ENOTDIR)+ }+}++/// A mounted file system.+pub trait FileSystem: Send + Sync {+ /// Type name, like the `-t` argument of `mount` ("ramfs", "fat32", ...).+ fn name(&self) -> &'static str;+ fn root(&self) -> Arc<dyn Inode>;+}++struct Mount {+ path: String, // normalized, e.g. "/" or "/tmp"+ fs: Arc<dyn FileSystem>,+}++/// The mount table. Lookups take the lock only briefly (never while doing+/// I/O), so it's fine as a spinlock. Not used from interrupt handlers.+static MOUNTS: Mutex<Vec<Mount>> = Mutex::new(Vec::new());++/// Attach `fs` at `path` (must be absolute, like "/" or "/tmp").+pub fn mount(path: &str, fs: Arc<dyn FileSystem>) -> Result<(), Errno> {+ if !path.starts_with('/') {+ return Err(Errno::EINVAL);+ }+ let path = String::from(if path.len() > 1 { path.trim_end_matches('/') } else { path });+ let mut mounts = MOUNTS.lock();+ if mounts.iter().any(|m| m.path == path) {+ return Err(Errno::EEXIST);+ }+ mounts.push(Mount { path, fs });+ Ok(())+}++/// (mount path, fs type) of every mount, for printing.+pub fn mounts() -> Vec<(String, &'static str)> {+ MOUNTS.lock().iter().map(|m| (m.path.clone(), m.fs.name())).collect()+}++/// Does mount point `mount` contain `path`? ("/tmp" contains "/tmp/x" but+/// not "/tmpfoo"; "/" contains everything.)+fn mount_contains(mount: &str, path: &str) -> bool {+ mount == "/"+ || path == mount+ || (path.starts_with(mount) && path.as_bytes().get(mount.len()) == Some(&b'/'))+}++/// Resolve an absolute path to an inode.+pub fn lookup(path: &str) -> Result<Arc<dyn Inode>, Errno> {+ if !path.starts_with('/') {+ return Err(Errno::EINVAL); // no current directory yet+ }+ if path.len() > 255 {+ return Err(Errno::ENAMETOOLONG);+ }+ // Longest mount point that contains the path wins.+ let (root, rest) = {+ let mounts = MOUNTS.lock();+ let m = mounts+ .iter()+ .filter(|m| mount_contains(&m.path, path))+ .max_by_key(|m| m.path.len())+ .ok_or(Errno::ENOENT)?;+ let rest = if m.path == "/" { path } else { &path[m.path.len()..] };+ (m.fs.root(), rest)+ };+ // Walk the remaining components ("a/b/c") from that file system's root.+ let mut inode = root;+ for name in rest.split('/').filter(|c| !c.is_empty() && *c != ".") {+ if name == ".." {+ return Err(Errno::EINVAL); // not supported yet+ }+ inode = inode.lookup(name)?;+ }+ Ok(inode)+}++/// Create a file at `path` (its parent directory must exist).+pub fn create(path: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {+ let path = path.trim_end_matches('/');+ let (parent, name) = path.rsplit_once('/').ok_or(Errno::EINVAL)?;+ if name.is_empty() {+ return Err(Errno::EINVAL);+ }+ let parent = lookup(if parent.is_empty() { "/" } else { parent })?;+ parent.create(name, kind)+}++/// Boot-time file system setup: a ramfs as "/", with a few files, and a+/// second, empty ramfs mounted at "/tmp" (to exercise the mount table).+pub fn init() {+ let root = ramfs::RamFs::new();+ let dir = root.root();+ let hello = dir.create("hello.txt", InodeKind::File).unwrap();+ hello.write_at(0, b"Hello from the ramfs! This is /hello.txt.\n").unwrap();+ let etc = dir.create("etc", InodeKind::Directory).unwrap();+ let motd = etc.create("motd", InodeKind::File).unwrap();+ motd.write_at(0, b"Welcome to Tyler's kernel. Next up: FAT32, ext2, exFAT, btrfs.\n").unwrap();+ dir.create("tmp", InodeKind::Directory).unwrap(); // mount point+ mount("/", root).unwrap();+ mount("/tmp", ramfs::RamFs::new()).unwrap();+}
@@ -0,0 +1,98 @@+//! ramfs: a file system that lives entirely on the heap (like Linux's ramfs /+//! tmpfs without the size limit). The simplest possible `FileSystem`: a tree+//! of nodes; files are a `Vec<u8>`, directories a sorted map name -> node.++use super::{FileSystem, Inode, InodeKind};+use crate::errno::Errno;+use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec};+use spin::Mutex;++pub struct RamFs {+ root: Arc<RamInode>,+}++impl RamFs {+ pub fn new() -> Arc<Self> {+ Arc::new(RamFs { root: Arc::new(RamInode::Dir(Mutex::new(BTreeMap::new()))) })+ }+}++impl FileSystem for RamFs {+ fn name(&self) -> &'static str {+ "ramfs"+ }+ fn root(&self) -> Arc<dyn Inode> {+ self.root.clone()+ }+}++pub enum RamInode {+ File(Mutex<Vec<u8>>),+ Dir(Mutex<BTreeMap<String, Arc<RamInode>>>),+}++impl Inode for RamInode {+ fn kind(&self) -> InodeKind {+ match self {+ RamInode::File(_) => InodeKind::File,+ RamInode::Dir(_) => InodeKind::Directory,+ }+ }++ fn size(&self) -> usize {+ match self {+ RamInode::File(data) => data.lock().len(),+ RamInode::Dir(entries) => entries.lock().len(),+ }+ }++ fn read_at(&self, offset: usize, buf: &mut [u8]) -> Result<usize, Errno> {+ let RamInode::File(data) = self else { return Err(Errno::EISDIR) };+ let data = data.lock();+ if offset >= data.len() {+ return Ok(0); // EOF+ }+ let n = buf.len().min(data.len() - offset);+ buf[..n].copy_from_slice(&data[offset..offset + n]);+ Ok(n)+ }++ fn write_at(&self, offset: usize, buf: &[u8]) -> Result<usize, Errno> {+ let RamInode::File(data) = self else { return Err(Errno::EISDIR) };+ let mut data = data.lock();+ let end = offset.checked_add(buf.len()).ok_or(Errno::EINVAL)?;+ if data.len() < end {+ data.resize(end, 0); // writing past the end grows the file (gap = zeros)+ }+ data[offset..end].copy_from_slice(buf);+ Ok(buf.len())+ }++ fn lookup(&self, name: &str) -> Result<Arc<dyn Inode>, Errno> {+ let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+ let node = entries.lock().get(name).cloned().ok_or(Errno::ENOENT)?;+ Ok(node)+ }++ fn create(&self, name: &str, kind: InodeKind) -> Result<Arc<dyn Inode>, Errno> {+ let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+ if name.is_empty() || name.contains('/') || name.len() > 255 {+ return Err(Errno::EINVAL);+ }+ let mut entries = entries.lock();+ if entries.contains_key(name) {+ return Err(Errno::EEXIST);+ }+ let node = Arc::new(match kind {+ InodeKind::File => RamInode::File(Mutex::new(Vec::new())),+ InodeKind::Directory => RamInode::Dir(Mutex::new(BTreeMap::new())),+ });+ entries.insert(String::from(name), node.clone());+ Ok(node)+ }++ fn list(&self) -> Result<Vec<String>, Errno> {+ let RamInode::Dir(entries) = self else { return Err(Errno::ENOTDIR) };+ Ok(entries.lock().keys().cloned().collect())+ }+}
@@ -1,16 +1,29 @@ //! GDT (Global Descriptor Table) + TSS (Task State Segment). //!-//! In 64-bit mode segmentation is mostly dead, but we still need a GDT for one-//! important reason: it is how the CPU finds the TSS, and the TSS holds the-//! Interrupt Stack Table (IST) -- a list of known-good stacks the CPU can-//! switch to when an exception happens.+//! In 64-bit mode segmentation is mostly dead, but the GDT still matters: //!-//! Why it matters: if the kernel stack overflows, the CPU hits the guard page-//! and raises a page fault. Pushing the exception frame onto the (already-//! overflowed) stack faults again -> double fault -> same problem -> TRIPLE-//! fault, which resets the machine. Giving the double-fault handler its own-//! IST stack breaks that chain.+//! * **Privilege levels.** The low 2 bits of CS are the *current privilege+//! level* (CPL): ring 0 = kernel, ring 3 = user. The code segment+//! descriptors in the GDT say which ring they belong to, so to run user code+//! we need user code/data segments (DPL 3) next to the kernel ones.+//! * **The TSS.** The GDT is how the CPU finds the TSS, which holds:+//! - `privilege_stack_table[0]` ("RSP0"): the stack the CPU switches to when+//! an interrupt/exception arrives while running *ring 3* code. User+//! stacks can't be trusted, so every thread that runs user code has its+//! own kernel stack, and the scheduler updates RSP0 on every switch.+//! - the Interrupt Stack Table (IST): known-good stacks for specific+//! exceptions. The double-fault handler uses one, so a kernel stack+//! overflow doesn't turn into a triple fault (= reboot).+//!+//! Segment order matters because of `syscall`/`sysret` (see syscall.rs): they+//! don't read the GDT entries' contents but *compute* selectors from the STAR+//! MSR: kernel CS = base, kernel SS = base + 8; user SS = base2 + 8,+//! user CS = base2 + 16. Hence:+//!+//! 0x00 null | 0x08 kernel code | 0x10 kernel data | 0x18 user data |+//! 0x20 user code | 0x28 TSS (16 bytes, two slots) +use core::ptr::{addr_of, addr_of_mut}; use lazy_static::lazy_static; use x86_64::VirtAddr; use x86_64::structures::gdt::{Descriptor, GlobalDescriptorTable, SegmentSelector};@@ -19,49 +32,74 @@ use x86_64::structures::tss::TaskStateSegment; /// Which IST slot (0..=6) the double-fault handler uses. pub const DOUBLE_FAULT_IST_INDEX: u16 = 0; -lazy_static! {- static ref TSS: TaskStateSegment = {- let mut tss = TaskStateSegment::new();- tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {- // No memory allocator yet, so the stack is just a static array.- // (It has no guard page, so don't do anything recursive in the- // double-fault handler!)- const STACK_SIZE: usize = 4096 * 5;- static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];+/// The TSS. A `static mut` because the scheduler rewrites RSP0 on every+/// context switch (the CPU re-reads it from memory on each ring 3 -> ring 0+/// transition, so writing the field is enough). Only touched with interrupts+/// disabled, on a single CPU.+static mut TSS: TaskStateSegment = TaskStateSegment::new(); - // Stacks grow downwards on x86, so hand the CPU the *end* address.- let stack_start = VirtAddr::from_ptr(&raw const STACK);- stack_start + STACK_SIZE as u64- };- tss- };+/// Stack for the double-fault handler (static: no heap needed. It has no+/// guard page, so don't do anything recursive in the double-fault handler!)+const DOUBLE_FAULT_STACK_SIZE: usize = 4096 * 5;+static mut DOUBLE_FAULT_STACK: [u8; DOUBLE_FAULT_STACK_SIZE] = [0; DOUBLE_FAULT_STACK_SIZE];++/// Selectors (GDT offsets + requested privilege level) of our segments.+pub struct Selectors {+ pub kernel_code: SegmentSelector,+ pub kernel_data: SegmentSelector,+ pub user_data: SegmentSelector, // RPL 3+ pub user_code: SegmentSelector, // RPL 3+ tss: SegmentSelector, } lazy_static! {- /// The GDT, plus the selectors (indexes) of the entries we added. static ref GDT: (GlobalDescriptorTable, Selectors) = { let mut gdt = GlobalDescriptorTable::new();- let code_selector = gdt.append(Descriptor::kernel_code_segment());- let tss_selector = gdt.append(Descriptor::tss_segment(&TSS));- (gdt, Selectors { code_selector, tss_selector })+ // Order matters for syscall/sysret, see the module comment.+ let kernel_code = gdt.append(Descriptor::kernel_code_segment());+ let kernel_data = gdt.append(Descriptor::kernel_data_segment());+ let user_data = gdt.append(Descriptor::user_data_segment());+ let user_code = gdt.append(Descriptor::user_code_segment());+ // Safety: TSS is a static, so the reference lives forever.+ let tss = gdt.append(Descriptor::tss_segment(unsafe { &*addr_of!(TSS) }));+ (gdt, Selectors { kernel_code, kernel_data, user_data, user_code, tss }) }; } -struct Selectors {- code_selector: SegmentSelector,- tss_selector: SegmentSelector,+/// The segment selectors, e.g. to build an `iretq` frame into ring 3.+pub fn selectors() -> &'static Selectors {+ &GDT.1 } -/// Load our GDT, point CS at our code segment and load the TSS.+/// Load our GDT, reload the segment registers and load the TSS. pub fn init() {- use x86_64::instructions::segmentation::{CS, Segment};+ use x86_64::instructions::segmentation::{CS, DS, ES, SS, Segment}; use x86_64::instructions::tables::load_tss; + unsafe {+ // Stacks grow downwards on x86, so hand the CPU the *end* address.+ let start = VirtAddr::from_ptr(addr_of!(DOUBLE_FAULT_STACK));+ (*addr_of_mut!(TSS)).interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] =+ start + DOUBLE_FAULT_STACK_SIZE as u64;+ }+ GDT.0.load();+ let s = &GDT.1;+ unsafe {+ // The old selectors point into the bootloader's GDT, which we just+ // replaced, so reload them. (`iretq` checks SS, so it must be valid.)+ CS::set_reg(s.kernel_code);+ SS::set_reg(s.kernel_data);+ DS::set_reg(s.kernel_data);+ ES::set_reg(s.kernel_data);+ load_tss(s.tss);+ }+}++/// Set RSP0: the kernel stack the CPU switches to when an interrupt arrives+/// while user code runs. Called by the scheduler on every context switch.+pub fn set_kernel_stack(stack_top: u64) { unsafe {- // The old CS value points into the bootloader's GDT, which we just- // replaced, so reload it. Then tell the CPU where our TSS is.- CS::set_reg(GDT.1.code_selector);- load_tss(GDT.1.tss_selector);+ (*addr_of_mut!(TSS)).privilege_stack_table[0] = VirtAddr::new(stack_top); } }
@@ -51,6 +51,8 @@ lazy_static! { let mut idt = InterruptDescriptorTable::new(); idt.breakpoint.set_handler_fn(breakpoint_handler); idt.page_fault.set_handler_fn(page_fault_handler);+ idt.general_protection_fault.set_handler_fn(general_protection_fault_handler);+ idt.invalid_opcode.set_handler_fn(invalid_opcode_handler); unsafe { // Run the double-fault handler on its own known-good stack (IST). idt.double_fault@@ -78,14 +80,51 @@ extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) { serial_println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame); } +/// Did the exception happen in user mode (ring 3)? Then it's the process's+/// fault, not the kernel's: kill the process and keep the kernel running.+/// The CPU puts the interrupted code's CS in the frame; its low 2 bits are+/// the privilege level the code ran at.+fn kill_if_user(stack_frame: &InterruptStackFrame, what: &'static str) {+ use x86_64::PrivilegeLevel;+ if stack_frame.code_segment.rpl() == PrivilegeLevel::Ring3 {+ serial_println!(+ "[kernel] {} in user mode at rip {:#x}",+ what,+ stack_frame.instruction_pointer.as_u64()+ );+ crate::process::exit_current(crate::process::ExitStatus::Killed(what)); // never returns+ }+}++/// General protection fault: e.g. a privileged instruction (`hlt`, `cli`)+/// in ring 3, or a bad segment selector.+extern "x86-interrupt" fn general_protection_fault_handler(+ stack_frame: InterruptStackFrame,+ error_code: u64,+) {+ kill_if_user(&stack_frame, "general protection fault");+ panic!("EXCEPTION: GENERAL PROTECTION FAULT (error {:#x})\n{:#?}", error_code, stack_frame);+}++/// Invalid opcode (`ud2`, or garbage executed as code).+extern "x86-interrupt" fn invalid_opcode_handler(stack_frame: InterruptStackFrame) {+ kill_if_user(&stack_frame, "invalid opcode");+ panic!("EXCEPTION: INVALID OPCODE\n{:#?}", stack_frame);+}+ /// Page fault: accessing unmapped/protected memory. CR2 holds the bad address, /// the error code says why (not present? write? user mode?). A real OS would-/// now load the page from disk or kill the process; we just report and stop.+/// load the page from disk / swap here; we kill a faulting user process, and+/// stop if the kernel itself faulted. extern "x86-interrupt" fn page_fault_handler( stack_frame: InterruptStackFrame, error_code: PageFaultErrorCode, ) { use x86_64::registers::control::Cr2;+ if error_code.contains(PageFaultErrorCode::USER_MODE) {+ serial_println!("[kernel] user page fault: address {:?}, error {:?}", Cr2::read(), error_code);+ kill_if_user(&stack_frame, "page fault");+ } println!("EXCEPTION: PAGE FAULT at {:?}", Cr2::read()); serial_println!( "EXCEPTION: PAGE FAULT\nAccessed address: {:?}\nError code: {:?}\n{:#?}",
@@ -22,19 +22,34 @@ extern crate alloc; use core::panic::PanicInfo; pub mod allocator; // heap: maps heap pages + the #[global_allocator]-pub mod gdt; // GDT + TSS: gives the double-fault handler its own stack+pub mod elf; // ELF64 loader for user programs+pub mod errno; // Linux-compatible error numbers+pub mod fs; // VFS: Inode/FileSystem/File traits, mount table, ramfs+pub mod gdt; // GDT (kernel + user segments) + TSS (RSP0, double-fault stack) pub mod interrupts; // IDT, exception handlers, PIC, timer + keyboard-pub mod memory; // paging: page tables, address translation, frame allocator+pub mod memory; // paging, frame allocator, per-process address spaces+pub mod process; // processes: address space + thread + fd table, ring 3 entry pub mod serial; // COM1 serial port+pub mod syscall; // syscall/sysret entry + system call implementations pub mod task; // cooperative multitasking: async tasks + executor pub mod thread; // preemptive kernel threads + context switch+pub mod tty; // console: keyboard line discipline + fd 0/1/2 pub mod vga_buffer; // 80x25 VGA text mode screen +/// The user programs (user/*.rs), compiled to ELF by build.rs and embedded.+pub mod user_programs {+ pub static HELLO: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/hello"));+ pub static EVIL: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/evil"));+ pub static SPIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/spin"));+ pub static ECHO: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/echo"));+}+ /// Set up everything interrupt-related. Order matters: /// GDT/TSS first (the IDT refers to the IST stack), then the IDT, then the /// PICs, and only then enable interrupts (`sti`). pub fn init() { gdt::init();+ syscall::init(); interrupts::init_idt(); unsafe { interrupts::PICS.lock().initialize() }; x86_64::instructions::interrupts::enable();
@@ -14,7 +14,8 @@ use alloc::{boxed::Box, rc::Rc, vec, vec::Vec}; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo; use rust_kernel::task::{Task, executor::Executor, keyboard};-use rust_kernel::{allocator, kprintln, memory, println, serial_println, thread, vga_buffer};+use rust_kernel::{allocator, fs, kprintln, memory, println, process, serial_println, thread};+use rust_kernel::{user_programs, vga_buffer}; use x86_64::{VirtAddr, structures::paging::{Page, Translate}}; // `entry_point!` defines the real `_start` for us and checks at compile time@@ -47,12 +48,31 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { thread::spawn("C", thread_c); kprintln!("[thread] spawned threads A, B, C (status on the top line)"); + // --- VFS + user mode (fs/, process.rs, syscall.rs) ----------------------+ fs::init();+ for (path, fs_type) in fs::mounts() {+ serial_println!("[vfs] mounted {} at {}", fs_type, path);+ }+ // Each program is an ELF file built from user/*.rs and embedded in the+ // kernel. Each gets its own address space and runs in ring 3.+ let programs: [(&'static str, &'static [u8]); 4] = [+ ("hello", user_programs::HELLO), // syscalls + ramfs demo, exits 0+ ("evil", user_programs::EVIL), // touches kernel memory -> killed+ ("spin", user_programs::SPIN), // busy loop, gets preempted+ ("echo", user_programs::ECHO), // reads keyboard lines via read(0)+ ];+ for (name, elf) in programs {+ if let Err(e) = process::spawn(name, elf) {+ kprintln!("[process] could not start {}: {:?}", name, e);+ }+ }+ // --- Cooperative async tasks (task/) ------------------------------------ // The main thread runs the executor; the keyboard is just another task. let mut executor = Executor::new(); executor.spawn(Task::new(example_task())); executor.spawn(Task::new(keyboard::print_keypresses()));- kprintln!("Type something! (keyboard echo, Backspace works)");+ kprintln!("Type a line + Enter: the echo program (ring 3) reads it"); executor.run(); // never returns: runs tasks, `hlt`s when idle } @@ -184,6 +204,10 @@ fn paging_demo(boot_info: &'static BootInfo) { kprintln!("[heap] Rc count after clone = {}", Rc::strong_count(&cloned_reference)); core::mem::drop(reference_counted); kprintln!("[heap] Rc count after drop = {}", Rc::strong_count(&cloned_reference));++ // From now on the kernel owns the frame allocator globally (processes+ // need frames for their page tables and pages).+ memory::init_frame_allocator(frame_allocator); } /// Recurse forever until we run off the end of the stack into the guard page.
@@ -7,22 +7,48 @@ //! feature maps all of physical RAM at `physical_memory_offset`, so //! virtual address = physical_memory_offset + physical address. //! `OffsetPageTable` from the x86_64 crate implements exactly that.+//!+//! Address spaces (for user processes): every process gets its own level-4+//! table. Its entries are copied from the kernel's level-4 table, so the+//! kernel (code, heap, stacks, the physical-memory window) is mapped+//! identically everywhere -- just *without* the USER_ACCESSIBLE flag, so ring+//! 3 code can't touch it. User code and stack go into L4 slot 1+//! (`USER_START..USER_END`), which the kernel doesn't use. Switching CR3 then+//! swaps only the user part. (Linux does the same with the upper half; our+//! bootloader put the kernel in the *lower* half, so we use a free slot.) use bootloader::bootinfo::{MemoryMap, MemoryRegionType};+use core::sync::atomic::{AtomicU64, Ordering};+use spin::Mutex; use x86_64::{ PhysAddr, VirtAddr,+ registers::control::Cr3, structures::paging::{ FrameAllocator, Mapper, OffsetPageTable, Page, PageTable, PageTableFlags, PhysFrame,- Size4KiB,+ Size4KiB, Translate, mapper::TranslateResult, }, }; +/// User space: level-4 slot 1, i.e. 512 GiB starting at 512 GiB.+pub const USER_START: u64 = 0x0000_0080_0000_0000;+pub const USER_END: u64 = 0x0000_0100_0000_0000; // exclusive+const USER_L4_INDEX: usize = 1;++/// Where all physical memory is mapped (set by `init`).+static PHYS_OFFSET: AtomicU64 = AtomicU64::new(0);+/// Physical address of the kernel's (boot) level-4 table (set by `init`).+static KERNEL_L4: AtomicU64 = AtomicU64::new(0);+/// The frame allocator, once `init_frame_allocator` hands it over.+static FRAME_ALLOCATOR: Mutex<Option<BootInfoFrameAllocator>> = Mutex::new(None);+ /// Create an `OffsetPageTable` for the currently active page tables. /// /// Unsafe: the caller must guarantee that all physical memory really is /// mapped at `physical_memory_offset`, and must call this only once (two /// `&mut` to the same page table would be undefined behaviour). pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> {+ PHYS_OFFSET.store(physical_memory_offset.as_u64(), Ordering::Relaxed);+ KERNEL_L4.store(Cr3::read().0.start_address().as_u64(), Ordering::Relaxed); unsafe { let level_4_table = active_level_4_table(physical_memory_offset); OffsetPageTable::new(level_4_table, physical_memory_offset)@@ -31,8 +57,6 @@ pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> /// Find the active level-4 table via CR3 and return a reference to it. unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) -> &'static mut PageTable {- use x86_64::registers::control::Cr3;- let (level_4_table_frame, _flags) = Cr3::read(); let phys = level_4_table_frame.start_address(); let virt = physical_memory_offset + phys.as_u64();@@ -100,3 +124,138 @@ pub fn usable_memory_bytes(memory_map: &MemoryMap) -> u64 { .map(|r| r.range.end_addr() - r.range.start_addr()) .sum() }++// ---------------------------------------------------------------------------+// Global frame allocator + per-process address spaces+// ---------------------------------------------------------------------------++/// Hand the frame allocator over to the kernel (after the heap is set up),+/// so processes can get page tables and pages later.+pub fn init_frame_allocator(frame_allocator: BootInfoFrameAllocator) {+ *FRAME_ALLOCATOR.lock() = Some(frame_allocator);+}++/// One fresh physical frame, or None if out of memory / not initialized.+pub fn alloc_frame() -> Option<PhysFrame> {+ FRAME_ALLOCATOR.lock().as_mut()?.allocate_frame()+}++/// `FrameAllocator` adapter around the global allocator (for `map_to`, which+/// may need frames for new page tables).+struct GlobalFrameAllocator;++unsafe impl FrameAllocator<Size4KiB> for GlobalFrameAllocator {+ fn allocate_frame(&mut self) -> Option<PhysFrame> {+ alloc_frame()+ }+}++/// Virtual address through which the kernel can reach physical address `phys`.+pub fn phys_to_virt(phys: PhysAddr) -> VirtAddr {+ VirtAddr::new(PHYS_OFFSET.load(Ordering::Relaxed) + phys.as_u64())+}++/// The kernel's level-4 table frame (what kernel threads run with).+pub fn kernel_l4_frame() -> PhysFrame {+ match KERNEL_L4.load(Ordering::Relaxed) {+ 0 => Cr3::read().0, // `init` not called yet: we're still on the boot tables+ addr => PhysFrame::containing_address(PhysAddr::new(addr)),+ }+}++/// Does the CPU honour the NO_EXECUTE page flag? (EFER.NXE, which our+/// bootloader turns on. With it off, setting the bit would be an error.)+pub fn nx_enabled() -> bool {+ use x86_64::registers::model_specific::{Efer, EferFlags};+ Efer::read().contains(EferFlags::NO_EXECUTE_ENABLE)+}++/// A frame filled with zeros (new page tables and user pages must not leak+/// old data).+fn alloc_zeroed_frame() -> Option<PhysFrame> {+ let frame = alloc_frame()?;+ let ptr: *mut u8 = phys_to_virt(frame.start_address()).as_mut_ptr();+ unsafe { core::ptr::write_bytes(ptr, 0, 4096) };+ Some(frame)+}++/// Page-table view of the address space whose level-4 table is `l4`.+///+/// Unsafe: the caller must not create two of these for the same table at the+/// same time (both would hand out `&mut` to the same memory).+unsafe fn mapper_for(l4: PhysFrame) -> OffsetPageTable<'static> {+ let table: *mut PageTable = phys_to_virt(l4.start_address()).as_mut_ptr();+ let offset = VirtAddr::new(PHYS_OFFSET.load(Ordering::Relaxed));+ unsafe { OffsetPageTable::new(&mut *table, offset) }+}++/// Create a new address space: a fresh level-4 table that shares all kernel+/// mappings (entries copied from the kernel's table) and has an empty user part.+pub fn new_address_space() -> Option<PhysFrame> {+ let frame = alloc_zeroed_frame()?;+ unsafe {+ let new: &mut PageTable = &mut *phys_to_virt(frame.start_address()).as_mut_ptr();+ let kernel: &PageTable = &*phys_to_virt(kernel_l4_frame().start_address()).as_ptr();+ // The kernel must not use our user slot, or user mappings would end up+ // in page tables shared with the kernel (and every other process).+ assert!(kernel[USER_L4_INDEX].is_unused(), "kernel uses the user L4 slot");+ for i in 0..512 {+ new[i] = kernel[i].clone();+ }+ }+ Some(frame)+}++/// Make sure `page` (in user space) is mapped in address space `l4` with at+/// least `flags` (USER_ACCESSIBLE + PRESENT are added). A new page gets a+/// zeroed frame; an already mapped one keeps its frame and gets the union+/// of old and new flags. Returns the backing frame.+pub fn map_user_page(l4: PhysFrame, page: Page, flags: PageTableFlags) -> Option<PhysFrame> {+ let addr = page.start_address().as_u64();+ assert!((USER_START..USER_END).contains(&addr), "not a user address");+ let flags = flags | PageTableFlags::PRESENT | PageTableFlags::USER_ACCESSIBLE;++ let mut mapper = unsafe { mapper_for(l4) };+ if let TranslateResult::Mapped { frame, flags: old, .. } = mapper.translate(page.start_address()) {+ // Already mapped (two ELF segments sharing a page): widen the flags.+ // Keep NO_EXECUTE only if both want it.+ let mut new = old | flags;+ if !(old & flags).contains(PageTableFlags::NO_EXECUTE) {+ new.remove(PageTableFlags::NO_EXECUTE);+ }+ unsafe { mapper.update_flags(page, new).ok()?.ignore() };+ return Some(PhysFrame::containing_address(frame.start_address()));+ }+ let frame = alloc_zeroed_frame()?;+ // map_to also creates missing L3/L2/L1 tables; since `flags` contains+ // USER_ACCESSIBLE, those parent entries get it too (the CPU checks the+ // user bit at every level). The TLB doesn't matter: this address space+ // may not even be active, and the mapping is new. Hence `ignore`.+ unsafe { mapper.map_to(page, frame, flags, &mut GlobalFrameAllocator).ok()?.ignore() };+ Some(frame)+}++/// Can user code legitimately access `len` bytes at `addr` in the *current*+/// address space? Checks the range is inside user space and every page is+/// mapped USER_ACCESSIBLE (and WRITABLE if `write`). Syscalls call this+/// before touching any pointer a user program hands them; otherwise a program+/// could make the kernel read or overwrite kernel memory for it.+pub fn user_range_ok(addr: u64, len: u64, write: bool) -> bool {+ let Some(end) = addr.checked_add(len) else { return false };+ if addr < USER_START || end > USER_END {+ return false;+ }+ if len == 0 {+ return true;+ }+ let mapper = unsafe { mapper_for(Cr3::read().0) };+ let first = Page::<Size4KiB>::containing_address(VirtAddr::new(addr));+ let last = Page::<Size4KiB>::containing_address(VirtAddr::new(end - 1));+ Page::range_inclusive(first, last).all(|page| match mapper.translate(page.start_address()) {+ TranslateResult::Mapped { flags, .. } => {+ flags.contains(PageTableFlags::USER_ACCESSIBLE)+ && (!write || flags.contains(PageTableFlags::WRITABLE))+ }+ _ => false,+ })+}
@@ -0,0 +1,186 @@+//! Processes = a thread + its own address space + a file descriptor table.+//!+//! Starting one (`spawn`):+//! 1. new level-4 page table sharing the kernel mappings (memory.rs)+//! 2. load the ELF program's segments into its user part (elf.rs)+//! 3. map a user stack just below `USER_END`+//! 4. fd table: 0, 1, 2 = the console (tty.rs)+//! 5. spawn a thread with that CR3. The thread starts in the kernel+//! (`user_thread_entry`) and "returns" into ring 3 with `iretq`.+//!+//! From then on the process only comes back into the kernel through+//! interrupts (timer -> maybe another thread runs), exceptions (page fault in+//! user mode -> the process is killed, see interrupts.rs) and syscalls.+//!+//! Not done yet (exercises): freeing a dead process's frames and page tables,+//! fork/exec/wait syscalls, passing arguments, more than one thread per process.++use crate::elf::{self, ElfError};+use crate::errno::Errno;+use crate::fs::file::File;+use crate::memory::{self, USER_END};+use crate::{serial_println, thread, tty::Console};+use alloc::{sync::Arc, vec, vec::Vec};+use core::sync::atomic::{AtomicU64, Ordering};+use spin::Mutex;+use x86_64::VirtAddr;+use x86_64::structures::paging::{Page, PageTableFlags, Size4KiB};++/// The user stack: 16 KiB, ending one (unmapped) page below USER_END.+pub const USER_STACK_TOP: u64 = USER_END - 0x1000;+pub const USER_STACK_SIZE: u64 = 16 * 1024;+/// Max open files per process.+const MAX_FDS: usize = 16;++#[derive(Debug, Clone, Copy, PartialEq, Eq)]+pub enum ExitStatus {+ /// Called exit(code).+ Exited(i64),+ /// Killed by the kernel (e.g. "page fault").+ Killed(&'static str),+}++pub struct Process {+ pub pid: u64,+ pub name: &'static str,+ entry: u64,+ fds: Mutex<Vec<Option<Arc<dyn File>>>>,+ status: Mutex<Option<ExitStatus>>,+}++static NEXT_PID: AtomicU64 = AtomicU64::new(1);++/// Load ELF program `elf` into a new address space and start it.+pub fn spawn(name: &'static str, elf_image: &[u8]) -> Result<Arc<Process>, ElfError> {+ let l4 = memory::new_address_space().ok_or(ElfError::OutOfMemory)?;+ let entry = elf::load(elf_image, l4)?;++ let mut stack_flags = PageTableFlags::WRITABLE;+ if memory::nx_enabled() {+ stack_flags |= PageTableFlags::NO_EXECUTE;+ }+ let first = Page::<Size4KiB>::containing_address(VirtAddr::new(USER_STACK_TOP - USER_STACK_SIZE));+ let last = Page::<Size4KiB>::containing_address(VirtAddr::new(USER_STACK_TOP - 1));+ for page in Page::range_inclusive(first, last) {+ memory::map_user_page(l4, page, stack_flags).ok_or(ElfError::OutOfMemory)?;+ }++ let console: Arc<dyn File> = Arc::new(Console);+ let process = Arc::new(Process {+ pid: NEXT_PID.fetch_add(1, Ordering::Relaxed),+ name,+ entry,+ fds: Mutex::new(vec![Some(console.clone()), Some(console.clone()), Some(console)]),+ status: Mutex::new(None),+ });+ serial_println!("[process] started pid {} ({}), entry {:#x}", process.pid, name, entry);+ thread::spawn_with(name, user_thread_entry, l4, Some(process.clone()));+ Ok(process)+}++/// First code of every process thread (still ring 0, but already running+/// with the process's CR3 and kernel stack).+fn user_thread_entry() {+ let process = thread::current_process().expect("process thread without process");+ // Functions expect rsp % 16 == 8 on entry (as if a return address had+ // just been pushed), so start 8 bytes below the 16-aligned top.+ unsafe { enter_user_mode(process.entry, USER_STACK_TOP - 8) }+}++/// Jump to ring 3. `iretq` pops RIP, CS, RFLAGS, RSP, SS -- we push a fake+/// interrupt frame with user selectors (RPL 3), so "returning" from it drops+/// the privilege level. RFLAGS = 0x202: interrupts enabled (+ the always-1 bit).+unsafe fn enter_user_mode(rip: u64, rsp: u64) -> ! {+ let s = crate::gdt::selectors();+ unsafe {+ core::arch::asm!(+ "push {ss}",+ "push {rsp}",+ "push {rflags}",+ "push {cs}",+ "push {rip}",+ // Don't leak kernel values to user space in registers.+ "xor eax, eax", "xor ebx, ebx", "xor ecx, ecx", "xor edx, edx",+ "xor esi, esi", "xor edi, edi", "xor ebp, ebp",+ "xor r8d, r8d", "xor r9d, r9d", "xor r10d, r10d", "xor r11d, r11d",+ "xor r12d, r12d", "xor r13d, r13d", "xor r14d, r14d", "xor r15d, r15d",+ "iretq",+ ss = in(reg) s.user_data.0 as u64,+ rsp = in(reg) rsp,+ rflags = in(reg) 0x202u64,+ cs = in(reg) s.user_code.0 as u64,+ rip = in(reg) rip,+ options(noreturn)+ );+ }+}++impl Process {+ /// The open file behind `fd`.+ pub fn file(&self, fd: u64) -> Result<Arc<dyn File>, Errno> {+ let fds = self.fds.lock();+ fds.get(fd as usize).and_then(|f| f.clone()).ok_or(Errno::EBADF)+ }++ /// Put `file` into the lowest free fd slot (like Unix).+ pub fn add_file(&self, file: Arc<dyn File>) -> Result<u64, Errno> {+ let mut fds = self.fds.lock();+ if let Some(i) = fds.iter().position(|f| f.is_none()) {+ fds[i] = Some(file);+ return Ok(i as u64);+ }+ if fds.len() >= MAX_FDS {+ return Err(Errno::EMFILE);+ }+ fds.push(Some(file));+ Ok(fds.len() as u64 - 1)+ }++ pub fn close(&self, fd: u64) -> Result<(), Errno> {+ let mut fds = self.fds.lock();+ match fds.get_mut(fd as usize) {+ Some(slot @ Some(_)) => {+ *slot = None; // dropping the Arc closes the file+ Ok(())+ }+ _ => Err(Errno::EBADF),+ }+ }++ pub fn status(&self) -> Option<ExitStatus> {+ *self.status.lock()+ }+}++/// End the current process (exit syscall, or killed by an exception).+pub fn exit_current(status: ExitStatus) -> ! {+ if let Some(process) = thread::current_process() {+ process.fds.lock().clear(); // close all files+ *process.status.lock() = Some(status);+ match status {+ ExitStatus::Exited(code) => crate::kprintln!(+ "[process] pid {} ({}) exited with code {}",+ process.pid,+ process.name,+ code+ ),+ ExitStatus::Killed(why) => crate::kprintln!(+ "[process] pid {} ({}) killed: {}",+ process.pid,+ process.name,+ why+ ),+ }+ }+ thread::exit_current()+}++/// Block (the calling kernel thread) until `process` has ended.+pub fn wait(process: &Process) -> ExitStatus {+ loop {+ if let Some(status) = process.status() {+ return status;+ }+ x86_64::instructions::hlt(); // the timer lets the process run meanwhile+ }+}
@@ -0,0 +1,204 @@+//! System calls via the `syscall` / `sysret` instructions.+//!+//! ABI (like Linux x86_64): number in rax, arguments in rdi, rsi, rdx (r10,+//! r8 for 4th/5th), result in rax; negative results are `-errno`. The+//! `syscall` instruction itself clobbers rcx (return address) and r11 (saved+//! RFLAGS); every other register is preserved for the caller.+//!+//! | nr | name | arguments | note |+//! |----|-------------|--------------------------------|------|+//! | 0 | read | fd, buf, len | fd 0 blocks until a line is typed |+//! | 1 | write | fd, buf, len | fd 1/2 = console |+//! | 2 | open | path_ptr, path_len, flags | Linux takes a C string; we take ptr+len. flags: O_CREAT = 0o100 |+//! | 3 | close | fd | |+//! | 24 | sched_yield | | |+//! | 39 | getpid | | |+//! | 60 | exit | code | |+//!+//! What happens on `syscall` (the CPU does steps 1-3 in one instruction):+//! 1. rcx = user RIP (next instruction), r11 = user RFLAGS+//! 2. RFLAGS &= !SFMASK (we mask IF -> interrupts off)+//! 3. CS/SS = kernel selectors from STAR, RIP = LSTAR (`syscall_entry`), CPL = 0+//! ...but RSP is still the *user* stack! The CPU doesn't switch stacks for+//! `syscall` (unlike interrupts, which use TSS.RSP0). So `syscall_entry`+//! must do it first: stash the user RSP, load this thread's kernel stack,+//! save registers, re-enable interrupts and call `syscall_dispatch` in Rust.+//! `sysretq` reverses it: RIP = rcx, RFLAGS = r11, CPL = 3.+//!+//! Single CPU only: the "current kernel stack" and the user-RSP scratch slot+//! are plain globals. (Multi-core kernels use per-CPU data via `swapgs`.)+//!+//! Why not `int 0x80`? It also works (and is simpler: the CPU switches stacks+//! via TSS.RSP0 for us), but `syscall` is what Linux and every modern x86_64+//! OS use, and it's faster.++use crate::errno::Errno;+use crate::fs::{self, file::InodeFile};+use crate::{memory, process, thread};+use alloc::sync::Arc;+use core::arch::global_asm;+use core::sync::atomic::{AtomicU64, Ordering};++pub const SYS_READ: u64 = 0;+pub const SYS_WRITE: u64 = 1;+pub const SYS_OPEN: u64 = 2;+pub const SYS_CLOSE: u64 = 3;+pub const SYS_SCHED_YIELD: u64 = 24;+pub const SYS_GETPID: u64 = 39;+pub const SYS_EXIT: u64 = 60;++pub const O_CREAT: u64 = 0o100;++/// Kernel stack top of the running thread (set by the scheduler).+#[unsafe(no_mangle)]+static SYSCALL_KERNEL_RSP: AtomicU64 = AtomicU64::new(0);+/// Scratch slot for the user RSP during the first instructions of the entry.+#[unsafe(no_mangle)]+static SYSCALL_USER_RSP: AtomicU64 = AtomicU64::new(0);++/// Called by the scheduler when switching threads.+pub fn set_kernel_stack(stack_top: u64) {+ SYSCALL_KERNEL_RSP.store(stack_top, Ordering::Relaxed);+}++/// Program the MSRs that make `syscall` work.+pub fn init() {+ use x86_64::VirtAddr;+ use x86_64::registers::model_specific::{Efer, EferFlags, LStar, SFMask, Star};+ use x86_64::registers::rflags::RFlags;++ let s = crate::gdt::selectors();+ unsafe {+ // EFER.SCE: enable the syscall/sysret instructions at all.+ Efer::update(|flags| flags.insert(EferFlags::SYSTEM_CALL_EXTENSIONS));+ }+ // STAR: which segments syscall/sysret load (see gdt.rs for the layout rule).+ Star::write(s.user_code, s.user_data, s.kernel_code, s.kernel_data)+ .expect("GDT layout doesn't fit syscall/sysret");+ // LSTAR: where `syscall` jumps to.+ LStar::write(VirtAddr::new(syscall_entry as *const () as u64));+ // SFMASK: RFLAGS bits to clear on entry. IF: no interrupts until we're on+ // the kernel stack. DF: Rust expects the direction flag clear. TF: no+ // single-stepping into the kernel.+ SFMask::write(RFlags::INTERRUPT_FLAG | RFlags::DIRECTION_FLAG | RFlags::TRAP_FLAG);+}++unsafe extern "C" {+ fn syscall_entry();+}++global_asm!(+ ".global syscall_entry",+ "syscall_entry:",+ // Interrupts are off (SFMASK). Switch to this thread's kernel stack.+ " mov [rip + SYSCALL_USER_RSP], rsp",+ " mov rsp, [rip + SYSCALL_KERNEL_RSP]",+ " push qword ptr [rip + SYSCALL_USER_RSP]", // user rsp+ " push rcx", // user rip+ " push r11", // user rflags+ // Preserve the argument registers for the user (Rust may clobber them).+ // rbx, rbp, r12-r15 are callee-saved: the Rust code keeps them intact.+ " push rdi",+ " push rsi",+ " push rdx",+ " push r8",+ " push r9",+ " push r10",+ " sub rsp, 8", // 10 pushes total -> stack 16-byte aligned for the call+ // syscall_dispatch(nr, a1, a2, a3, a4, a5) in the C calling convention:+ // shuffle rax, rdi, rsi, rdx, r10, r8 -> rdi, rsi, rdx, rcx, r8, r9.+ " mov r9, r8",+ " mov r8, r10",+ " mov rcx, rdx",+ " mov rdx, rsi",+ " mov rsi, rdi",+ " mov rdi, rax",+ " sti", // we're on our own stack now: preemption is fine+ " call {dispatch}", // result in rax+ " cli", // no interrupts while we're back on the user stack in ring 0+ " add rsp, 8",+ " pop r10",+ " pop r9",+ " pop r8",+ " pop rdx",+ " pop rsi",+ " pop rdi",+ " pop r11",+ " pop rcx",+ " pop rsp",+ // (rcx came from the `syscall` itself, so it's a canonical address;+ // sysret with a non-canonical rcx would fault *in ring 0* -- a classic bug.)+ " sysretq",+ dispatch = sym syscall_dispatch,+);++/// Rust side of every system call. Returns the value for rax.+extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, _a4: u64, _a5: u64) -> i64 {+ let result = match nr {+ SYS_READ => sys_read(a1, a2, a3),+ SYS_WRITE => sys_write(a1, a2, a3),+ SYS_OPEN => sys_open(a1, a2, a3),+ SYS_CLOSE => sys_close(a1),+ SYS_SCHED_YIELD => {+ thread::yield_now();+ Ok(0)+ }+ SYS_GETPID => current().map(|p| p.pid),+ SYS_EXIT => process::exit_current(process::ExitStatus::Exited(a1 as i64)),+ _ => Err(Errno::ENOSYS),+ };+ match result {+ Ok(value) => value as i64,+ Err(errno) => -(errno as i64),+ }+}++fn current() -> Result<Arc<process::Process>, Errno> {+ thread::current_process().ok_or(Errno::EINVAL)+}++/// Turn a user pointer + length into a slice, after checking that the user+/// may really access that memory. Never trust pointers from user space!+pub fn user_slice(ptr: u64, len: u64) -> Result<&'static [u8], Errno> {+ if !memory::user_range_ok(ptr, len, false) {+ return Err(Errno::EFAULT);+ }+ Ok(unsafe { core::slice::from_raw_parts(ptr as *const u8, len as usize) })+}++/// Like `user_slice`, but the user must be allowed to *write* the memory.+pub fn user_slice_mut(ptr: u64, len: u64) -> Result<&'static mut [u8], Errno> {+ if !memory::user_range_ok(ptr, len, true) {+ return Err(Errno::EFAULT);+ }+ Ok(unsafe { core::slice::from_raw_parts_mut(ptr as *mut u8, len as usize) })+}++fn sys_read(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> {+ let file = current()?.file(fd)?; // clone the Arc: no lock held while we block+ let buf = user_slice_mut(buf, len)?;+ file.read(buf).map(|n| n as u64)+}++fn sys_write(fd: u64, buf: u64, len: u64) -> Result<u64, Errno> {+ let file = current()?.file(fd)?;+ let buf = user_slice(buf, len)?;+ file.write(buf).map(|n| n as u64)+}++fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, Errno> {+ if path_len > 255 {+ return Err(Errno::ENAMETOOLONG);+ }+ let path = core::str::from_utf8(user_slice(path_ptr, path_len)?).map_err(|_| Errno::EINVAL)?;+ let inode = match fs::lookup(path) {+ Err(Errno::ENOENT) if flags & O_CREAT != 0 => fs::create(path, fs::InodeKind::File)?,+ other => other?,+ };+ let file = Arc::new(InodeFile::open(inode)?);+ current()?.add_file(file)+}++fn sys_close(fd: u64) -> Result<u64, Errno> {+ current()?.close(fd).map(|_| 0)+}
@@ -6,7 +6,7 @@ //! (`print_keypresses`), outside interrupt context. That keeps the handler //! short and means it never takes a lock (no deadlock risk). -use crate::{print, serial_print, vga_buffer};+use crate::{print, serial_print}; use conquer_once::spin::OnceCell; use core::{ pin::Pin,@@ -91,17 +91,10 @@ pub async fn print_keypresses() { && let Some(key) = keyboard.process_keyevent(key_event) { match key {- // Backspace: erase the previous character on screen. On- // serial, the uart_16550 crate already turns a 0x08 byte into- // "\x08 \x08" (back, overwrite with space, back) for us.- DecodedKey::Unicode('\u{8}') => {- vga_buffer::backspace();- serial_print!("\x08");- }- DecodedKey::Unicode(character) => {- print!("{}", character);- serial_print!("{}", character);- }+ // Characters (incl. Backspace and Enter) go to the tty's+ // line discipline, which echoes them and buffers the line+ // for user programs reading fd 0.+ DecodedKey::Unicode(character) => crate::tty::input_char(character), // Modifiers only change how other keys decode: print nothing. DecodedKey::RawKey( KeyCode::LShift | KeyCode::RShift | KeyCode::LControl | KeyCode::RControl
@@ -21,15 +21,24 @@ //! So a sleeping thread's whole CPU state is simply sitting on its own stack, //! and all we have to remember per thread is its stack pointer. //!+//! Threads that run user code (processes, see process.rs) additionally carry+//! an address space (CR3) and a kernel stack top. On every switch the+//! scheduler loads the next thread's CR3 and points the TSS's RSP0 and the+//! syscall entry at its kernel stack, so interrupts and syscalls from ring 3+//! land on the right stack.+//! //! A *new* thread has never been interrupted, so `spawn` builds a fake stack //! that looks like `switch_context` saved it, with the "return address" //! pointing at `thread_trampoline`, which enables interrupts and calls the //! thread's function. -use alloc::{boxed::Box, vec, vec::Vec};+use crate::process::Process;+use alloc::{boxed::Box, sync::Arc, vec, vec::Vec}; use core::arch::global_asm; use spin::Mutex; use x86_64::instructions::interrupts;+use x86_64::registers::control::Cr3;+use x86_64::structures::paging::PhysFrame; /// 16 KiB per thread stack (no guard page! a deep recursion in a thread /// silently overwrites whatever is below its stack on the heap).@@ -41,6 +50,13 @@ struct Thread { rsp: u64, /// The thread's stack. `None` for the boot thread (uses the bootloader's stack). _stack: Option<Vec<u8>>,+ /// Top of that stack (0 for the boot thread). Becomes TSS.RSP0 and the+ /// syscall stack while this thread runs.+ kernel_stack_top: u64,+ /// Level-4 page table to run with (the kernel's, or a process's own).+ cr3: PhysFrame,+ /// The process this thread belongs to (None = pure kernel thread).+ process: Option<Arc<Process>>, finished: bool, } @@ -55,6 +71,12 @@ static SCHEDULER: Mutex<Scheduler> = Mutex::new(Scheduler { threads: Vec::new(), /// Start a new kernel thread running `entry`. Needs the heap. pub fn spawn(name: &'static str, entry: fn()) {+ spawn_with(name, entry, crate::memory::kernel_l4_frame(), None);+}++/// Start a thread for `process`: runs `entry` (in the kernel) with the+/// process's address space `cr3` active; `entry` then drops to ring 3.+pub fn spawn_with(name: &'static str, entry: fn(), cr3: PhysFrame, process: Option<Arc<Process>>) { let mut stack = vec![0u8; STACK_SIZE]; // Build the initial stack, top down (stacks grow towards lower addresses):@@ -74,7 +96,15 @@ pub fn spawn(name: &'static str, entry: fn()) { let rsp = top - 8 * initial.len() as u64; unsafe { core::ptr::copy_nonoverlapping(initial.as_ptr(), rsp as *mut u64, initial.len()) }; - let thread = Box::new(Thread { name, rsp, _stack: Some(stack), finished: false });+ let thread = Box::new(Thread {+ name,+ rsp,+ _stack: Some(stack),+ kernel_stack_top: top,+ cr3,+ process,+ finished: false,+ }); // The timer handler also takes this lock, so keep interrupts off while we // hold it -- otherwise the handler would spin forever on our lock.@@ -88,6 +118,9 @@ pub fn spawn(name: &'static str, entry: fn()) { name: "main", rsp: 0, _stack: None,+ kernel_stack_top: 0,+ cr3: Cr3::read().0,+ process: None, finished: false, })); }@@ -113,6 +146,20 @@ pub fn schedule() { _ => return, // nobody else to run }; s.current = next;++ // Prepare the CPU for the next thread *before* switching stacks.+ let t = &s.threads[next];+ if t.kernel_stack_top != 0 {+ crate::gdt::set_kernel_stack(t.kernel_stack_top); // interrupts from ring 3+ crate::syscall::set_kernel_stack(t.kernel_stack_top); // `syscall` from ring 3+ }+ if Cr3::read().0 != t.cr3 {+ // Switch address space. Safe for the code running right now:+ // all kernel mappings (incl. every kernel stack) are identical in+ // every address space. Writing CR3 also flushes the TLB.+ unsafe { Cr3::write(t.cr3, Cr3::read().1) };+ }+ let old_rsp: *mut u64 = &mut s.threads[current].rsp; (old_rsp, s.threads[next].rsp) // The lock is released here, *before* switching: the next thread@@ -121,6 +168,32 @@ pub fn schedule() { unsafe { switch_context(old_rsp, new_rsp) }; } +/// Give up the rest of this time slice (also callable outside interrupts).+pub fn yield_now() {+ interrupts::without_interrupts(schedule);+}++/// The process the running thread belongs to, if any.+pub fn current_process() -> Option<Arc<Process>> {+ interrupts::without_interrupts(|| {+ let s = SCHEDULER.lock();+ s.threads.get(s.current).and_then(|t| t.process.clone())+ })+}++/// End the running thread: mark it finished and switch away for good.+/// (Its stack is kept: we're still running on it until the switch.)+pub fn exit_current() -> ! {+ interrupts::disable();+ {+ let mut s = SCHEDULER.lock();+ let current = s.current;+ s.threads[current].finished = true;+ }+ schedule(); // never returns: finished threads aren't picked again+ crate::hlt_loop(); // only reached if nothing else exists to run+}+ /// Name of the running thread (for printing). pub fn current_name() -> &'static str { interrupts::without_interrupts(|| {@@ -189,13 +262,6 @@ extern "C" fn thread_start(entry: usize) -> ! { let entry: fn() = unsafe { core::mem::transmute(entry) }; entry(); - // The thread function returned: mark it finished so the scheduler skips- // it, then idle until we're switched away for good. (Its stack is kept;- // freeing it while we still run on it would be a bad idea.)- interrupts::without_interrupts(|| {- let mut s = SCHEDULER.lock();- let current = s.current;- s.threads[current].finished = true;- });- crate::hlt_loop();+ // The thread function returned: mark it finished so the scheduler skips it.+ exit_current(); }
@@ -0,0 +1,95 @@+//! The console "tty": keyboard input with a tiny line discipline, and the+//! `File` behind file descriptors 0, 1 and 2.+//!+//! Like a Linux terminal in canonical mode: typed characters are echoed and+//! collected in a line buffer (Backspace edits it), and only when Enter is+//! pressed does the whole line become readable from fd 0. So a user program's+//! `read(0, ...)` gets a complete, already-edited line.++use crate::errno::Errno;+use crate::fs::file::File;+use crate::{print, serial_print, vga_buffer};+use alloc::string::String;+use conquer_once::spin::OnceCell;+use crossbeam_queue::ArrayQueue;+use spin::Mutex;++/// The line being typed (not yet readable).+static LINE: Mutex<String> = Mutex::new(String::new());+/// Finished lines, as bytes, waiting for `read(0, ...)`.+static INPUT: OnceCell<ArrayQueue<u8>> = OnceCell::uninit();++fn input() -> &'static ArrayQueue<u8> {+ INPUT.get_or_init(|| ArrayQueue::new(4096))+}++/// Called by the keyboard task for every typed character.+pub fn input_char(c: char) {+ let mut line = LINE.lock();+ match c {+ '\u{8}' => {+ // Backspace: only erase what's in the current line.+ if line.pop().is_some() {+ vga_buffer::backspace();+ serial_print!("\x08"); // uart_16550 turns this into "\x08 \x08"+ }+ }+ '\n' => {+ print!("\n");+ serial_print!("\n");+ for b in line.bytes().chain(Some(b'\n')) {+ let _ = input().push(b); // queue full: drop (nobody is reading)+ }+ line.clear();+ }+ c => {+ print!("{}", c);+ serial_print!("{}", c);+ line.push(c);+ }+ }+}++/// The console as a file: reading gets keyboard lines, writing prints to the+/// screen and serial port.+pub struct Console;++impl File for Console {+ /// Blocks until a line is available, then returns up to `buf.len()`+ /// bytes of it (stopping after the '\n').+ fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> {+ if buf.is_empty() {+ return Ok(0);+ }+ // Sleep until a line arrives. Syscalls run with interrupts enabled,+ // so `hlt` wakes on the next interrupt (and the timer may switch to+ // other threads meanwhile). Lines are queued whole, so once the+ // first byte is there, the rest of the line is too.+ while input().is_empty() {+ x86_64::instructions::hlt();+ }+ let mut n = 0;+ while n < buf.len() {+ let Some(b) = input().pop() else { break };+ buf[n] = b;+ n += 1;+ if b == b'\n' {+ break;+ }+ }+ Ok(n)+ }++ fn write(&self, buf: &[u8]) -> Result<usize, Errno> {+ // Print valid UTF-8 as is, anything else as '?'.+ for chunk in buf.utf8_chunks() {+ print!("{}", chunk.valid());+ serial_print!("{}", chunk.valid());+ if !chunk.invalid().is_empty() {+ print!("?");+ serial_print!("?");+ }+ }+ Ok(buf.len())+ }+}
@@ -0,0 +1,95 @@+//! User mode: real ring-3 programs (user/*.rs) run as processes, make+//! syscalls, get killed when they misbehave, and get preempted when they hog+//! the CPU. Plus the ELF loader and user-pointer checks.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rust_kernel::test_runner)]+#![reexport_test_harness_main = "test_main"]++use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use core::sync::atomic::Ordering;+use rust_kernel::errno::Errno;+use rust_kernel::interrupts::TICKS;+use rust_kernel::memory::{self, BootInfoFrameAllocator, USER_START};+use rust_kernel::process::{self, ExitStatus};+use rust_kernel::{allocator, elf, fs, syscall, user_programs};+use x86_64::VirtAddr;++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+ rust_kernel::init();+ let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);+ let mut mapper = unsafe { memory::init(phys_mem_offset) };+ let mut frame_allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };+ allocator::init_heap(&mut mapper, &mut frame_allocator).expect("heap initialization failed");+ memory::init_frame_allocator(frame_allocator);+ fs::init();++ test_main();+ rust_kernel::hlt_loop();+}++/// hello does ~10 syscalls (open/read/write/close on the ramfs, error cases)+/// and exits with the number of failed checks.+#[test_case]+fn hello_program_passes_its_syscall_checks() {+ let p = process::spawn("hello", user_programs::HELLO).expect("spawn hello");+ assert_eq!(process::wait(&p), ExitStatus::Exited(0));+ // It created /tmp/note.txt through the syscalls; the kernel sees it too.+ let inode = fs::lookup("/tmp/note.txt").expect("hello's file is missing");+ assert_eq!(inode.size(), 19);+}++/// evil reads kernel memory -> page fault in ring 3 -> killed. We're still here.+#[test_case]+fn kernel_memory_access_kills_only_the_process() {+ let p = process::spawn("evil", user_programs::EVIL).expect("spawn evil");+ assert_eq!(process::wait(&p), ExitStatus::Killed("page fault"));+}++/// spin never makes a syscall while it loops. If preemption didn't work,+/// this kernel thread couldn't run again until spin finished.+#[test_case]+fn spinning_process_is_preempted() {+ let p = process::spawn("spin", user_programs::SPIN).expect("spawn spin");+ let start = TICKS.load(Ordering::Relaxed);+ let mut samples_while_spinning = 0;+ while p.status().is_none() {+ // Busy-wait (no hlt): we only get here again if the timer took the+ // CPU away from spin and gave it back to us.+ if TICKS.load(Ordering::Relaxed) > start + 3 {+ samples_while_spinning += 1;+ }+ assert!(TICKS.load(Ordering::Relaxed) < start + 18 * 30, "spin didn't finish");+ core::hint::spin_loop();+ }+ assert_eq!(p.status(), Some(ExitStatus::Exited(0)));+ assert!(samples_while_spinning > 0, "never ran while spin was running");+}++#[test_case]+fn elf_loader_rejects_garbage() {+ assert_eq!(elf::check(&[b'x'; 128]), Err(elf::ElfError::NotElf));+ assert_eq!(elf::check(b"\x7fELF"), Err(elf::ElfError::TooShort));+ let entry = elf::check(user_programs::HELLO).expect("hello should parse");+ assert!(entry >= USER_START);+ // A truncated program: the header is fine, the program headers are cut off.+ assert!(elf::check(&user_programs::HELLO[..70]).is_err());+}++#[test_case]+fn user_pointers_are_validated() {+ // Kernel addresses, unmapped user addresses and overflowing ranges all fail.+ assert_eq!(syscall::user_slice(0x20_0000, 16).err(), Some(Errno::EFAULT));+ assert_eq!(syscall::user_slice(USER_START, 16).err(), Some(Errno::EFAULT));+ assert_eq!(syscall::user_slice(u64::MAX - 4, 16).err(), Some(Errno::EFAULT));+ assert!(!memory::user_range_ok(USER_START - 8, 16, false));+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rust_kernel::test_panic_handler(info)+}
@@ -0,0 +1,85 @@+//! VFS + ramfs, used directly from the kernel (no user mode involved).+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rust_kernel::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::{string::String, vec};+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rust_kernel::errno::Errno;+use rust_kernel::fs::file::{File, InodeFile};+use rust_kernel::fs::{self, InodeKind, ramfs::RamFs};+use rust_kernel::memory::{self, BootInfoFrameAllocator};+use rust_kernel::allocator;+use x86_64::VirtAddr;++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+ rust_kernel::init();+ let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);+ let mut mapper = unsafe { memory::init(phys_mem_offset) };+ let mut frame_allocator = unsafe { BootInfoFrameAllocator::init(&boot_info.memory_map) };+ allocator::init_heap(&mut mapper, &mut frame_allocator).expect("heap initialization failed");+ fs::init();++ test_main();+ rust_kernel::hlt_loop();+}++#[test_case]+fn read_boot_files() {+ let file = InodeFile::open(fs::lookup("/hello.txt").unwrap()).unwrap();+ let mut buf = [0u8; 64];+ let n = file.read(&mut buf).unwrap();+ assert!(buf[..n].starts_with(b"Hello from the ramfs!"));+ assert_eq!(file.read(&mut buf), Ok(0)); // EOF+ assert_eq!(fs::lookup("/etc/motd").unwrap().kind(), InodeKind::File);+ assert_eq!(fs::lookup("/etc").unwrap().kind(), InodeKind::Directory);+ assert_eq!(fs::lookup("//etc/./motd").unwrap().kind(), InodeKind::File);+}++#[test_case]+fn lookup_errors() {+ assert_eq!(fs::lookup("/nope").err(), Some(Errno::ENOENT));+ assert_eq!(fs::lookup("/hello.txt/x").err(), Some(Errno::ENOTDIR));+ assert_eq!(fs::lookup("relative").err(), Some(Errno::EINVAL));+ assert_eq!(InodeFile::open(fs::lookup("/etc").unwrap()).err(), Some(Errno::EISDIR));+}++#[test_case]+fn mounts_are_separate_file_systems() {+ // /tmp is its own ramfs: files created there don't show up in "/"'s tmp dir.+ fs::create("/tmp/a.txt", InodeKind::File).unwrap();+ assert!(fs::lookup("/tmp/a.txt").is_ok());+ let root_tmp_dir = fs::lookup("/").unwrap().lookup("tmp").unwrap();+ assert_eq!(root_tmp_dir.list().unwrap().len(), 0);+ assert_eq!(fs::create("/tmp/a.txt", InodeKind::File).err(), Some(Errno::EEXIST));++ // Mount a third one; the longest matching mount point wins.+ fs::create("/tmp/deeper", InodeKind::Directory).unwrap();+ fs::mount("/tmp/deeper", RamFs::new()).unwrap();+ assert_eq!(fs::lookup("/tmp/deeper").unwrap().list().unwrap().len(), 0);+ assert_eq!(fs::mount("/tmp/deeper", RamFs::new()).err(), Some(Errno::EEXIST));+ assert_eq!(fs::mounts().len(), 3);+}++#[test_case]+fn write_grow_and_list() {+ let inode = fs::create("/tmp/grow.bin", InodeKind::File).unwrap();+ inode.write_at(4, b"abc").unwrap(); // gap is filled with zeros+ let mut buf = vec![0xffu8; 10];+ assert_eq!(inode.read_at(0, &mut buf), Ok(7));+ assert_eq!(&buf[..7], b"\0\0\0\0abc");+ let names = fs::lookup("/").unwrap().list().unwrap();+ assert_eq!(names, vec![String::from("etc"), String::from("hello.txt"), String::from("tmp")]);+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rust_kernel::test_panic_handler(info)+}
@@ -0,0 +1,23 @@+//! Reads lines from the keyboard (fd 0) and echoes them back. "exit" quits.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+ println!("[echo] pid {}: type a line and press Enter", ulib::getpid());+ let mut buf = [0u8; 256];+ loop {+ let n = ulib::read(0, &mut buf); // blocks until Enter+ if n <= 0 {+ return 1;+ }+ let line = core::str::from_utf8(&buf[..n as usize]).unwrap_or("?").trim_end();+ println!("[echo] you typed: {}", line);+ if line == "exit" {+ return 0;+ }+ }+}
@@ -0,0 +1,17 @@+//! A misbehaving program: tries to read kernel memory directly. The kernel's+//! pages aren't USER_ACCESSIBLE, so the CPU raises a page fault, and the+//! kernel kills this process (and carries on).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+ let kernel_code = 0x20_0000 as *const u64; // where the kernel is loaded+ println!("[evil] pid {}: reading kernel memory at {:p}...", ulib::getpid(), kernel_code);+ let value = unsafe { core::ptr::read_volatile(kernel_code) };+ println!("[evil] got {:#x} -- this must never be printed!", value);+ 1+}
@@ -0,0 +1,57 @@+//! First user program: says hello, uses the VFS through syscalls, and checks+//! that bad syscall arguments get the right errors. Exit code = failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn check(failures: &mut i64, ok: bool, what: &str) {+ if !ok {+ println!("[hello] CHECK FAILED: {}", what);+ *failures += 1;+ }+}++fn main() -> i64 {+ let mut failures = 0;+ println!("Hello from user mode! I'm pid {}.", getpid());++ // Read a file from the ramfs.+ let mut buf = [0u8; 128];+ let fd = open("/hello.txt", 0);+ check(&mut failures, fd >= 3, "open /hello.txt");+ let n = read(fd, &mut buf);+ let text = core::str::from_utf8(&buf[..n.max(0) as usize]).unwrap_or("<not utf-8>");+ print!("[hello] /hello.txt says: {}", text);+ check(&mut failures, text.starts_with("Hello from the ramfs!"), "contents of /hello.txt");+ check(&mut failures, read(fd, &mut buf) == 0, "read at EOF returns 0");+ check(&mut failures, close(fd) == 0, "close");++ // Create a file on the second mount (/tmp), then read it back.+ let fd = open("/tmp/note.txt", O_CREAT);+ check(&mut failures, write(fd, b"written from ring 3") == 19, "write /tmp/note.txt");+ close(fd);+ let fd = open("/tmp/note.txt", 0);+ let n = read(fd, &mut buf);+ check(&mut failures, &buf[..n.max(0) as usize] == b"written from ring 3", "read back /tmp/note.txt");+ close(fd);+ println!("[hello] wrote and read back /tmp/note.txt");++ // Error cases.+ check(&mut failures, open("/does/not/exist", 0) == -ENOENT, "ENOENT");+ check(&mut failures, read(42, &mut buf) == -EBADF, "EBADF");+ check(&mut failures, close(fd) == -EBADF, "double close -> EBADF");+ // Ask the kernel to print *kernel* memory for us (0x200000 = kernel code).+ let r = syscall3(SYS_WRITE, 1, 0x20_0000, 16);+ check(&mut failures, r == -EFAULT, "kernel pointer -> EFAULT");+ check(&mut failures, syscall3(999, 0, 0, 0) == -38, "unknown syscall -> ENOSYS");++ if failures == 0 {+ println!("[hello] all syscall checks passed, exiting with 0");+ }+ failures+}
@@ -0,0 +1,23 @@+/* Linker script for user programs: a static executable loaded at 4 MiB into+ user space (USER_START = 0x80_0000_0000, see src/memory.rs). Each part+ starts on its own page so it can get its own page permissions:+ code = read+execute, rodata = read-only, data/bss = read+write.+ The `large` code model (see build.rs) names its sections .ltext, .lrodata,+ .ldata and .lbss, so those must be listed too -- otherwise the linker+ places them wherever it likes (e.g. code in a non-executable segment). */+ENTRY(_start)++PHDRS {+ text PT_LOAD FLAGS(5); /* R-X */+ rodata PT_LOAD FLAGS(4); /* R-- */+ data PT_LOAD FLAGS(6); /* RW- */+}++SECTIONS {+ . = 0x8000400000;+ .text : ALIGN(4K) { *(.text .text.* .ltext .ltext.*) } :text+ .rodata : ALIGN(4K) { *(.rodata .rodata.* .lrodata .lrodata.*) } :rodata+ .data : ALIGN(4K) { *(.data .data.* .ldata .ldata.* .got .got.*) } :data+ .bss : ALIGN(8) { *(.bss .bss.* .lbss .lbss.* COMMON) } :data+ /DISCARD/ : { *(.comment) *(.eh_frame*) *(.note*) }+}
@@ -0,0 +1,20 @@+//! A CPU hog: busy-loops without ever making a syscall in between. The timer+//! interrupt still preempts it, so everything else keeps running.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++fn main() -> i64 {+ println!("[spin] pid {}: spinning without syscalls...", ulib::getpid());+ let mut count: u64 = 0;+ for round in 1..=3 {+ for _ in 0..30_000_000 {+ count = core::hint::black_box(count + 1);+ }+ println!("[spin] round {} done (count = {})", round, count);+ }+ 0+}
@@ -0,0 +1,105 @@+//! A tiny "libc" for user programs: syscall wrappers, print!/println!,+//! the `_start` entry point and a panic handler.+//!+//! Each user program includes it with+//! #[macro_use] #[path = "ulib.rs"] mod ulib;+//! and defines `fn main() -> i64` (the exit code).+//!+//! User programs are built for the `x86_64-unknown-none` target, which has+//! SSE/floating point disabled. That matters: the kernel doesn't save SSE+//! registers on a context switch.++#![allow(dead_code)]++use core::arch::asm;+use core::fmt;++pub const SYS_READ: u64 = 0;+pub const SYS_WRITE: u64 = 1;+pub const SYS_OPEN: u64 = 2;+pub const SYS_CLOSE: u64 = 3;+pub const SYS_SCHED_YIELD: u64 = 24;+pub const SYS_GETPID: u64 = 39;+pub const SYS_EXIT: u64 = 60;++pub const O_CREAT: u64 = 0o100;+pub const ENOENT: i64 = 2;+pub const EBADF: i64 = 9;+pub const EFAULT: i64 = 14;++/// Raw system call: number in rax, args in rdi/rsi/rdx, result in rax.+/// The `syscall` instruction overwrites rcx and r11.+pub fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 {+ let ret: i64;+ unsafe {+ asm!(+ "syscall",+ inlateout("rax") nr as i64 => ret,+ in("rdi") a1,+ in("rsi") a2,+ in("rdx") a3,+ lateout("rcx") _,+ lateout("r11") _,+ options(nostack),+ );+ }+ ret+}++pub fn read(fd: i64, buf: &mut [u8]) -> i64 {+ syscall3(SYS_READ, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64)+}+pub fn write(fd: i64, buf: &[u8]) -> i64 {+ syscall3(SYS_WRITE, fd as u64, buf.as_ptr() as u64, buf.len() as u64)+}+pub fn open(path: &str, flags: u64) -> i64 {+ syscall3(SYS_OPEN, path.as_ptr() as u64, path.len() as u64, flags)+}+pub fn close(fd: i64) -> i64 {+ syscall3(SYS_CLOSE, fd as u64, 0, 0)+}+pub fn sched_yield() {+ syscall3(SYS_SCHED_YIELD, 0, 0, 0);+}+pub fn getpid() -> i64 {+ syscall3(SYS_GETPID, 0, 0, 0)+}+pub fn exit(code: i64) -> ! {+ syscall3(SYS_EXIT, code as u64, 0, 0);+ loop {} // not reached+}++pub struct Stdout;++impl fmt::Write for Stdout {+ fn write_str(&mut self, s: &str) -> fmt::Result {+ write(1, s.as_bytes());+ Ok(())+ }+}++macro_rules! print {+ ($($arg:tt)*) => {{+ use core::fmt::Write;+ let _ = write!($crate::ulib::Stdout, $($arg)*);+ }};+}++macro_rules! println {+ ($($arg:tt)*) => {{+ use core::fmt::Write;+ let _ = writeln!($crate::ulib::Stdout, $($arg)*);+ }};+}++/// The kernel jumps here (see process.rs). Run main, exit with its result.+#[unsafe(no_mangle)]+pub extern "C" fn _start() -> ! {+ exit(crate::main())+}++#[panic_handler]+fn panic(info: &core::panic::PanicInfo) -> ! {+ println!("[user panic] {}", info);+ exit(101)+}