rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 01:22:29 -0700 |
|---|---|---|
| commit | cd21aabc566c397fcafc3de71454a81b47f7656f | |
| parent | 20756a688a45abfec809ec6ee710c35307350834 | |
| tree | browse files at this commit | |
Stage 7: Rustbelt shell - /bin/sh as a ring-3 program: raw-mode line editor (Backspace, history), builtins (cd pwd echo help exit cls/clear ver alias drives history jobs wait), run / run --trace / start / &, PATH over each drive's bin, multi-stage pipes, < > >>, DOS aliases (dir type del md copy), drive letters - new programs: ls cat wc grep mkdir rm touch cp sleep (echo removed) - kernel: ioctl raw tty, uname, mounts, wait WNOHANG, O_APPEND, process memory freed on reap (frame free list), strace-style trace output, console \x08 / \x0c, ELF loader rejects shared pages with mixed flags - fixes: pc-keyboard Oem7 -> Oem5 so the | key works; user link.ld aligns before .data so .bss never lands on the .rodata page - boot runs the shell (respawned on exit); old demos behind --features demos; headless boot runs a scripted shell session as the self-test - tests/shell.rs (parser, drives, scripted session, jobs, memory reuse) - kernel named Rustbelt 0.7.0; README shell section; new boot.png
| M | Cargo.lock | +1 -1 |
| M | Cargo.toml | +8 -4 |
| M | README.md | +423 -159 |
| M | boot.png | +0 -0 |
| M | build.rs | +6 -1 |
| M | disk/notes/todo.txt | +2 -2 |
| M | disk/readme.txt | +1 -0 |
| A | disk/scripts/selftest.sh | +32 -0 |
| M | scripts/run-headless.sh | +38 -4 |
| M | scripts/screenshot.sh | +48 -26 |
| M | src/elf.rs | +13 -0 |
| M | src/errno.rs | +15 -0 |
| M | src/fs/file.rs | +10 -1 |
| M | src/interrupts.rs | +4 -13 |
| M | src/main.rs | +108 -45 |
| M | src/memory.rs | +79 -4 |
| M | src/process.rs | +50 -8 |
| M | src/syscall.rs | +106 -5 |
| M | src/task/keyboard.rs | +13 -7 |
| M | src/tty.rs | +106 -24 |
| M | src/vga_buffer.rs | +12 -0 |
| M | tests/fat.rs | +1 -1 |
| A | tests/shell.rs | +218 -0 |
| M | tests/user_mode.rs | +15 -1 |
| A | user/cat.rs | +41 -0 |
| A | user/cp.rs | +52 -0 |
| M | user/demo.rs | +1 -1 |
| D | user/echo.rs | +0 -23 |
| A | user/grep.rs | +85 -0 |
| M | user/link.ld | +6 -1 |
| A | user/ls.rs | +126 -0 |
| A | user/mkdir.rs | +26 -0 |
| A | user/rm.rs | +30 -0 |
| A | user/sh.rs | +554 -0 |
| A | user/shparse.rs | +236 -0 |
| A | user/sleep.rs | +28 -0 |
| A | user/touch.rs | +25 -0 |
| M | user/ulib.rs | +206 -2 |
| A | user/wc.rs | +83 -0 |
39 files changed, 2808 insertions, 333 deletions
@@ -160,7 +160,7 @@ dependencies = [ [[package]] name = "rust_kernel"-version = "0.1.0"+version = "0.7.0" dependencies = [ "bootloader", "conquer-once",
@@ -1,6 +1,6 @@ [package] name = "rust_kernel"-version = "0.1.0"+version = "0.7.0" edition = "2024" [dependencies]@@ -33,10 +33,14 @@ version = "1.0" features = ["spin_no_std"] [features]-# Build with `--features headless` to make the kernel shut QEMU down after-# ~5 seconds of timer ticks (used by scripts/run-headless.sh). Without it the-# kernel just halts forever so you can look at the QEMU window.+# Build with `--features headless` to make the shell run the self-test+# script (disk/scripts/selftest.sh) instead of reading the keyboard, and shut+# QEMU down when it exits (used by scripts/run-headless.sh). headless = []+# Build with `--features demos` to see the earlier stages' demos at boot:+# breakpoint, paging/heap printouts, threads A/B/C, async tasks and the+# hello/evil/spin/demo programs (before the shell starts).+demos = [] # Build with `--features stack-overflow` to deliberately overflow the kernel # stack after boot, to check the double-fault handler (GDT/TSS/IST) catches it. stack-overflow = []
@@ -1,31 +1,44 @@-# rust-kernel--A tiny hobby x86_64 kernel written in Rust, following the 2nd edition of-Philipp Oppermann's [Writing an OS in Rust](https://os.phil-opp.com/).-Right now it boots in QEMU, prints to the VGA text screen and the serial port-(COM1), handles CPU exceptions (breakpoint, page fault, double fault on its own-stack), runs the timer interrupt (uptime clock top-right), echoes what you-type on the PS/2 keyboard, walks and edits its own page tables, and has a-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`-(about 20 calls, numbered like Linux: `read`, `write`, `open`, `stat`, `lseek`,-`getdents64`, `chdir`, `pipe`, plus our own `spawn` and `wait`...). Files live-behind a Linux-style **VFS**: an in-memory ramfs at `/` and `/tmp`, and a-**FAT32** partition on a second **virtio-blk** disk (found by **PCI**-enumeration, **GPT/MBR** partitions parsed) mounted at `/mnt/disk0`. The user-programs themselves are loaded from that disk.-It has a `cargo test` suite that runs inside QEMU.+# Rustbelt++**Rustbelt** is Tyler's hobby x86_64 kernel written in Rust. It started from+the 2nd edition of Philipp Oppermann's+[Writing an OS in Rust](https://os.phil-opp.com/) and keeps going past the end+of it. It boots in QEMU straight into **its own shell**: a user-mode program+that has a Unix feel with DOS touches. It offers `ls`, `cat`, pipes and+redirection, plus `dir`, `type`, `ver`, and drive letters like `C:` and `D:`.++Under the shell:+- **VGA text and serial output.** The kernel prints to the VGA text screen and+ to the serial port (COM1).+- **CPU exceptions.** It handles breakpoints, page faults, and double faults+ (on their own stack).+- **Timer and keyboard.** The timer drives an uptime clock and preemption. The+ PS/2 keyboard feeds a tty with cooked and raw modes.+- **Memory.** It walks and edits its own page tables. It has a kernel heap+ with a fixed-size-block allocator.+- **Multitasking.** It multitasks two ways: an **async/await executor** and+ **preemptive kernel threads**.+- **User programs.** Small Rust ELF binaries run in **ring 3** as processes,+ each with its own address space. Their memory is freed when they exit. They+ talk to the kernel through about 25 `syscall`s, numbered like Linux's:+ `read`, `write`, `open`, `stat`, `getdents64`, `pipe`, `uname`... Two are+ our own: `spawn` and `wait`.+- **Files.** Files live behind a Linux-style **VFS**: a ramfs at `/` and+ `/tmp`, and a **FAT32** partition mounted at `/mnt/disk0`. That partition is+ on a second **virtio-blk** disk, found by **PCI** enumeration with its+ **GPT/MBR** partitions parsed. The shell and its programs are loaded from+ that disk.++It has a `cargo test` suite that runs inside QEMU, and a headless self-test+that drives the shell with a script. 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 everything past chapter 12: preemptive threads, user mode,-syscalls, ELF loading, the VFS, PCI, virtio-blk, partitions and FAT32.+comments. Everything else is new: the serial mirroring, uptime clock, demo+features and scripts, and everything past chapter 12. That covers preemptive+threads, user mode, syscalls, ELF loading, the VFS, PCI, virtio-blk,+partitions, FAT32 and the shell. -+ ## What's installed (on this box) @@ -65,57 +78,56 @@ cargo bootimage # build -> target/x86_64-rust_kernel/debug/b cargo run # build + boot in a QEMU window; serial output also prints in the terminal ``` -**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). 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;-quit from another terminal with `pkill qemu-system`.)+**Using it:** `cargo run` opens a QEMU window. The kernel prints a few boot+lines, then you're at the shell prompt `C:/ $`. Click into the window and type,+for example `help`, `cd D:`, `ls` or `cat readme.txt | wc`. See+[The shell](#the-shell-stage-7) for everything it can do. Everything on the+screen is also mirrored to your terminal over serial. Typing `exit` ends the+shell, and the kernel starts a fresh one.++To get the mouse and keyboard back from QEMU, press Ctrl+Alt+G. To quit, close+the window or press Ctrl+C in the terminal. Over SSH without X, use+`cargo run -- -display curses` to see the VGA screen right in your terminal,+and quit from another terminal with `pkill qemu-system`. Headless (no window, good for SSH / scripts): ```sh-./scripts/run-headless.sh # boot, breakpoint, demos, ~5 s of threads + timer ticks, then QEMU exits (script exit 0)+./scripts/run-headless.sh # boot, the shell runs disk/scripts/selftest.sh, QEMU exits;+ # passes if the shell exits 0 AND the expected lines appear ./scripts/run-headless.sh stack-overflow # deliberately overflow the stack; passes if the double-fault handler catches it ./scripts/run-headless.sh page-fault # write to unmapped 0xdeadbeaf; passes if the page-fault handler catches it-cargo run --features headless -- -display none # same as the first, via cargo-cargo run --features headless,stack-overflow -- -display none # same as the second+cargo run --features headless -- -display none # the self-test via cargo # (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,- # "|" = 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 run --features demos # the earlier stages' demos first, then the shell+./scripts/screenshot.sh [out.png] [session.txt] [expected...]+ # boot, type a shell session via QEMU monitor `sendkey`+ # (one command per line; {BS} = Backspace, {UP} = up arrow),+ # check the expected texts appear, save the VGA screen+ # (default: the session in boot.png); exits 1 if text is missing ``` Cargo features (in `Cargo.toml`): | Feature | Effect | |---|---|-| `headless` | print a `.` per timer tick to serial and exit QEMU after 90 ticks (~5 s); panics exit with failure |+| (none) | boot, start the shell on the console (the default) |+| `headless` | the shell runs `/mnt/disk0/scripts/selftest.sh` instead of reading the keyboard; its exit code becomes QEMU's (0 -> success); a 60 s timer safety net fails the run if it hangs; panics exit with failure |+| `demos` | before the shell: the breakpoint, paging and heap demos, threads A/B/C, the async example, and the `hello`/`evil`/`spin`/`demo` programs of stages 2-6 | | `stack-overflow` | after boot, recurse forever to test the double-fault handler (with `headless`: exit success when caught) |-| `page-fault` | after reading CR3, write to unmapped address `0xdeadbeaf` (with `headless`: exit success when caught) |+| `page-fault` | after boot, write to unmapped address `0xdeadbeaf` (with `headless`: exit success when caught) | -Expected serial output of `./scripts/run-headless.sh` (stack frame dump and-some timer dots / thread lines trimmed):+Expected serial output of `./scripts/run-headless.sh`. Boot is quick now+that the demos are behind a feature; most of the output is the shell running+`disk/scripts/selftest.sh`. Each command is echoed after its prompt. The+`[process] started/exited` lines are the kernel's own log. They go to serial+only, so the VGA screen shows just the shell session (and kernel messages+such as a killed process). ```-Hello from Tyler's Rust kernel!-EXCEPTION: BREAKPOINT-... (breakpoint, paging and memory lines as before)-[heap] 4096 KiB heap at 0x444444440000 (fixed-size-block allocator)-... (heap demo lines)-[thread] spawned threads A, B, C (status on the top line)+Rustbelt 0.7.0 -- a hobby x86_64 kernel in Rust [pci] 00:00.0 8086:1237 host bridge-[pci] 00:01.0 8086:7000 ISA bridge-[pci] 00:01.1 8086:7010 IDE controller-[pci] 00:01.3 8086:7113 bridge (other)-[pci] 00:02.0 1234:1111 display controller-[pci] 00:03.0 8086:100e network controller-[pci] 00:04.0 1af4:1001 SCSI storage (virtio-blk)+... (7 PCI devices) [pci] 7 devices on the PCI bus [disk] vda: virtio-blk, 131072 sectors (64 MiB) [disk] vda: GPT partition table, 1 partition(s)@@ -124,59 +136,119 @@ EXCEPTION: BREAKPOINT [vfs] mounted ramfs at / [vfs] mounted ramfs at /tmp [vfs] mounted fat32 at /mnt/disk0-[process] started pid 1 (hello), entry 0x8000400180-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-[process] started pid 2 (evil), entry 0x80004000f0-[process] started pid 3 (spin), entry 0x80004000e0-[evil] pid 2: reading kernel memory at 0x200000...+C:/ $ ver+Rustbelt 0.7.0 (stage 7: shell) x86_64+C:/ $ drives+C: / ramfs+D: /mnt/disk0 fat32+C:/ $ pwd+/+C:/ $ cd D:+D:/ $ ls+[process] started pid 2 (ls), entry 0x8000402c00+bin/ notes/ readme.txt scripts/+[process] pid 2 (ls) exited with code 0+D:/ $ ls -l notes+[process] started pid 3 (ls), entry 0x8000402c00+ 125 todo.txt+[process] pid 3 (ls) exited with code 0+D:/ $ type D:/readme.txt+[process] started pid 4 (cat), entry 0x8000400260+Hello from the FAT32 disk! This file lives on a virtio-blk disk, in a GPT+partition, in a FAT32 file system -- read through the kernel's VFS.+In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.+[process] pid 4 (cat) exited with code 0+D:/ $ cat d:notes/todo.txt | grep -i EXT | wc -l+[process] started pid 5 (cat), entry 0x8000400260+[process] started pid 6 (grep), entry 0x8000400610+[process] pid 5 (cat) exited with code 0+[process] started pid 7 (wc), entry 0x8000400260+[process] pid 6 (grep) exited with code 0+1+[process] pid 7 (wc) exited with code 0+D:/ $ dir bin | wc -l+[process] started pid 8 (ls), entry 0x8000402c00+[process] started pid 9 (wc), entry 0x8000400260+[process] pid 8 (ls) exited with code 0+15+[process] pid 9 (wc) exited with code 0+D:/ $ echo first line > C:/tmp/out.txt+D:/ $ echo second line >> /tmp/out.txt+D:/ $ cat < /tmp/out.txt+[process] started pid 10 (cat), entry 0x8000400260+first line+second line+[process] pid 10 (cat) exited with code 0+D:/ $ md /tmp/work+[process] started pid 11 (mkdir), entry 0x8000400100+[process] pid 11 (mkdir) exited with code 0+D:/ $ copy D:/notes/todo.txt /tmp/work+[process] started pid 12 (cp), entry 0x8000400670+[process] pid 12 (cp) exited with code 0+D:/ $ touch /tmp/work/empty.txt+[process] started pid 13 (touch), entry 0x8000400100+[process] pid 13 (touch) exited with code 0+D:/ $ ls -l C:/tmp/work+[process] started pid 14 (ls), entry 0x8000402c00+ 0 empty.txt+ 125 todo.txt+[process] pid 14 (ls) exited with code 0+D:/ $ del /tmp/work/todo.txt /tmp/work/empty.txt /tmp/work+[process] started pid 15 (rm), entry 0x8000400100+[process] pid 15 (rm) exited with code 0+D:/ $ grep "D:" readme.txt+[process] started pid 16 (grep), entry 0x8000400610+In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.+[process] pid 16 (grep) exited with code 0+D:/ $ alias ll="ls -l"+D:/ $ ll C:/tmp+[process] started pid 17 (ls), entry 0x8000402c00+ 23 out.txt+[process] pid 17 (ls) exited with code 0+D:/ $ run --trace cat D:/notes/todo.txt+[process] started pid 18 (cat), entry 0x8000400260+[trace 18] open("/mnt/disk0/notes/todo.txt", 0o0) = 3+[trace 18] read(3, "Done: the shell (cd, ls, cat, run, start"..., 512) = 125+Done: the shell (cd, ls, cat, run, start, pipes, redirection, drive letters).+Next: more file systems -- ext2, exFAT, btrfs.+[trace 18] write(1, "Done: the shell (cd, ls, cat, run, start"..., 125) = 125+[trace 18] read(3, "", 512) = 0+[trace 18] close(3) = 0+[trace 18] exit(0)+[process] pid 18 (cat) exited with code 0+D:/ $ start sleep 1+[process] started pid 19 (sleep), entry 0x8000400000+[pid 19] sleep started in the background+D:/ $ jobs+[pid 19] running start sleep 1+D:/ $ wait+[process] pid 19 (sleep) exited with code 0+[done] pid 19 (start sleep 1), exit code 0+D:/ $ run evil+[process] started pid 20 (evil), entry 0x80004000f0+[evil] pid 20: 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...-[process] started pid 4 (demo), entry 0x8000400320-[process] started pid 5 (echo), entry 0x8000400180-Type a line + Enter: the echo program (ring 3) reads it-[async] async number: 42-...-[demo] pid 4 started from /mnt/disk0/bin/demo-[demo] readme.txt from FAT32: Hello from the FAT32 disk! This file lives on a virtio-blk disk, in a GPT-[demo] /mnt/disk0/bin: hello evil spin echo demo child-[echo] pid 5: type a line and press Enter-[spin] round 1 done (count = 30000000)-[demo] mkdir/unlink/rmdir work on ramfs and FAT32-[process] started pid 6 (child), entry 0x8000400240-[process] pid 6 (child) exited with code 0-[demo] read from pipe: "hello through a pipe from pid 6"-[process] started pid 7 (child), entry 0x8000400240-[child] pid 7 got 3 args: "child" "with" "three args"-[process] pid 7 (child) exited with code 0-[process] started pid 8 (child), entry 0x8000400240-[process] pid 8 (child) exited with code 42-[spin] round 3 done (count = 90000000)-[process] pid 3 (spin) exited with code 0-[demo] spawned child 42 (pid 8), wait -> exit code 42-[process] started pid 9 (evil), entry 0x80004000f0-[evil] pid 9: reading kernel memory at 0x200000...-...-[process] pid 9 (evil) killed: page fault-[demo] all stage 6 checks passed, exiting with 0-[process] pid 4 (demo) exited with code 0-[thread C] done, exiting-.................. 90 ticks-[ok] timer works, exiting+[process] pid 20 (evil) killed: page fault+[exit code 139]+D:/ $ nosuchprogram+sh: nosuchprogram: command not found (try `help`)+D:/ $ cd Q:+cd: there is no drive Q: (see `drives`)+D:/ $ cd C:/tmp+C:/tmp $ pwd+/tmp+C:/tmp $ exit 0+[process] pid 1 (sh) exited with code 0+[ok] shell self-test finished: Exited(0)+[memory] 10 frames in use+[run-headless] all 14+2 expected shell lines found [run-headless] kernel exited OK ``` -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. (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 and the demo: that's preemption of ring-3 code by the timer.-The `[pci]` lines only go to serial (the VGA screen gets the count).+The run ends with `[memory] 10 frames in use`, even though 20 processes have+come and gone. That shows exited processes give their page tables, code and+stacks back. The kernel boots from the bootimage on the first (IDE) disk; the scripts and `Cargo.toml` attach a **second** disk, `target/disk.img`, as@@ -213,10 +285,11 @@ 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` | programs loaded from the FAT disk: `hello` exits 0, `demo` passes all its checks (exit 0), `evil` is killed with code 139, `spin` is preempted; spawn with arguments + trace + exit code 7, then reaped; a child's stdout into a pipe; spawn errors (`ENOENT`, `ENOEXEC`, `EISDIR`, `ECHILD`); ELF loader rejects garbage; user pointers give `EFAULT` (length 0 is fine); `sleep` blocks for the right number of ticks; a `WaitQueue` wakes its waiter |+| `tests/shell.rs` | **the shell**: tokenizer (operators without spaces, quotes, comments), parser (pipelines, `<` `>` `>>`, `&`, error cases), drive-letter assignment from a mount table, `D:/x` -> `/mnt/disk0/x` translation and back for the prompt; then the real `/bin/sh` runs a script (cd with a drive letter, ls, a pipe, `>`/`>>`/`<`, `ver`, `exit 3`) with its stdout in a pipe and the output is checked; background jobs (`start`, `jobs`, `wait`); 20 processes started and reaped leave `frames_in_use` unchanged |+| `tests/user_mode.rs` | programs loaded from the FAT disk: `hello` exits 0, `demo` passes all its checks (exit 0), `evil` is killed with code 139, `spin` is preempted; spawn with arguments + trace + exit code 7, then reaped; a child's stdout into a pipe; spawn errors (`ENOENT`, `ENOEXEC`, `EISDIR`, `ECHILD`); `wait` with `WNOHANG` returns `EAGAIN` while the child runs; ELF loader rejects garbage; user pointers give `EFAULT` (length 0 is fine); `sleep` blocks for the right number of ticks; a `WaitQueue` wakes its waiter | | `tests/vfs.rs` | read `/hello.txt` to EOF, path normalisation (`.`, `..`, relative to a cwd), `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, longest-prefix mounts, sparse writes zero-fill, listing, remove (`ENOTEMPTY`, `EBUSY` on mount points), seek/stat/truncate, `getdents64` record layout, pipes pass bytes and report EOF / `EPIPE` | | `tests/block.rs` | PCI scan finds the virtio disk; sector 1 of the image is the GPT header; multi-sector reads equal single-sector reads; write + read back; out-of-range and partial-sector requests fail; the GPT partition is registered as `vda1` (with a FAT32 boot sector); MBR parsing on a RAM disk |-| `tests/fat.rs` | the FAT32 volume is mounted at `/mnt/disk0` with the expected files; read `readme.txt`; every program in `bin/` is a valid ELF; create/write/read/truncate/remove a 10 KB file; directories with long names, `ENOTEMPTY`; the `BlockIo` adapter handles unaligned reads and writes |+| `tests/fat.rs` | the FAT32 volume is mounted at `/mnt/disk0` with the expected files (`bin`, `notes`, `readme.txt`, `scripts`); read `readme.txt`; every program in `bin/` is a valid ELF; create/write/read/truncate/remove a 10 KB file; directories with long names, `ENOTEMPTY`; the `BlockIo` adapter handles unaligned reads and writes | | `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]`@@ -236,21 +309,23 @@ rust-toolchain.toml pin nightly + rust-src + llvm-tools-preview x86_64-rust_kernel.json custom target: no OS, no red zone, soft-float, abort on panic, rust-lld 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/main.rs the bootable binary: kernel_main (init, then a thread that runs the shell and+ restarts it when it exits), the `demos` feature's demos, panic handler 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,- global frame allocator, per-process address spaces, user pointer checks+ global frame allocator + free list, per-process address spaces (and freeing+ them), 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 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/task/keyboard.rs lock-free scancode queue, ScancodeStream, async keyboard task (keys -> tty) 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 + all syscalls (table at the top)@@ -262,7 +337,8 @@ src/block/virtio_blk.rs legacy virtio-blk driver: one virtqueue, polled, bounc src/block/partition.rs MBR + GPT parsing, Partition = a BlockDevice window onto its disk 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/tty.rs console line discipline (cooked: echo + line editing; raw: every key, arrows+ as escape sequences) + Console (the File behind fds 0/1/2) src/fs/mod.rs VFS: Inode + FileSystem traits, mount table, path normalize/lookup/create/remove, init (boot files), mount_disks (FAT partitions -> /mnt/diskN) src/fs/file.rs File trait (what an fd points to), InodeFile (inode + offset), Stat, getdents64@@ -270,17 +346,22 @@ src/fs/ramfs.rs in-memory filesystem src/fs/fat.rs FAT32 via the fatfs crate: BlockIo adapter + FatFs/FatInode for the VFS src/fs/pipe.rs pipes: a byte queue with a blocking read end and a write end build.rs compiles user/*.rs to ELF files, then builds target/disk.img (GPT + FAT32)-disk/ files copied onto the FAT32 disk (readme.txt, notes/todo.txt)-user/ulib.rs tiny user-space "libc": syscall wrappers, println!, _start, panic handler+disk/ files copied onto the FAT32 disk (readme.txt, notes/todo.txt,+ scripts/selftest.sh = the headless shell session)+user/ulib.rs tiny user-space "libc": syscall wrappers, println!/eprintln!, a 64 KiB heap+ (Vec/String), line reading, strerror, _start, panic handler user/link.ld linker script for user programs (base 0x80_0040_0000)-user/{hello,evil,spin,echo,demo,child}.rs the user programs (copied to /mnt/disk0/bin)+user/sh.rs the shell: line editor, builtins, aliases, PATH, pipes, redirection, jobs+user/shparse.rs the shell's parser + drive letters (also compiled into tests/shell.rs)+user/{ls,cat,wc,grep,mkdir,rm,touch,cp,sleep}.rs the shell's tools+user/{hello,evil,spin,demo,child}.rs test/demo programs (all programs go to /mnt/disk0/bin) 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 scripts/runner.sh `cargo run`/`cargo test` runner: `bootimage runner` + stale-image fix-scripts/run-headless.sh headless boot with timeout + exit-code check-scripts/screenshot.sh headless boot + typed keys + PNG screenshot via QEMU monitor-boot.png screenshot of the VGA screen+scripts/run-headless.sh headless boot + shell self-test, timeout, exit-code and output checks+scripts/screenshot.sh headless boot + a typed shell session + PNG screenshot via QEMU monitor+boot.png screenshot of the VGA screen (a shell session) ``` ## What the interrupt code does (chapters 5-7)@@ -457,20 +538,22 @@ so `spin::Mutex` is enough. ## User mode, syscalls and processes -Six tiny Rust programs in `user/` run in **ring 3** as separate processes, each-with its own address space and file-descriptor table:+The user programs in `user/` run in **ring 3** as separate processes, each+with its own address space and file-descriptor table. The shell and its+tools are described in [The shell](#the-shell-stage-7). These are the test+and demo programs from stages 5 and 6 (run them from the shell: `run hello`,+`run evil`...): | 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 (the thread is blocked until Enter), prints `[echo] you typed: ...`; type `exit` to end it | | `demo` | stage 6 checks: cwd, reads `/mnt/disk0/readme.txt`, `stat`/`fstat`/`lseek`, lists `bin/` with `getdents64`, `chdir` errors, `mkdir`/`unlink`/`rmdir` on ramfs and FAT, a pipe from a child, arguments, `wait` exit codes (42, 139 for killed `evil`), `ECHILD`/`ENOENT`/`ENOEXEC`, `sleep_ms`; exits with the number of failed checks | | `child` | helper for `demo` and tests: `child pipe` writes a line to stdout, `child cat` copies stdin to stdout, `child 42` exits with 42, otherwise prints its arguments | -The kernel starts them all from `/mnt/disk0/bin/` -- they're read from the FAT32-partition on the virtio disk, not compiled into the kernel any more.+All of them are read from `/mnt/disk0/bin/`, on the FAT32 partition of the+virtio disk; they're not compiled into the kernel any more. ### Rings in one paragraph @@ -535,17 +618,19 @@ Numbers are Linux's wherever Linux has the same call, so man pages and |---|---|---|---| | 0 | `read` | fd, buf, len | console: blocks until Enter; pipe: blocks until data or EOF | | 1 | `write` | fd, buf, len | console = VGA + serial; pipe: `EPIPE` once the reader is gone |-| 2 | `open` | path, path_len, flags | `O_CREAT` 0o100, `O_TRUNC` 0o1000; directories can be opened (for `getdents64`) |+| 2 | `open` | path, path_len, flags | `O_CREAT` 0o100, `O_TRUNC` 0o1000, `O_APPEND` 0o2000 (every write goes to the end: `>>`); directories can be opened (for `getdents64`) | | 3 | `close` | fd | | | 4 | `stat` | path, path_len, *Stat | | | 5 | `fstat` | fd, *Stat | | | 8 | `lseek` | fd, offset, whence | `SEEK_SET/CUR/END` = 0/1/2; `ESPIPE` on console/pipes |+| 16 | `ioctl` | fd, request, arg | only `TTY_SET_RAW` (1): console raw mode on (arg 1) / off (0); returns the old mode, `ENOTTY` if fd isn't the console | | 22 | `pipe` | *[u32; 2] | [read end, write end] | | 24 | `sched_yield` | | | | 35 | `sleep_ms` | ms | **ours**: Linux nr 35 is `nanosleep(timespec)` | | 39 | `getpid` | | | | 60 | `exit` | code | |-| 61 | `wait` | pid | **simpler than Linux `wait4`**: blocks, returns the exit code (139 = killed, like a shell shows SIGSEGV), reaps the child; `ECHILD` if not your child |+| 61 | `wait` | pid, flags | **simpler than Linux `wait4`**: blocks, returns the exit code (139 = killed, like a shell shows SIGSEGV), reaps the child (freeing its memory); `ECHILD` if not your child; flags `WNOHANG` (1): don't block, `EAGAIN` while it's still running |+| 63 | `uname` | *utsname | Linux's struct: 6 NUL-padded 65-byte fields: "Rustbelt", "qemu", "0.7.0", "stage 7: shell", "x86_64", "(none)" | | 79 | `getcwd` | buf, len | returns the length (no NUL); `ERANGE` if too small | | 80 | `chdir` | path, path_len | per-process cwd, inherited by spawned children | | 83 | `mkdir` | path, path_len | |@@ -553,6 +638,7 @@ Numbers are Linux's wherever Linux has the same call, so man pages and | 87 | `unlink` | path, path_len | files only (`EISDIR`) | | 217 | `getdents64` | fd, buf, len | real Linux `struct linux_dirent64` records (ino, off, reclen, type, name\0) | | 500 | `spawn` | path, path_len, argv, argv_len, *SpawnOptions | **ours**: start a program, returns its pid |+| 501 | `mounts` | buf, len | **ours** (Linux reads `/proc/mounts`): the mount table as text, one `path fstype` line per mount, in mount order; returns the length, `ERANGE` if too small. The shell assigns drive letters from it | Deviations from Linux worth knowing: @@ -569,8 +655,9 @@ Deviations from Linux worth knowing: exec -- pointing fds 0/1/2 at a pipe or a file -- is spawn's `SpawnOptions`: `{ stdin, stdout, stderr: i64 (one of *my* fds, -1 = same as mine), flags }`. `flags = SPAWN_TRACE` (1) prints every syscall the child makes, as- `[trace pid] name(a1, a2, a3) = result` -- that's what the shell's- `run --trace` will use. fork + exec could still be added later.+ `[trace pid] open("notes/todo.txt", 0o0) = 3` in strace style (strings+ shown, errors as `-2 ENOENT`), which is what the shell's `run --trace`+ uses. fork + exec could still be added later. - **argv** is passed as one buffer with NUL-separated arguments (`"child\0with\0three args"`; empty = just the program name). The kernel copies the strings onto the new process's stack and starts `_start` with@@ -634,14 +721,25 @@ them onto the FAT32 disk image instead and `process::spawn_path` reads them through the VFS (see "Disks" below). `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)`).+`syscall` inline asm, wrappers, `print!/println!/eprintln!`, `_start` (calls+`main`, then `exit` with its return value) and a panic handler (`exit(101)`).+Since stage 7 it also has a **heap**: a 64 KiB array in `.bss` with a+power-of-two size-class allocator (free list per size) as the+`#[global_allocator]`. A program that says `extern crate alloc;` can use+`Vec`, `String` and `format!`; the `x86_64-unknown-none` target ships a+precompiled `alloc` too. 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.+physical-memory map and leaves the rest (`.bss`) zero. It refuses (`ENOEXEC`)+a file where two segments with different permissions share a page. A page+has one set of permissions, so the second segment would silently get the+first one's. This happened for real in stage 7: `sleep` has no `.data`, so+the linker put its `.bss` on the read-only `.rodata` page, and `sleep` died+on its first write to a static. `user/link.ld` now aligns the location+counter before `.data`, not just the (empty, hence dropped) section. ### VFS (`src/fs/`) and file descriptors @@ -668,12 +766,33 @@ Designed like Linux's so real filesystems can plug in later: 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; orphans aren't reparented; no signals; user pointer+Keyboard input for `read(0)` goes through a small tty line discipline+(`src/tty.rs`), like Linux's termios:++- **Cooked mode** (the default): the kernel echoes the characters and+ Backspace edits the current line. Enter makes the line, with its `\n`,+ available to readers. Programs like `cat` with no arguments see this.+- **Raw mode** (`ioctl(0, TTY_SET_RAW, 1)`): every key goes to the reader at+ once and nothing is echoed. Backspace is `\x08`, and the arrows, Home, End+ and Delete arrive as ANSI escape sequences (`ESC [ A` = up...). The shell+ uses this while you edit a line, so it can do the echo and history itself.+ It switches back to cooked before running a program. Raw mode is also+ switched off automatically if its owner process exits.++The console understands two control characters on output. `\x08` moves back+one column and blanks it, and `\x0c` (form feed) clears the screen, which is+what `cls` writes.++**Process cleanup.** When `wait` reaps a process, `memory::free_address_space`+walks L4 slot 1 (the user half) and frees every page, page table and the L4+table. Freed frames go on a free list that `alloc_frame` uses first. The list+is an intrusive linked list: each free frame stores the address of the next+one, written through the physical-memory map, so it needs no heap. The+exiting process disables interrupts before publishing its exit status. That+way the parent can't free the address space while the dying thread might+still run on it.++Known simplifications (exercises): a process nobody waits for keeps its memory; orphans aren't reparented; no signals; 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). @@ -767,31 +886,164 @@ mdir -i target/disk.img@@1M ::/bin # @@1M = partition offset (2048 * 51 mtype -i target/disk.img@@1M ::/readme.txt ``` -## Next stage: the shell (Tyler's design)--A **hybrid** shell: Unix navigation with a DOS flavour on top. All the-syscalls it needs exist now (`read(0)` blocks, `spawn` + `wait`, `pipe`,-`getdents64`, `stat`, `chdir`/`getcwd`, `mkdir`/`rmdir`/`unlink`, `lseek`,-`SPAWN_TRACE`).--- **Unix navigation**: `cd`, `ls`, `pwd`, `cat`, `mkdir`, `rm`, `echo`.-- **DOS-style program running**: `run prog args` (foreground: spawn + wait,- show the exit code) and `start prog args` (background: spawn, don't wait),- plus aliases `dir` (= ls), `type` (= cat), `cls` (clear screen), `ver`.-- **Pipes and redirection**: `a | b` (pipe + two spawns with stdout/stdin set)- and `> file` (open with `O_CREAT|O_TRUNC`, pass it as the child's stdout).-- `help`, `exit`.-- **`run --trace prog`**: spawn with `SPAWN_TRACE` to print every syscall the- program makes (a built-in mini `strace`).-- **Drive letters as aliases over the single tree**: `C:` = `/`, and each- mount gets the next letter automatically (`D:` = `/mnt/disk0`, ...). It's- only shell syntax: the kernel keeps one Unix-style tree.+## The shell (stage 7)++`/bin/sh` on the disk (`user/sh.rs`, about 550 lines) is an ordinary ring-3+program. It uses nothing but the syscalls above. Its design is a **hybrid**:+Unix navigation and pipes, with DOS-style program running and drive letters+on top. At boot a kernel thread starts it on the console and waits for it.+When it exits, the thread starts a new one, the way `init` respawns a login+shell.++```+Rustbelt shell. Type `help` for commands. Drives: C: = /, D: = /mnt/disk0+C:/ $ cd d:+D:/ $ ls+bin/ notes/ readme.txt scripts/+D:/ $ type readme.txt | grep -i rustbelt+In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.+D:/ $ ls bin | wc -l+15+D:/ $ echo first > C:/tmp/a.txt+D:/ $ echo second >> C:/tmp/a.txt+D:/ $ wc < C:/tmp/a.txt+ 2 2 13+D:/ $ run --trace cat notes/todo.txt+[trace 8] open("notes/todo.txt", 0o0) = 3+[trace 8] read(3, "Done: the shell (cd, ls, cat, run, start"..., 512) = 125+Done: the shell (cd, ls, cat, run, start, pipes, redirection, drive letters).+Next: more file systems -- ext2, exFAT, btrfs.+[trace 8] write(1, "Done: the shell (cd, ls, cat, run, start"..., 125) = 125+[trace 8] read(3, "", 512) = 0+[trace 8] close(3) = 0+[trace 8] exit(0)+D:/ $ start sleep 5+[pid 9] sleep started in the background+D:/ $ jobs+[pid 9] running start sleep 5+D:/ $ run evil+[evil] pid 10: reading kernel memory at 0x200000...+[process] pid 10 (evil) killed: page fault+[exit code 139]+D:/ $ ver+Rustbelt 0.7.0 (stage 7: shell) x86_64+D:/ $ <- Enter on an empty line, a few seconds later+[done] pid 9 (start sleep 5), exit code 0+D:/ $+```++### Commands++| Command | What it does |+|---|---|+| `cd [dir]` | change directory (`cd` alone = `C:/`); `cd D:` goes to the root of drive D |+| `pwd` | print the current directory as a Unix path (the prompt shows the drive form) |+| `echo text...` | print the arguments |+| `ver` | kernel name and version, from the `uname` syscall |+| `drives` | the drive letters and what they map to |+| `alias [name=value]` | list aliases or define one: `alias ll="ls -l"` |+| `history` | the commands typed so far |+| `jobs` / `wait [pid]` | list background jobs / wait for them (all, or one pid) |+| `cls`, `clear` | clear the screen |+| `help` | the command list |+| `exit [code]` | leave the shell (with the last command's exit code, or `code`) |+| `run [--trace] prog args` | run a program in the foreground; `--trace` prints every syscall it makes |+| `start prog args` | run it in the background and print its pid (same as `prog args &`) |+| `prog args` | a bare name is looked up in each drive's `bin` directory, in order: `C:/bin`, `D:/bin`... |++Programs in `D:/bin` (`user/*.rs`, each 30-150 lines):++| Program | What it does |+|---|---|+| `ls [-l] [path...]` | list a directory, sorted; in columns on the console, one name per line into a pipe or file (like real `ls`); `-l` shows sizes and `<DIR>` |+| `cat [file...]` | print files, or stdin (also for `-`) |+| `wc [-l] [file...]` | lines, words, bytes |+| `grep [-i] [-v] [-n] text [file...]` | lines containing `text` (plain text, no regexes); exit code 0 = found, 1 = not found |+| `mkdir dir...` | create directories |+| `rm path...` | delete files or empty directories |+| `touch file...` | create empty files |+| `cp src dest` | copy a file (into `dest` if it's a directory) |+| `sleep N` / `sleep 250ms` | wait (blocks in the kernel, no busy loop) |+| `hello`, `demo`, `evil`, `spin`, `child` | the stage 5/6 test programs: `run demo` runs all the stage 6 syscall checks |++Built-in aliases are the DOS names: `dir` = `ls`, `type` = `cat`,+`del` = `rm`, `md` = `mkdir`, `copy` = `cp`.++**Line editing**: Backspace, and Up/Down to browse the history. Left/Right+and Tab aren't supported yet. **Scripts**: `sh file` runs the commands in a+file and echoes each one after its prompt. If stdin isn't the console+(`sh < file`, `cat file | sh`), the shell reads commands from it the same+way. `#` starts a comment.++### Pipes and redirection++`a | b | c`, `> file` (create or truncate), `>> file` (append) and `< file`+can be combined: `cat < in.txt | grep x | wc -l > count.txt`. The operators+don't need spaces around them (`ls|wc`). Quotes (`'...'` or `"..."`) group+words and hide operators. There are no variables or escapes.++How a pipeline runs (`Shell::run_pipeline`): for each stage, the shell picks+its stdin and stdout and spawns it.+- **stdin**: the `< file`, else the read end of the previous stage's pipe,+ else the shell's own stdin.+- **stdout**: a new `pipe()` to the next stage, overridden by `> file`, else+ the shell's own stdout.+- **Spawning**: the stage starts with `spawn(path, argv, SpawnOptions { stdin,+ stdout, ... })`.++Then the shell **closes its own copies** of all pipe ends and files. A reader+only sees end-of-file once every write end is closed, and the shell holds+one. Last, it waits for every stage; the last stage's exit code is the+pipeline's. A builtin can be a stage too (`help | grep run`): it just writes+to the pipe. Pipes have no size limit here, so that can't block.++### Drive letters++Drive letters are only shell syntax. The kernel keeps one Unix tree:++- **Mapping**: `C:` is `/`. `D:`, `E:`... go to the mounts under `/mnt`,+ in the order they were mounted. The shell reads them from the `mounts`+ syscall before every prompt, so a newly mounted disk would get a letter+ right away. Other mounts, such as `/tmp`, stay under C: (`C:/tmp`).+- **Translation**: every unquoted word that starts with a known letter is+ translated before any syscall sees it (`shparse::translate`):+ - `D:/notes/todo.txt` becomes `/mnt/disk0/notes/todo.txt`.+ - `d:` becomes `/mnt/disk0` (any case).+ - `D:notes` becomes `/mnt/disk0/notes`, since drives don't have their own+ current directory.++ Quote a word to keep it literal: `grep "D:" readme.txt`.+- **Prompt**: the prompt goes the other way (`shparse::display`). The longest+ matching drive path wins, so `/mnt/disk0/bin` shows as `D:/bin`.+- **Unknown letters**: these are left alone, so `cd Q:` says+ `there is no drive Q:`.++### How the pieces fit++- `user/shparse.rs`: the tokenizer, the parser (`Pipeline` of `Command`s,+ each with words and redirections) and the drive-letter functions. All of+ them are pure functions with no syscalls. The same file is compiled into+ the shell *and* into `tests/shell.rs` (`#[path = "../user/shparse.rs"]`),+ so the parser is unit-tested inside the kernel test suite.+- `user/sh.rs`: the main loop runs these steps: refresh the drives, report+ finished background jobs (`wait` with `WNOHANG`), print the prompt, read a+ line, then run it. Before running, each command goes through `prepare()`:+ it translates drive letters, expands aliases, and strips `run` /+ `run --trace` / `start` prefixes. The command then runs as a builtin or is+ spawned with the right fds.+- **Kernel support added for the shell**:+ - `ioctl` (raw mode), `uname`, `mounts`, `wait` flags and `O_APPEND`.+ - Freeing process memory, so a long session doesn't run out of frames.+ - strace-style `--trace` output.+ - The `\x08` and `\x0c` console controls.+ - A fix for the `|` key (see Notes). ## Roadmap Done: all 12 chapters of the 2nd edition, plus preemptive kernel threads, user mode + syscalls, ELF loading, a VFS with a ramfs, PCI, virtio-blk,-MBR/GPT partitions, FAT32, blocking threads, pipes, spawn/wait.+MBR/GPT partitions, FAT32, blocking threads, pipes, spawn/wait, process+memory cleanup, and the shell. - [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)@@ -817,7 +1069,8 @@ MBR/GPT partitions, FAT32, blocking threads, pipes, spawn/wait. - [x] MBR + GPT partitions — [OSDev: MBR](https://wiki.osdev.org/MBR_(x86)), [OSDev: GPT](https://wiki.osdev.org/GPT) - [x] FAT32 through `fatfs`, mounted at `/mnt/disk0`; programs loaded from disk — [OSDev: FAT](https://wiki.osdev.org/FAT) - [x] Blocking threads + wait queues, pipes, `spawn`/`wait`, cwd, `stat`/`lseek`/`getdents64` — [OSDev: Blocking Process](https://wiki.osdev.org/Blocking_Process)-- [ ] Shell (next stage, design above) — [OSDev: Shell](https://wiki.osdev.org/Shell)+- [x] Shell as a user program: line editing, builtins, PATH, pipes, redirection, background jobs, drive letters, `run --trace` — [OSDev: Shell](https://wiki.osdev.org/Shell)+- [x] Process cleanup: frame free list, address spaces freed on reap ### Filesystem roadmap (Tyler's plan) @@ -852,8 +1105,10 @@ whose `root()` returns `Inode`s; mounting it is one `fs::mount` call. Other next stages: -- **Shell** — next stage; design in "Next stage: the shell" above.-- **Process cleanup** — free a process's page tables and frames on exit.+- **Shell extras**: Left/Right cursor movement, Tab completion of file+ names, `$?` and environment variables, Ctrl+C to kill the foreground job+ (that needs signals or a kill syscall).+- **More programs**: `more`/`less` (needs raw mode and screen size), an editor. Small exercises along the way: guard pages under thread stacks, a timer-based async `sleep` future, speed up the PIT@@ -890,3 +1145,12 @@ and for a UEFI/framebuffer setup later, the newer [`bootloader` 0.11](https://gi - Debug builds use a lot of stack: deep call chains (syscall -> VFS -> fatfs -> ELF loader) needed more than 16 KiB. If you see a weird ring-0 fault at address 0 or "kernel stack overflow in thread", suspect the stack first.+- **The `\ |` key**: pc-keyboard 0.7's scancode set 1 decodes it (scancode+ 0x2B) as `KeyCode::Oem7`, but its US 104-key layout only maps `Oem5` to+ `\` and `|`. So that key typed nothing, which went unnoticed until the+ shell needed `|`. `task/keyboard.rs` renames `Oem7` to `Oem5` before+ decoding.+- User programs: the linker drops empty output sections, and their+ `ALIGN(4K)` with them. A program without `.data` then had its `.bss` on the+ read-only `.rodata` page (see "ELF loading"). `user/link.ld` uses+ `. = ALIGN(4K);` before `.data`, and the loader now rejects such files.
Binary file changed.
@@ -24,7 +24,12 @@ use std::{env, fs, io::Write, path::{Path, PathBuf}, process::{Command, Stdio}}; -const PROGRAMS: &[&str] = &["hello", "evil", "spin", "echo", "demo", "child"];+/// Everything in /bin on the disk: the shell, its tools, and the test and+/// demo programs of the earlier stages.+const PROGRAMS: &[&str] = &[+ "sh", "ls", "cat", "wc", "grep", "mkdir", "rm", "touch", "cp", "sleep", // the shell + tools+ "hello", "evil", "spin", "demo", "child", // stage 5/6 demos and tests+]; const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512; const PART_START: u64 = 2048;
@@ -1,2 +1,2 @@-Next stage: the shell (cd, ls, pwd, cat, run, start, pipes, > redirection).-Later: ext2, exFAT, btrfs.+Done: the shell (cd, ls, cat, run, start, pipes, redirection, drive letters).+Next: more file systems -- ext2, exFAT, btrfs.
@@ -1,2 +1,3 @@ Hello from the FAT32 disk! This file lives on a virtio-blk disk, in a GPT partition, in a FAT32 file system -- read through the kernel's VFS.+In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.
@@ -0,0 +1,32 @@+# Rustbelt shell self-test: `cargo run --features headless` runs this with+# `sh /mnt/disk0/scripts/selftest.sh` (D:/scripts/selftest.sh) and scripts/run-headless.sh checks the output.+ver+drives+pwd+cd D:+ls+ls -l notes+type D:/readme.txt+cat d:notes/todo.txt | grep -i EXT | wc -l+dir bin | wc -l+echo first line > C:/tmp/out.txt+echo second line >> /tmp/out.txt+cat < /tmp/out.txt+md /tmp/work+copy D:/notes/todo.txt /tmp/work+touch /tmp/work/empty.txt+ls -l C:/tmp/work+del /tmp/work/todo.txt /tmp/work/empty.txt /tmp/work+grep "D:" readme.txt+alias ll="ls -l"+ll C:/tmp+run --trace cat D:/notes/todo.txt+start sleep 1+jobs+wait+run evil+nosuchprogram+cd Q:+cd C:/tmp+pwd+exit 0
@@ -3,7 +3,11 @@ # window. Serial output goes to this terminal; the kernel powers QEMU off via # the isa-debug-exit device when done. A timeout guards against hangs. #-# Usage: ./scripts/run-headless.sh # normal boot, exits after ~3 s of timer ticks+# A normal boot runs the shell self-test (disk/scripts/selftest.sh); the+# shell's exit code decides success, and this script then also checks that+# the expected lines appear in the output.+#+# Usage: ./scripts/run-headless.sh # boot + shell self-test # ./scripts/run-headless.sh stack-overflow # extra cargo features, comma separated set -euo pipefail cd "$(dirname "$0")/.."@@ -14,15 +18,45 @@ touch target/x86_64-rust_kernel/debug/rust_kernel cargo bootimage --features "headless${1:+,$1}" IMG=target/x86_64-rust_kernel/debug/bootimage-rust_kernel.bin +LOG=target/headless-serial.log set +e-timeout 30 qemu-system-x86_64 \+timeout 90 qemu-system-x86_64 \ -drive format=raw,file="$IMG" \ -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \ -device isa-debug-exit,iobase=0xf4,iosize=0x04 \- -serial stdio -display none -no-reboot-code=$?+ -serial stdio -display none -no-reboot | tee "$LOG"+code=${PIPESTATUS[0]} set -e +# Lines the shell self-test must have printed (only for a plain boot).+if [ -z "${1:-}" ] && [ "$code" = 33 ]; then+ expect=(+ "Rustbelt 0.7.0 (stage 7: shell) x86_64" # ver+ "D: /mnt/disk0 fat32" # drives+ "D:/ $ ls" # cd D: changed the prompt+ "readme.txt scripts/" # ls in columns+ "In the Rustbelt shell this disk is drive D:" # type D:/readme.txt+ "second line" # >> and <+ " 125 todo.txt" # cp + ls -l+ '[trace 18] open("/mnt/disk0/notes/todo.txt", 0o0) = 3' # run --trace+ "sleep started in the background" # start+ "[done] pid 19 (start sleep 1), exit code 0" # wait+ "[exit code 139]" # run evil+ "sh: nosuchprogram: command not found"+ "cd: there is no drive Q:"+ "C:/tmp $ pwd"+ )+ missing=0+ for line in "${expect[@]}"; do+ grep -qF -- "$line" "$LOG" || { echo "[run-headless] MISSING: $line"; missing=1; }+ done+ # Pipe results: `... | grep -i EXT | wc -l` -> 1, `dir bin | wc -l` -> 15.+ for n in 1 15; do+ grep -qx -- "$n" "$LOG" || { echo "[run-headless] MISSING pipe result: $n"; missing=1; }+ done+ [ $missing = 0 ] && echo "[run-headless] all ${#expect[@]}+2 expected shell lines found" || code=35+fi+ # isa-debug-exit: QEMU exit status = (value << 1) | 1, so Success(0x10) -> 33. case $code in 33) echo "[run-headless] kernel exited OK"; exit 0 ;;
@@ -1,13 +1,14 @@ #!/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 + 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.+# Boot the normal (interactive) kernel with no window, type a shell session+# into it through the QEMU monitor's `sendkey` command, then dump the VGA+# screen to a PNG. Doubles as an automated test of the keyboard, the raw+# console mode and the shell's line editor (Backspace, up-arrow history):+# afterwards the serial copy of the console is checked for expected text. #-# Usage: ./scripts/screenshot.sh [out.png] [text-to-type] [expected-result]-# In text-to-type, "<" means Backspace and "|" means Enter.-# Exits 1 if the expected text doesn't show up on serial.+# Usage: ./scripts/screenshot.sh [out.png] [session.txt] [expected text...]+# session.txt: one command per line, typed key by key followed by Enter.+# In it, {BS} is Backspace and {UP} is the up arrow.+# Exits 1 if any expected text doesn't show up. set -euo pipefail cd "$(dirname "$0")/.." @@ -17,8 +18,23 @@ 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:-[echo] you typed: Hello, kernel!}"+SESSION="${2:-}"+shift $(( $# > 2 ? 2 : $# ))+if [ -z "$SESSION" ]; then+ # The default session (what boot.png shows). "lx{BS}s" types a typo+ # and fixes it.+ SESSION=$(mktemp /tmp/session.XXXXXX)+ cat > "$SESSION" <<'TXT'+cd d:+lx{BS}s+cat D:/notes/todo.txt+type readme.txt | grep -i rustbelt+run --trace cat notes/todo.txt+TXT+ set -- "D:/ $ ls" "bin/ notes/" "Next: more file systems" \+ "In the Rustbelt shell this disk is drive D:" \+ '] open("notes/todo.txt", 0o0) = 3' "] exit(0)"+fi SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX) SERIAL_LOG=$(mktemp /tmp/serial.XXXXXX) @@ -26,22 +42,26 @@ qemu-system-x86_64 -drive format=raw,file="$IMG" \ -drive file=target/disk.img,format=raw,if=virtio,snapshot=on \ -serial file:"$SERIAL_LOG" -display none -monitor unix:"$SOCK",server,nowait & QPID=$!-sleep 6 # boot, and let the demo threads finish their serial output+sleep 4 # boot until the shell prompt # Talk to the QEMU monitor over its UNIX socket (python avoids needing socat).-python3 - "$SOCK" "$OUT" "$TEXT" <<'PY'-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', '|': 'ret'}+python3 - "$SOCK" "$OUT" "$SESSION" <<'PY'+import re, socket, sys, time+sock, out, session = sys.argv[1:]+# QEMU key names for the characters we might type (US keyboard layout).+names = {' ': 'spc', ',': 'comma', '.': 'dot', '/': 'slash', '-': 'minus', '=': 'equal',+ ':': 'shift-semicolon', '|': 'shift-backslash', '>': 'shift-dot', '<': 'shift-comma',+ '"': 'shift-apostrophe', "'": 'apostrophe', '_': 'shift-minus', '!': 'shift-1',+ '#': 'shift-3', '&': 'shift-7', '{BS}': 'backspace', '{UP}': 'up', '\n': 'ret'} def key(c): if c in names: return names[c] if c.isupper(): return 'shift-' + c.lower() return c s = socket.socket(socket.AF_UNIX); s.connect(sock)-for c in text:- s.sendall(f"sendkey {key(c)}\n".encode()); time.sleep(0.1)+for line in open(session):+ for c in re.findall(r'\{[A-Z]+\}|.|\n', line, re.S):+ s.sendall(f"sendkey {key(c)}\n".encode()); time.sleep(0.08)+ time.sleep(0.8) # let the command run time.sleep(0.5) s.sendall(f"screendump {out} -f png\n".encode()); time.sleep(1) s.sendall(b"quit\n"); time.sleep(0.5)@@ -50,23 +70,25 @@ wait $QPID || true echo "---- serial output ----" cat -v "$SERIAL_LOG"; echo # cat -v shows Backspace as ^H echo "-----------------------"-# Replay the serial output like a terminal would ("\b \b" = erase one-# character) and check the expected text is there.-if python3 - "$SERIAL_LOG" "$EXPECT" <<'PY'+# Replay the serial output like a terminal would (\b = erase one character)+# and check the expected text is there.+if python3 - "$SERIAL_LOG" "$@" <<'PY' import sys data = open(sys.argv[1], 'rb').read().decode(errors='replace').replace('\x08 \x08', '\x08') out = [] for ch in data: if ch == '\x08':- if out: out.pop()+ if out and out[-1] != '\n': out.pop() else: out.append(ch)-sys.exit(0 if sys.argv[2] in ''.join(out) else 1)+text = ''.join(out)+missing = [e for e in sys.argv[2:] if e not in text]+for e in missing: print(f"[screenshot] MISSING: {e}")+sys.exit(1 if missing else 0) PY then- echo "[screenshot] keyboard echo OK: typed '$TEXT' -> '$EXPECT'"+ echo "[screenshot] session OK: all $# expected texts found" else- echo "[screenshot] keyboard echo NOT found (expected '$EXPECT')" rm -f "$SERIAL_LOG" exit 1 fi
@@ -23,6 +23,9 @@ pub enum ElfError { NotExecutable, BadProgramHeader, OutsideUserSpace,+ /// Two segments with different permissions share a page (we can only+ /// give a page one set of permissions). A linker script problem.+ SharedPage, OutOfMemory, } @@ -104,6 +107,8 @@ pub fn load(data: &[u8], l4: PhysFrame) -> Result<u64, ElfError> { return Err(ElfError::OutsideUserSpace); } let nx_enabled = memory::nx_enabled();+ // The previous segment's last page and its flags.+ let mut previous: Option<(Page<Size4KiB>, PageTableFlags)> = None; for segment in segments { let seg = segment?;@@ -132,6 +137,14 @@ pub fn load(data: &[u8], l4: PhysFrame) -> Result<u64, ElfError> { // 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));+ // Segments come sorted by address. If this one starts on the page+ // where the previous one ended, map_user_page would keep the old+ // page's permissions: e.g. .bss on a read-only .rodata page, and the+ // program dies on its first write. Refuse such files up front.+ if previous.is_some_and(|(page, f)| page >= first && f != flags) {+ return Err(ElfError::SharedPage);+ }+ previous = Some((last, flags)); for page in Page::range_inclusive(first, last) { let frame = memory::map_user_page(l4, page, flags).ok_or(ElfError::OutOfMemory)?;
@@ -10,6 +10,7 @@ pub enum Errno { ENOEXEC = 8, // not a valid executable EBADF = 9, // bad file descriptor ECHILD = 10, // no such child process (wait)+ EAGAIN = 11, // try again (wait with WNOHANG: child still running) ENOMEM = 12, // out of memory EFAULT = 14, // bad address (pointer not in user space / not mapped) EBUSY = 16, // busy (e.g. removing a mount point)@@ -19,6 +20,7 @@ pub enum Errno { EINVAL = 22, // invalid argument EMFILE = 24, // too many open files ENOSPC = 28, // no space left on device+ ENOTTY = 25, // not a terminal (ioctl on a file) ESPIPE = 29, // can't seek (pipe, console) EPIPE = 32, // writing to a pipe nobody reads ERANGE = 34, // result doesn't fit (getcwd buffer too small)@@ -26,3 +28,16 @@ pub enum Errno { ENOSYS = 38, // no such system call ENOTEMPTY = 39, // directory not empty }++impl Errno {+ /// The errno for a negative syscall result like -2, if we know it.+ pub fn from_ret(ret: i64) -> Option<Errno> {+ use Errno::*;+ const ALL: [Errno; 23] = [+ ENOENT, EIO, E2BIG, ENOEXEC, EBADF, ECHILD, EAGAIN, ENOMEM, EFAULT, EBUSY, EEXIST, ENOTDIR,+ EISDIR, EINVAL, EMFILE, ENOTTY, ENOSPC, ESPIPE, EPIPE, ERANGE, ENAMETOOLONG, ENOSYS,+ ENOTEMPTY,+ ];+ ALL.into_iter().find(|&e| -(e as i64) == ret)+ }+}
@@ -63,11 +63,17 @@ pub trait File: Send + Sync { pub struct InodeFile { inode: Arc<dyn Inode>, offset: Mutex<usize>,+ /// O_APPEND: every write goes to the current end of the file (`>>`).+ append: bool, } impl InodeFile { pub fn open(inode: Arc<dyn Inode>) -> Result<Self, Errno> {- Ok(InodeFile { inode, offset: Mutex::new(0) })+ Ok(InodeFile { inode, offset: Mutex::new(0), append: false })+ }++ pub fn open_append(inode: Arc<dyn Inode>) -> Result<Self, Errno> {+ Ok(InodeFile { inode, offset: Mutex::new(0), append: true }) } } @@ -81,6 +87,9 @@ impl File for InodeFile { fn write(&self, buf: &[u8]) -> Result<usize, Errno> { let mut offset = self.offset.lock();+ if self.append {+ *offset = self.inode.size();+ } let n = self.inode.write_at(*offset, buf)?; *offset += n; Ok(n)
@@ -177,20 +177,11 @@ extern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: InterruptStackFr crate::vga_buffer::show_uptime(ticks / 18); } - // Headless runs: print a dot per tick to serial (proof the timer fires),- // then stop after ~5 seconds so scripts finish on their own. (Not done in- // normal runs, so the dots don't get mixed into your keyboard echo.)+ // Headless runs: a safety net in case the shell self-test hangs. #[cfg(feature = "headless")]- {- crate::serial_print!(".");- if ticks % 18 == 0 {- serial_println!(" {} ticks", ticks);- }- #[cfg(not(any(feature = "stack-overflow", feature = "page-fault")))]- if ticks >= 90 {- serial_println!("[ok] timer works, exiting");- crate::exit_qemu(crate::QemuExitCode::Success);- }+ if ticks == 60 * 18 {+ serial_println!("[failed] headless run still going after 60 s");+ crate::exit_qemu(crate::QemuExitCode::Failed); } // Tell the PIC we're done, or it will never send another timer interrupt.
@@ -1,5 +1,7 @@ //! The bootable kernel binary. All the real code is in the library-//! (src/lib.rs and friends); this file just runs the demos in order.+//! (src/lib.rs and friends); this file brings the machine up and starts the+//! shell. Build with `--features demos` to also see the step-by-step demos+//! of the earlier stages (paging, heap, threads, async, user programs). //! //! Boot flow: BIOS -> `bootloader` crate (switches to 64-bit long mode, sets up //! paging, maps all physical memory) -> `kernel_main` below, with a@@ -10,27 +12,34 @@ extern crate alloc; // Box, Vec, Rc... once the heap is set up -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, fs, kprintln, memory, println, process, serial_println, thread};-use rust_kernel::vga_buffer;-use x86_64::{VirtAddr, structures::paging::{Page, Translate}};+use rust_kernel::{fs, kprintln, println, process, serial_println, thread};+// Only the demos need these.+#[cfg(feature = "demos")]+use {+ alloc::{boxed::Box, rc::Rc, vec, vec::Vec},+ rust_kernel::{allocator, memory, vga_buffer},+ x86_64::{VirtAddr, structures::paging::{Page, Translate}},+}; // `entry_point!` defines the real `_start` for us and checks at compile time // that `kernel_main` has the right signature (takes `&'static BootInfo`). entry_point!(kernel_main); fn kernel_main(boot_info: &'static BootInfo) -> ! {- kprintln!("Hello from Tyler's Rust kernel!");+ kprintln!("Rustbelt {} -- a hobby x86_64 kernel in Rust", env!("CARGO_PKG_VERSION")); rust_kernel::init(); // GDT, IDT, PICs, enable interrupts - // Fire a breakpoint exception on purpose: the handler prints a message,- // returns, and we carry on as if nothing happened.- x86_64::instructions::interrupts::int3();- kprintln!("Survived the breakpoint, still running.");+ #[cfg(feature = "demos")]+ {+ // Fire a breakpoint exception on purpose: the handler prints a+ // message, returns, and we carry on as if nothing happened.+ x86_64::instructions::interrupts::int3();+ kprintln!("Survived the breakpoint, still running.");+ } #[cfg(feature = "stack-overflow")] {@@ -38,15 +47,28 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { stack_overflow(); } - paging_demo(boot_info); // also sets up the heap, needed from here on+ #[cfg(feature = "page-fault")]+ {+ kprintln!("[paging] writing to unmapped address 0xdeadbeaf...");+ unsafe { *(0xdeadbeaf as *mut u8) = 42 }; // -> page fault handler+ } - // --- Preemptive threads (thread.rs) ------------------------------------ // From now on the timer interrupt switches between these and the code- // below (the "main" thread) every tick (~55 ms).- thread::spawn("A", thread_a);- thread::spawn("B", thread_b);- thread::spawn("C", thread_c);- kprintln!("[thread] spawned threads A, B, C (status on the top line)");+ // Paging, heap and the frame allocator. The `demos` feature does the+ // same steps one at a time and prints what happens (chapters 8-11).+ #[cfg(feature = "demos")]+ paging_demo(boot_info);+ #[cfg(not(feature = "demos"))]+ rust_kernel::init_memory(boot_info);++ // --- Preemptive threads demo (thread.rs) --------------------------------+ // From now on the timer interrupt switches between threads every tick.+ #[cfg(feature = "demos")]+ {+ thread::spawn("A", thread_a);+ thread::spawn("B", thread_b);+ thread::spawn("C", thread_c);+ kprintln!("[thread] spawned threads A, B, C (status on the top line)");+ } // --- VFS, PCI, disks (fs/, pci.rs, block/) ------------------------------ fs::init();@@ -65,42 +87,84 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! { serial_println!("[vfs] mounted {} at {}", fs_type, path); } - // --- User mode (process.rs, syscall.rs) ---------------------------------- // The programs (built from user/*.rs) are ELF files on the FAT32 disk.- // Each gets its own address space and runs in ring 3.- const BIN: &str = "/mnt/disk0/bin";- if fs::lookup(BIN).is_err() {- kprintln!("[process] no {} -- was QEMU started with the disk image?", BIN);- }- let programs = [- "hello", // syscalls + ramfs demo, exits 0- "evil", // touches kernel memory -> killed- "spin", // busy loop, gets preempted- "demo", // stage 6: FAT file, cwd, getdents, spawn + wait, pipes- "echo", // reads keyboard lines via read(0)- ];- for name in programs {- let path = alloc::format!("{}/{}", BIN, name);- if let Err(e) = process::spawn_path(&path, process::SpawnArgs::kernel(name)) {- kprintln!("[process] could not start {}: {:?}", path, e);- }- }+ // --- The shell (user/sh.rs), run by its own kernel thread ---------------+ thread::spawn("shell", shell_supervisor); // --- Cooperative async tasks (task/) ------------------------------------ // The main thread runs the executor; the keyboard is just another task. let mut executor = Executor::new();+ #[cfg(feature = "demos")] executor.spawn(Task::new(example_task())); executor.spawn(Task::new(keyboard::print_keypresses()));- kprintln!("Type a line + Enter: the echo program (ring 3) reads it"); executor.run(); // never returns: runs tasks, `hlt`s when idle } +/// The shell and the programs live on the FAT32 disk.+const SHELL: &str = "/mnt/disk0/bin/sh";+/// Commands the shell runs in headless mode instead of reading the keyboard.+#[cfg(feature = "headless")]+const SELFTEST: &str = "/mnt/disk0/scripts/selftest.sh";++/// Kernel thread: start the shell, wait for it, start a new one when it+/// exits (like `init` respawning a login shell). In headless builds the+/// shell runs the self-test script and its exit code ends the QEMU run.+fn shell_supervisor() {+ #[cfg(feature = "demos")]+ run_demo_programs();++ if fs::lookup(SHELL).is_err() {+ kprintln!("[kernel] no {} -- was QEMU started with the disk image?", SHELL);+ #[cfg(feature = "headless")]+ rust_kernel::exit_qemu(rust_kernel::QemuExitCode::Failed);+ return;+ }+ loop {+ #[allow(unused_mut)]+ let mut args = process::SpawnArgs::kernel("sh");+ #[cfg(feature = "headless")]+ args.args.push(alloc::string::String::from(SELFTEST));+ let status = match process::spawn_path(SHELL, args) {+ Ok(shell) => process::wait(&shell),+ Err(e) => {+ kprintln!("[kernel] could not start {}: {:?}", SHELL, e);+ #[cfg(feature = "headless")]+ rust_kernel::exit_qemu(rust_kernel::QemuExitCode::Failed);+ return;+ }+ };+ #[cfg(feature = "headless")]+ {+ let ok = status == process::ExitStatus::Exited(0);+ serial_println!("[{}] shell self-test finished: {:?}", if ok { "ok" } else { "failed" }, status);+ serial_println!("[memory] {} frames in use", rust_kernel::memory::frames_in_use());+ rust_kernel::exit_qemu(if ok { rust_kernel::QemuExitCode::Success } else { rust_kernel::QemuExitCode::Failed });+ }+ kprintln!("[kernel] shell exited ({:?}); starting a new one", status);+ }+}++/// `demos` feature: the stage 5/6 user programs, one after the other.+#[cfg(feature = "demos")]+fn run_demo_programs() {+ for name in ["hello", "evil", "spin", "demo"] {+ let path = alloc::format!("/mnt/disk0/bin/{}", name);+ match process::spawn_path(&path, process::SpawnArgs::kernel(name)) {+ Ok(p) => {+ process::wait(&p);+ }+ Err(e) => kprintln!("[process] could not start {}: {:?}", path, e),+ }+ }+}+ // --- async demo ------------------------------------------------------------- +#[cfg(feature = "demos")] async fn async_number() -> u32 { 42 } +#[cfg(feature = "demos")] async fn example_task() { let number = async_number().await; kprintln!("[async] async number: {}", number);@@ -112,6 +176,7 @@ async fn example_task() { // --- thread demo ------------------------------------------------------------ +#[cfg(feature = "demos")] /// Threads A and B: sleep a bit, count, show the count on the top line. /// They print their first 5 counts to serial too (more would drown out the /// keyboard echo in the terminal).@@ -125,14 +190,17 @@ fn counter_thread(name: &str, period_ticks: u64, col: usize) { } } +#[cfg(feature = "demos")] fn thread_a() { counter_thread("A", 5, 0); } +#[cfg(feature = "demos")] fn thread_b() { counter_thread("B", 7, 10); } +#[cfg(feature = "demos")] /// Thread C never sleeps or yields: a pure busy loop. It still only gets /// its share of the CPU, because the timer preempts it. It finishes after a /// while to show that threads can exit.@@ -149,6 +217,7 @@ fn thread_c() { vga_buffer::print_at(0, 20, format_args!(" C: done ")); } +#[cfg(feature = "demos")] /// Chapters "Introduction to Paging", "Paging Implementation", "Heap Allocation". fn paging_demo(boot_info: &'static BootInfo) { use x86_64::registers::control::Cr3;@@ -158,12 +227,6 @@ fn paging_demo(boot_info: &'static BootInfo) { let (level_4_frame, _) = Cr3::read(); kprintln!("[paging] CR3 (level 4 table) at phys {:#x}", level_4_frame.start_address().as_u64()); - #[cfg(feature = "page-fault")]- {- kprintln!("[paging] writing to unmapped address 0xdeadbeaf...");- unsafe { *(0xdeadbeaf as *mut u8) = 42 }; // -> page fault handler- }- // --- Paging Implementation -------------------------------------------- let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); serial_println!("[paging] physical memory mapped at virt {:#x}", phys_mem_offset.as_u64());
@@ -135,9 +135,51 @@ 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.+/// Frames given back with `free_frame`, as a linked list threaded through+/// the free frames themselves: the first 8 bytes of each free frame hold the+/// physical address of the next one (0 = end). No heap memory needed.+struct FreeList {+ head: u64,+ len: usize,+}+static FREE_FRAMES: Mutex<FreeList> = Mutex::new(FreeList { head: 0, len: 0 });+/// Frames handed out by the boot allocator so far (for statistics).+static BOOT_FRAMES_USED: AtomicU64 = AtomicU64::new(0);++/// One physical frame (contents undefined), or None if out of memory / not+/// initialized. Recycled frames first, then fresh ones from the memory map. pub fn alloc_frame() -> Option<PhysFrame> {- FRAME_ALLOCATOR.lock().as_mut()?.allocate_frame()+ {+ let mut free = FREE_FRAMES.lock();+ if free.head != 0 {+ let frame = PhysFrame::containing_address(PhysAddr::new(free.head));+ free.head = unsafe { *phys_to_virt(frame.start_address()).as_ptr::<u64>() };+ free.len -= 1;+ return Some(frame);+ }+ }+ fresh_frame()+}++/// A never-used frame straight from the boot allocator.+fn fresh_frame() -> Option<PhysFrame> {+ let frame = FRAME_ALLOCATOR.lock().as_mut()?.allocate_frame()?;+ BOOT_FRAMES_USED.fetch_add(1, Ordering::Relaxed);+ Some(frame)+}++/// Give a frame back. The caller promises nothing uses it any more (no page+/// table maps it, no device writes to it).+pub fn free_frame(frame: PhysFrame) {+ let mut free = FREE_FRAMES.lock();+ unsafe { *phys_to_virt(frame.start_address()).as_mut_ptr::<u64>() = free.head };+ free.head = frame.start_address().as_u64();+ free.len += 1;+}++/// How many frames are in use right now (handed out and not freed).+pub fn frames_in_use() -> usize {+ BOOT_FRAMES_USED.load(Ordering::Relaxed) as usize - FREE_FRAMES.lock().len } /// `FrameAllocator` adapter around the global allocator (for `map_to`, which@@ -176,10 +218,11 @@ pub fn nx_enabled() -> bool { /// if a run gets broken (end of a memory region) we start over and simply /// leak the frames we skipped. Fine for a few boot-time allocations. pub fn alloc_contiguous_frames(count: usize) -> Option<PhysAddr> {- let mut start = alloc_frame()?.start_address();+ // Fresh frames only: recycled ones come back in no particular order.+ let mut start = fresh_frame()?.start_address(); let mut got = 1; while got < count {- let frame = alloc_frame()?.start_address();+ let frame = fresh_frame()?.start_address(); if frame == start + (got as u64) * 4096 { got += 1; } else {@@ -228,6 +271,38 @@ pub fn new_address_space() -> Option<PhysFrame> { Some(frame) } +/// Free everything a process's address space owns: every user page's frame,+/// the page tables of the user slot (levels 3, 2, 1) and the level-4 table.+/// The kernel entries are shared with every other address space, so they're+/// left alone.+///+/// Unsafe: `l4` must not be in use by any thread (not loaded in CR3, and the+/// thread that ran in it must have finished).+pub unsafe fn free_address_space(l4: PhysFrame) {+ // A page table, reached through the physical-memory map.+ fn table(frame: PhysFrame) -> &'static PageTable {+ unsafe { &*phys_to_virt(frame.start_address()).as_ptr() }+ }+ // The used entries' frames (user space only has 4 KiB pages, no huge ones).+ fn used(t: &'static PageTable) -> impl Iterator<Item = PhysFrame> {+ t.iter().filter(|e| !e.is_unused()).filter_map(|e| e.frame().ok())+ }+ let user_l4_entry = &table(l4)[USER_L4_INDEX];+ if let Ok(l3) = user_l4_entry.frame() {+ for l2 in used(table(l3)) {+ for l1 in used(table(l2)) {+ for page in used(table(l1)) {+ free_frame(page);+ }+ free_frame(l1);+ }+ free_frame(l2);+ }+ free_frame(l3);+ }+ free_frame(l4);+}+ /// 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
@@ -37,7 +37,7 @@ use core::sync::atomic::{AtomicU64, Ordering}; use spin::Mutex; use x86_64::VirtAddr; use x86_64::instructions::interrupts::without_interrupts;-use x86_64::structures::paging::{Page, PageTableFlags, Size4KiB};+use x86_64::structures::paging::{Page, PageTableFlags, PhysFrame, Size4KiB}; /// The user stack: 16 KiB, ending one (unmapped) page below USER_END. pub const USER_STACK_TOP: u64 = USER_END - 0x1000;@@ -78,6 +78,8 @@ pub struct Process { /// Current directory (always a normalized absolute path). pub cwd: Mutex<String>, entry: u64,+ /// Level-4 page table of its address space (freed when it's reaped).+ l4: PhysFrame, args: Vec<String>, fds: Mutex<Vec<Option<Arc<dyn File>>>>, /// Locked with interrupts off only (it's read in a wait_until condition).@@ -130,8 +132,19 @@ pub fn spawn(name: &str, elf_image: &[u8]) -> Result<Arc<Process>, ElfError> { /// Load `elf_image` into a new address space and start it. pub fn spawn_image(name: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc<Process>, ElfError> { let l4 = memory::new_address_space().ok_or(ElfError::OutOfMemory)?;- let entry = elf::load(elf_image, l4)?;+ match load_into(elf_image, l4) {+ Ok(entry) => Ok(start_process(name, l4, entry, spawn)),+ Err(e) => {+ // Nobody runs in this address space yet: give its memory back.+ unsafe { memory::free_address_space(l4) };+ Err(e)+ }+ }+} +/// Load the program and map its stack; returns the entry point.+fn load_into(elf_image: &[u8], l4: PhysFrame) -> Result<u64, ElfError> {+ let entry = elf::load(elf_image, l4)?; let mut stack_flags = PageTableFlags::WRITABLE; if memory::nx_enabled() { stack_flags |= PageTableFlags::NO_EXECUTE;@@ -141,7 +154,11 @@ pub fn spawn_image(name: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc for page in Page::range_inclusive(first, last) { memory::map_user_page(l4, page, stack_flags).ok_or(ElfError::OutOfMemory)?; }+ Ok(entry)+} +/// Create the `Process` and its thread.+fn start_process(name: &str, l4: PhysFrame, entry: u64, spawn: SpawnArgs) -> Arc<Process> { let [stdin, stdout, stderr] = spawn.stdio; let process = Arc::new(Process { pid: NEXT_PID.fetch_add(1, Ordering::Relaxed),@@ -150,6 +167,7 @@ pub fn spawn_image(name: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc trace: spawn.trace, cwd: Mutex::new(spawn.cwd), entry,+ l4, args: spawn.args, fds: Mutex::new(vec![Some(stdin), Some(stdout), Some(stderr)]), status: Mutex::new(None),@@ -160,7 +178,7 @@ pub fn spawn_image(name: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc // Thread names are &'static str; leaking a few bytes per process is fine here. let thread_name: &'static str = Box::leak(String::from(name).into_boxed_str()); thread::spawn_with(thread_name, user_thread_entry, l4, Some(process.clone()));- Ok(process)+ process } /// First code of every process thread (still ring 0, but already running@@ -279,16 +297,23 @@ pub fn exit_current(status: ExitStatus) -> ! { // Close all files (a pipe's reader sees EOF once its writers are gone). let files = core::mem::take(&mut *process.fds.lock()); drop(files);+ crate::tty::process_exited(process.pid);+ // Serial only: on screen the shell reports exit codes itself. match status { ExitStatus::Exited(code) => {- crate::kprintln!("[process] pid {} ({}) exited with code {}", process.pid, process.name, code)+ serial_println!("[process] pid {} ({}) exited with code {}", process.pid, process.name, code) } ExitStatus::Killed(why) => { crate::kprintln!("[process] pid {} ({}) killed: {}", process.pid, process.name, why) } }- without_interrupts(|| *process.status.lock() = Some(status));+ // From here until this thread is gone, no preemption: once the status+ // is set, the parent may reap us and free our address space -- which+ // must not happen while we could still be switched back to.+ x86_64::instructions::interrupts::disable();+ *process.status.lock() = Some(status); process.exited.wake_all(); // the parent may be blocked in wait()+ drop(process); } thread::exit_current() }@@ -297,12 +322,29 @@ pub fn exit_current(status: ExitStatus) -> ! { /// the process table. Used by the kernel and by the `wait` syscall. pub fn wait(process: &Process) -> ExitStatus { let status = process.exited.wait_until(|| *process.status.lock());- PROCESSES.lock().remove(&process.pid);+ reap(process); status } -/// The `wait(pid)` syscall: only a process's parent may wait for it.-pub fn wait_child(parent: u64, pid: u64) -> Result<ExitStatus, Errno> {+/// Remove an ended process from the table and free its memory.+fn reap(process: &Process) {+ if PROCESSES.lock().remove(&process.pid).is_some() {+ // Its thread has finished (status is set with interrupts off right+ // before the thread exits), so nothing runs in this address space.+ unsafe { memory::free_address_space(process.l4) };+ }+}++/// `wait` flag: don't block; `EAGAIN` if the child is still running.+pub const WNOHANG: u64 = 1;++/// The `wait(pid, flags)` syscall: only a process's parent may wait for it.+pub fn wait_child(parent: u64, pid: u64, flags: u64) -> Result<ExitStatus, Errno> { let child = get(pid).filter(|c| c.parent == parent).ok_or(Errno::ECHILD)?;+ if flags & WNOHANG != 0 {+ let status = child.status().ok_or(Errno::EAGAIN)?;+ reap(&child);+ return Ok(status);+ } Ok(wait(&child)) }
@@ -18,12 +18,14 @@ //! | 4 | stat | path, path_len, *Stat | our own small `Stat {kind, size}` | //! | 5 | fstat | fd, *Stat | | //! | 8 | lseek | fd, offset, whence | SEEK_SET/CUR/END = 0/1/2 |+//! | 16 | ioctl | fd, request, arg | only TTY_SET_RAW (ours): console raw/cooked mode | //! | 22 | pipe | *[u32; 2] | [read end, write end] | //! | 24 | sched_yield | | | //! | 35 | sleep_ms | milliseconds | Linux's nr 35 is nanosleep(timespec); ours is simpler | //! | 39 | getpid | | | //! | 60 | exit | code | |-//! | 61 | wait | pid | returns the exit code (139 if killed). Linux wait4 is fancier |+//! | 61 | wait | pid, flags | returns the exit code (139 if killed); WNOHANG = 1 -> EAGAIN if running. Linux wait4 is fancier |+//! | 63 | uname | *utsname | Linux's `struct utsname` (6 x 65-byte strings) | //! | 79 | getcwd | buf, len | returns the length (no NUL) | //! | 80 | chdir | path, path_len | | //! | 83 | mkdir | path, path_len | |@@ -31,6 +33,7 @@ //! | 87 | unlink | path, path_len | files only | //! | 217 | getdents64 | fd, buf, len | Linux `struct linux_dirent64` records | //! | 500 | spawn | path, path_len, argv, argv_len, *SpawnOptions | ours: start a program, returns its pid (see process.rs for why not fork/exec) |+//! | 501 | mounts | buf, len | ours: "path fstype\n" per mount (Linux: read /proc/mounts) | //! //! spawn's `argv` is all arguments separated by NUL bytes ("ls\0-l\0/bin"); //! empty means just [program name]. `SpawnOptions` (or a null pointer for@@ -69,12 +72,14 @@ pub const SYS_CLOSE: u64 = 3; pub const SYS_STAT: u64 = 4; pub const SYS_FSTAT: u64 = 5; pub const SYS_LSEEK: u64 = 8;+pub const SYS_IOCTL: u64 = 16; pub const SYS_PIPE: u64 = 22; pub const SYS_SCHED_YIELD: u64 = 24; pub const SYS_SLEEP_MS: u64 = 35; pub const SYS_GETPID: u64 = 39; pub const SYS_EXIT: u64 = 60; pub const SYS_WAIT: u64 = 61;+pub const SYS_UNAME: u64 = 63; pub const SYS_GETCWD: u64 = 79; pub const SYS_CHDIR: u64 = 80; pub const SYS_MKDIR: u64 = 83;@@ -82,9 +87,13 @@ pub const SYS_RMDIR: u64 = 84; pub const SYS_UNLINK: u64 = 87; pub const SYS_GETDENTS64: u64 = 217; pub const SYS_SPAWN: u64 = 500;+pub const SYS_MOUNTS: u64 = 501; pub const O_CREAT: u64 = 0o100; pub const O_TRUNC: u64 = 0o1000;+pub const O_APPEND: u64 = 0o2000;+/// ioctl request (ours): arg 1 = raw mode, 0 = cooked; returns the old mode.+pub const TTY_SET_RAW: u64 = 1; pub const SPAWN_TRACE: u64 = 1; /// Name of a syscall number (for tracing).@@ -97,12 +106,14 @@ pub fn name(nr: u64) -> &'static str { SYS_STAT => "stat", SYS_FSTAT => "fstat", SYS_LSEEK => "lseek",+ SYS_IOCTL => "ioctl", SYS_PIPE => "pipe", SYS_SCHED_YIELD => "sched_yield", SYS_SLEEP_MS => "sleep_ms", SYS_GETPID => "getpid", SYS_EXIT => "exit", SYS_WAIT => "wait",+ SYS_UNAME => "uname", SYS_GETCWD => "getcwd", SYS_CHDIR => "chdir", SYS_MKDIR => "mkdir",@@ -110,6 +121,7 @@ pub fn name(nr: u64) -> &'static str { SYS_UNLINK => "unlink", SYS_GETDENTS64 => "getdents64", SYS_SPAWN => "spawn",+ SYS_MOUNTS => "mounts", _ => "?", } }@@ -223,6 +235,7 @@ extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: SYS_STAT => sys_stat(a1, a2, a3), SYS_FSTAT => sys_fstat(a1, a2), SYS_LSEEK => current().and_then(|p| p.file(a1)).and_then(|f| f.seek(a2 as i64, a3)),+ SYS_IOCTL => sys_ioctl(a1, a2, a3), SYS_PIPE => sys_pipe(a1), SYS_SCHED_YIELD => { thread::yield_now();@@ -235,7 +248,8 @@ extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: } SYS_GETPID => current().map(|p| p.pid), SYS_EXIT => process::exit_current(process::ExitStatus::Exited(a1 as i64)),- SYS_WAIT => current().and_then(|p| process::wait_child(p.pid, a1)).map(|s| s.code() as u64),+ SYS_WAIT => current().and_then(|p| process::wait_child(p.pid, a1, a2)).map(|s| s.code() as u64),+ SYS_UNAME => sys_uname(a1), SYS_GETCWD => sys_getcwd(a1, a2), SYS_CHDIR => sys_chdir(a1, a2), SYS_MKDIR => user_path(a1, a2).and_then(|p| fs::create(&p, InodeKind::Directory)).map(|_| 0),@@ -243,6 +257,7 @@ extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: SYS_UNLINK => user_path(a1, a2).and_then(|p| fs::remove(&p, false)).map(|_| 0), SYS_GETDENTS64 => sys_getdents64(a1, a2, a3), SYS_SPAWN => sys_spawn(a1, a2, a3, a4, a5),+ SYS_MOUNTS => sys_mounts(a1, a2), _ => Err(Errno::ENOSYS), }; let ret = match result {@@ -251,11 +266,52 @@ extern "C" fn syscall_dispatch(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: }; if trace { let pid = current().map(|p| p.pid).unwrap_or(0);- crate::kprintln!("[trace {}] {}({:#x}, {:#x}, {:#x}) = {}", pid, name(nr), a1, a2, a3, ret);+ crate::kprintln!("[trace {}] {}", pid, trace_line(nr, [a1, a2, a3, a4], ret)); } ret } +/// One syscall in strace style: `open("/mnt/disk0/readme.txt", 0) = 3`.+/// Strings and buffers are read back from user memory (after the call, so+/// `read` shows what it got), shortened to 40 bytes.+fn trace_line(nr: u64, a: [u64; 4], ret: i64) -> String {+ use alloc::format;+ let text = |ptr: u64, len: u64| -> String {+ let Ok(bytes) = user_slice(ptr, len.min(40)) else { return String::from("<bad pointer>") };+ let mut s = String::from("\"");+ for &b in bytes {+ match b {+ b'\n' => s.push_str("\\n"),+ b'"' => s.push_str("\\\""),+ 0x20..=0x7e => s.push(b as char),+ _ => s.push_str(&format!("\\x{:02x}", b)),+ }+ }+ s.push('"');+ if len > 40 {+ s.push_str("...");+ }+ s+ };+ let args = match nr {+ SYS_READ => format!("{}, {}, {}", a[0], if ret > 0 { text(a[1], ret as u64) } else { String::from("\"\"") }, a[2]),+ SYS_WRITE => format!("{}, {}, {}", a[0], text(a[1], a[2]), a[2]),+ SYS_OPEN => format!("{}, {:#o}", text(a[0], a[1]), a[2]),+ SYS_STAT => format!("{}, {:#x}", text(a[0], a[1]), a[2]),+ SYS_CHDIR | SYS_MKDIR | SYS_RMDIR | SYS_UNLINK => text(a[0], a[1]),+ SYS_SPAWN => format!("{}, {}", text(a[0], a[1]), text(a[2], a[3])),+ SYS_SCHED_YIELD | SYS_GETPID => String::new(),+ SYS_CLOSE | SYS_SLEEP_MS | SYS_EXIT => format!("{}", a[0] as i64),+ SYS_PIPE | SYS_UNAME => format!("{:#x}", a[0]),+ SYS_FSTAT | SYS_WAIT | SYS_GETCWD | SYS_MOUNTS => format!("{}, {:#x}", a[0], a[1]),+ _ => format!("{}, {:#x}, {:#x}", a[0], a[1], a[2]),+ };+ match Errno::from_ret(ret) {+ Some(e) if ret < 0 => format!("{}({}) = {} {:?}", name(nr), args, ret, e),+ _ => format!("{}({}) = {}", name(nr), args, ret),+ }+}+ fn current() -> Result<Arc<process::Process>, Errno> { thread::current_process().ok_or(Errno::EINVAL) }@@ -322,8 +378,8 @@ fn sys_open(path_ptr: u64, path_len: u64, flags: u64) -> Result<u64, Errno> { if flags & O_TRUNC != 0 && inode.kind() == InodeKind::File { inode.truncate()?; }- let file = Arc::new(InodeFile::open(inode)?);- current()?.add_file(file)+ let file = if flags & O_APPEND != 0 { InodeFile::open_append(inode)? } else { InodeFile::open(inode)? };+ current()?.add_file(Arc::new(file)) } fn sys_stat(path_ptr: u64, path_len: u64, stat_ptr: u64) -> Result<u64, Errno> {@@ -407,3 +463,48 @@ fn sys_spawn(path_ptr: u64, path_len: u64, argv_ptr: u64, argv_len: u64, opts_pt drop(parent); process::spawn_path(&path, spawn).map(|child| child.pid) }++fn sys_ioctl(fd: u64, request: u64, arg: u64) -> Result<u64, Errno> {+ let process = current()?;+ if process.file(fd)?.stat()?.kind != crate::fs::file::KIND_CONSOLE {+ return Err(Errno::ENOTTY);+ }+ match request {+ TTY_SET_RAW => Ok(crate::tty::set_raw(arg != 0, process.pid) as u64),+ _ => Err(Errno::EINVAL),+ }+}++/// Linux's `struct utsname`: six NUL-terminated strings of 65 bytes each.+fn sys_uname(buf: u64) -> Result<u64, Errno> {+ let fields = [+ "Rustbelt", // sysname (`uname -s`)+ "qemu", // nodename (hostname)+ env!("CARGO_PKG_VERSION"), // release (`uname -r`)+ "stage 7: shell", // version+ "x86_64", // machine+ "(none)", // domainname+ ];+ let out = user_slice_mut(buf, 6 * 65)?;+ out.fill(0);+ for (i, field) in fields.iter().enumerate() {+ out[i * 65..i * 65 + field.len()].copy_from_slice(field.as_bytes());+ }+ Ok(0)+}++/// The mount table as text, one "path fstype" line per mount.+fn sys_mounts(buf: u64, len: u64) -> Result<u64, Errno> {+ let mut text = String::new();+ for (path, fs_type) in fs::mounts() {+ text.push_str(&path);+ text.push(' ');+ text.push_str(fs_type);+ text.push('\n');+ }+ if (len as usize) < text.len() {+ return Err(Errno::ERANGE);+ }+ user_slice_mut(buf, text.len() as u64)?.copy_from_slice(text.as_bytes());+ Ok(text.len() as u64)+}
@@ -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};+use crate::serial_print; use conquer_once::spin::OnceCell; use core::{ pin::Pin,@@ -87,8 +87,16 @@ pub async fn print_keypresses() { let mut keyboard = Keyboard::new(ScancodeSet1::new(), layouts::Us104Key, HandleControl::Ignore); while let Some(scancode) = scancodes.next().await {- if let Ok(Some(key_event)) = keyboard.add_byte(scancode)- && let Some(key) = keyboard.process_keyevent(key_event)+ if let Ok(Some(mut key_event)) = keyboard.add_byte(scancode)+ && let Some(key) = keyboard.process_keyevent({+ // pc-keyboard 0.7 quirk: scancode set 1 calls the `\ |` key+ // (0x2B) Oem7, but the US layout only knows it as Oem5 --+ // without this, the shell's pipe key would type nothing.+ if key_event.code == KeyCode::Oem7 {+ key_event.code = KeyCode::Oem5;+ }+ key_event+ }) { match key { // Characters (incl. Backspace and Enter) go to the tty's@@ -100,10 +108,8 @@ pub async fn print_keypresses() { KeyCode::LShift | KeyCode::RShift | KeyCode::LControl | KeyCode::RControl | KeyCode::LAlt | KeyCode::RAltGr | KeyCode::CapsLock, ) => {}- DecodedKey::RawKey(key) => {- print!("<{:?}>", key);- serial_print!("<{:?}>", key);- }+ // Arrows etc.: the tty decides (escape sequence in raw mode).+ DecodedKey::RawKey(key) => crate::tty::input_key(key), } } }
@@ -1,10 +1,20 @@-//! The console "tty": keyboard input with a tiny line discipline, and the-//! `File` behind file descriptors 0, 1 and 2.+//! The console "tty": keyboard input, 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.+//! Two input modes, like a Linux terminal:+//!+//! * **cooked** (canonical, the default): typed characters are echoed and+//! collected in a line buffer (Backspace edits it); only when Enter is+//! pressed does the whole line become readable from fd 0. So `read(0)`+//! gets a complete, already-edited line. Programs like `cat` use this.+//! * **raw**: every key goes to the reader immediately and nothing is echoed;+//! arrow keys arrive as ANSI escape sequences (`ESC [ A` = up). The shell+//! switches to raw while it edits its command line, so it can do its own+//! echo, Backspace and up-arrow history. (Linux: `tcsetattr` / `ioctl`.)+//!+//! Output (`Console::write`) understands two control characters besides+//! '\n': '\x08' erases the character before the cursor, and '\x0c' (form+//! feed) clears the screen. use crate::errno::Errno; use crate::fs::file::{File, KIND_CONSOLE, Stat};@@ -12,23 +22,63 @@ use crate::thread::WaitQueue; use crate::{print, serial_print, vga_buffer}; use alloc::string::String; use conquer_once::spin::OnceCell;+use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use crossbeam_queue::ArrayQueue;+use pc_keyboard::KeyCode; use spin::Mutex; -/// The line being typed (not yet readable).+/// The line being typed in cooked mode (not yet readable). static LINE: Mutex<String> = Mutex::new(String::new());-/// Finished lines, as bytes, waiting for `read(0, ...)`.+/// Bytes waiting for `read(0, ...)`: finished lines (cooked) or keys (raw). static INPUT: OnceCell<ArrayQueue<u8>> = OnceCell::uninit();+/// Raw mode on?+static RAW: AtomicBool = AtomicBool::new(false);+/// pid of the process that switched raw mode on (0 = none). If it exits+/// without switching back, the console goes back to cooked mode.+static RAW_OWNER: AtomicU64 = AtomicU64::new(0); -/// Threads blocked in `Console::read`, waiting for a line.+/// Threads blocked in `Console::read`, waiting for input. static INPUT_WAIT: WaitQueue = WaitQueue::new(); fn input() -> &'static ArrayQueue<u8> { INPUT.get_or_init(|| ArrayQueue::new(4096)) } +/// Switch between raw (`true`) and cooked mode for process `pid`. Returns+/// the old mode.+pub fn set_raw(raw: bool, pid: u64) -> bool {+ LINE.lock().clear(); // a half-typed cooked line doesn't survive a switch+ RAW_OWNER.store(if raw { pid } else { 0 }, Ordering::SeqCst);+ RAW.swap(raw, Ordering::SeqCst)+}++/// A process ended: if it left the console in raw mode, undo that (so the+/// next program, or a respawned shell, gets normal line input).+pub fn process_exited(pid: u64) {+ if RAW_OWNER.compare_exchange(pid, 0, Ordering::SeqCst, Ordering::SeqCst).is_ok() {+ RAW.store(false, Ordering::SeqCst);+ }+}++pub fn is_raw() -> bool {+ RAW.load(Ordering::SeqCst)+}++/// Make `bytes` readable and wake readers. (Queue full: drop -- nobody reads.)+fn deliver(bytes: &[u8]) {+ for &b in bytes {+ let _ = input().push(b);+ }+ INPUT_WAIT.wake_all();+}+ /// Called by the keyboard task for every typed character. pub fn input_char(c: char) {+ if is_raw() {+ let mut utf8 = [0; 4];+ deliver(c.encode_utf8(&mut utf8).as_bytes());+ return;+ } let mut line = LINE.lock(); match c { '\u{8}' => {@@ -41,11 +91,9 @@ pub fn input_char(c: char) { '\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.push('\n');+ deliver(line.as_bytes()); line.clear();- INPUT_WAIT.wake_all(); // a reader may be blocked in Console::read } c => { print!("{}", c);@@ -55,22 +103,41 @@ pub fn input_char(c: char) { } } -/// The console as a file: reading gets keyboard lines, writing prints to the+/// Called by the keyboard task for keys that aren't characters (arrows...).+pub fn input_key(key: KeyCode) {+ if is_raw() {+ // The ANSI/VT100 sequences a Linux terminal would send.+ let seq: &[u8] = match key {+ KeyCode::ArrowUp => b"\x1b[A",+ KeyCode::ArrowDown => b"\x1b[B",+ KeyCode::ArrowRight => b"\x1b[C",+ KeyCode::ArrowLeft => b"\x1b[D",+ KeyCode::Home => b"\x1b[H",+ KeyCode::End => b"\x1b[F",+ KeyCode::Delete => b"\x1b[3~",+ _ => return,+ };+ deliver(seq);+ } else {+ print!("<{:?}>", key);+ serial_print!("<{:?}>", key);+ }+}++/// The console as a file: reading gets keyboard input, 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').+ /// Blocks until input is available. Cooked: returns up to `buf.len()`+ /// bytes of a line, stopping after its '\n'. Raw: whatever keys are there. fn read(&self, buf: &mut [u8]) -> Result<usize, Errno> { if buf.is_empty() { return Ok(0); }- // Block until a line arrives: the thread is parked on INPUT_WAIT- // (using no CPU) and `input_char` wakes it when Enter is pressed.- // Lines are queued whole, so once the first byte is there the rest- // of the line is too. (The closure runs with interrupts off; the- // ArrayQueue is lock-free, so that's fine.)+ // Block until input arrives: the thread is parked on INPUT_WAIT+ // (using no CPU) and `deliver` wakes it. The closure runs with+ // interrupts off; the ArrayQueue is lock-free, so that's fine. Ok(INPUT_WAIT.wait_until(|| { let mut n = 0; while n < buf.len() {@@ -86,10 +153,25 @@ impl File for Console { } fn write(&self, buf: &[u8]) -> Result<usize, Errno> {- // Print valid UTF-8 as is, anything else as '?'.+ // Print valid UTF-8 as is, anything else as '?'. Control characters+ // are handled one by one. for chunk in buf.utf8_chunks() {- print!("{}", chunk.valid());- serial_print!("{}", chunk.valid());+ for piece in chunk.valid().split_inclusive(['\x08', '\x0c']) {+ let (text, control) = match piece.strip_suffix(['\x08', '\x0c']) {+ Some(text) => (text, piece.chars().last()),+ None => (piece, None),+ };+ print!("{}", text);+ serial_print!("{}", text);+ match control {+ Some('\x08') => {+ vga_buffer::backspace();+ serial_print!("\x08");+ }+ Some(_) => vga_buffer::clear_screen(), // serial: nothing to clear+ None => {}+ }+ } if !chunk.invalid().is_empty() { print!("?"); serial_print!("?");
@@ -173,6 +173,18 @@ pub fn backspace() { }); } +/// Clear the screen (rows 1..; row 0 is the status line) and start+/// writing at the left of the bottom row again.+pub fn clear_screen() {+ x86_64::instructions::interrupts::without_interrupts(|| {+ let mut writer = WRITER.lock();+ for row in 1..BUFFER_HEIGHT {+ writer.clear_row(row);+ }+ writer.column_position = 0;+ });+}+ #[doc(hidden)] pub fn _print(args: fmt::Arguments) { use core::fmt::Write;
@@ -35,7 +35,7 @@ fn fat_volume_is_mounted() { assert!(fs::mounts().iter().any(|(path, kind)| path == "/mnt/disk0" && *kind == "fat32")); let mut names: Vec<String> = fs::lookup("/mnt/disk0").unwrap().list().unwrap().into_iter().map(|e| e.name).collect(); names.sort(); // FAT keeps directory entries in creation order- assert_eq!(names, vec!["bin", "notes", "readme.txt"]);+ assert_eq!(names, vec!["bin", "notes", "readme.txt", "scripts"]); } #[test_case]
@@ -0,0 +1,218 @@+//! The shell: its parser (user/shparse.rs, compiled right into this test)+//! and a scripted session of the real /bin/sh with its output in a pipe.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rust_kernel::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++#[path = "../user/shparse.rs"]+mod shparse;++use alloc::{string::String, sync::Arc, vec, vec::Vec};+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rust_kernel::fs::{self, InodeKind, file::File};+use rust_kernel::memory;+use rust_kernel::process::{self, ExitStatus, SpawnArgs};+use shparse::{Token, Word, parse, tokenize};++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+ rust_kernel::init();+ rust_kernel::init_memory(boot_info);+ fs::init();+ rust_kernel::init_storage();+ test_main();+ rust_kernel::hlt_loop();+}++fn w(text: &str) -> Token {+ Token::Word(Word { text: String::from(text), quoted: false })+}+fn q(text: &str) -> Token {+ Token::Word(Word { text: String::from(text), quoted: true })+}++// --- tokenizer -----------------------------------------------------------++#[test_case]+fn tokenizer_splits_words_and_operators() {+ // Operators need no spaces around them.+ assert_eq!(+ tokenize("ls -l|wc >>out.txt <in &").unwrap(),+ vec![w("ls"), w("-l"), Token::Pipe, w("wc"), Token::Append, w("out.txt"), Token::In, w("in"), Token::Background]+ );+ assert_eq!(tokenize(" ").unwrap(), vec![]);+}++#[test_case]+fn tokenizer_quotes_and_comments() {+ // Quotes group (and can be glued to plain text); they hide operators.+ assert_eq!(tokenize(r#"echo "a | b" c'd e'f"#).unwrap(), vec![w("echo"), q("a | b"), q("cd ef")]);+ assert_eq!(tokenize("ls # list files").unwrap(), vec![w("ls")]);+ assert_eq!(tokenize("echo a#b").unwrap(), vec![w("echo"), w("a#b")]); // # only starts a word+ assert!(tokenize("echo \"oops").is_err());+}++// --- parser ----------------------------------------------------------------++#[test_case]+fn parser_builds_pipelines_with_redirections() {+ let p = parse("cat < in.txt | grep x | wc -l >> out.txt &").unwrap().unwrap();+ assert!(p.background);+ assert_eq!(p.commands.len(), 3);+ let names: Vec<&str> = p.commands.iter().map(|c| c.words[0].text.as_str()).collect();+ assert_eq!(names, ["cat", "grep", "wc"]);+ assert_eq!(p.commands[0].stdin.as_ref().unwrap().text, "in.txt");+ let (out, append) = p.commands[2].stdout.as_ref().unwrap();+ assert_eq!((out.text.as_str(), *append), ("out.txt", true));+ assert_eq!(p.commands[1].words.len(), 2);++ let p = parse("echo hi > f").unwrap().unwrap();+ assert!(!p.background);+ assert_eq!(p.commands[0].stdout.as_ref().unwrap().1, false);+ assert_eq!(parse("# just a comment").unwrap(), None);+ assert_eq!(parse("").unwrap(), None);+}++#[test_case]+fn parser_rejects_broken_lines() {+ for bad in ["| wc", "ls |", "ls | | wc", "ls >", "cat <", "ls & wc", "&", "> out"] {+ assert!(parse(bad).is_err(), "should fail: {}", bad);+ }+}++// --- drive letters -----------------------------------------------------------++const MOUNTS: &str = "/ ramfs\n/tmp ramfs\n/mnt/disk0 fat32\n/mnt/disk1 fat32\n";++#[test_case]+fn drive_letters_are_assigned_in_mount_order() {+ let drives = shparse::assign_drives(MOUNTS);+ let summary: Vec<(char, &str, &str)> = drives.iter().map(|d| (d.letter, d.path.as_str(), d.fs.as_str())).collect();+ // /tmp gets no letter: it's C:/tmp.+ assert_eq!(summary, [('C', "/", "ramfs"), ('D', "/mnt/disk0", "fat32"), ('E', "/mnt/disk1", "fat32")]);+}++#[test_case]+fn drive_letters_translate_to_paths() {+ let drives = shparse::assign_drives(MOUNTS);+ let t = |s| shparse::translate(s, &drives);+ assert_eq!(t("D:").as_deref(), Some("/mnt/disk0"));+ assert_eq!(t("D:/").as_deref(), Some("/mnt/disk0"));+ assert_eq!(t("d:/notes/todo.txt").as_deref(), Some("/mnt/disk0/notes/todo.txt")); // any case+ assert_eq!(t("D:notes").as_deref(), Some("/mnt/disk0/notes"));+ assert_eq!(t("e:/x").as_deref(), Some("/mnt/disk1/x"));+ assert_eq!(t("C:").as_deref(), Some("/"));+ assert_eq!(t("C:/tmp/a.txt").as_deref(), Some("/tmp/a.txt"));+ assert_eq!(t("Z:/x"), None); // no such drive+ assert_eq!(t("readme.txt"), None);+ assert_eq!(t("/mnt/disk0"), None);+ assert_eq!(shparse::drive_letter("q:"), Some('Q'));+}++#[test_case]+fn paths_display_with_drive_letters() {+ let drives = shparse::assign_drives(MOUNTS);+ let d = |s| shparse::display(s, &drives);+ assert_eq!(d("/"), "C:/");+ assert_eq!(d("/tmp"), "C:/tmp");+ assert_eq!(d("/mnt/disk0"), "D:/");+ assert_eq!(d("/mnt/disk0/bin"), "D:/bin");+ assert_eq!(d("/mnt/disk1/a/b"), "E:/a/b");+ assert_eq!(d("/mnt/disk01"), "C:/mnt/disk01"); // not inside /mnt/disk0+ assert_eq!(d("/mnt"), "C:/mnt");+}++// --- the real shell --------------------------------------------------------++/// Run `/mnt/disk0/bin/sh script` with stdout in a pipe; return the output+/// and how the shell ended.+fn run_shell_script(script: &str) -> (String, ExitStatus) {+ let path = "/tmp/session.sh";+ let _ = fs::remove(path, false);+ fs::create(path, InodeKind::File).unwrap().write_at(0, script.as_bytes()).unwrap();++ let (reader, writer) = fs::pipe::new();+ let mut spawn = SpawnArgs::kernel("sh");+ spawn.args.push(String::from(path));+ spawn.stdio[1] = Arc::new(writer);+ let sh = process::spawn_path("/mnt/disk0/bin/sh", spawn).unwrap();+ let mut out = Vec::new();+ let mut buf = [0u8; 256];+ // EOF once the shell *and* every program it started have exited.+ while let Ok(n @ 1..) = reader.read(&mut buf) {+ out.extend_from_slice(&buf[..n]);+ }+ (String::from_utf8(out).unwrap(), process::wait(&sh))+}++#[test_case]+fn scripted_shell_session() {+ let (out, status) = run_shell_script(+ "cd D:\n\+ pwd\n\+ ls notes\n\+ echo piped text | grep piped\n\+ type d:notes/todo.txt | wc -l\n\+ echo saved > C:/tmp/s.txt\n\+ echo more >> C:/tmp/s.txt\n\+ cat < /tmp/s.txt\n\+ ver\n\+ nosuchprogram\n\+ exit 3\n\+ echo not reached\n",+ );+ // Each command is echoed after its prompt; the prompt shows the drive.+ for expected in [+ "C:/ $ cd D:\n",+ "D:/ $ pwd\n/mnt/disk0\n",+ "D:/ $ ls notes\ntodo.txt\n", // into a pipe: one name per line+ "piped text\n",+ "wc -l\n2\n",+ "saved\nmore\n",+ "Rustbelt 0.7.0",+ "D:/ $ exit 3\n",+ ] {+ assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out);+ }+ assert!(!out.contains("not reached"));+ assert!(!out.contains("command not found")); // that went to stderr+ assert_eq!(status, ExitStatus::Exited(3));+}++#[test_case]+fn shell_runs_background_jobs() {+ let (out, status) = run_shell_script("start sleep 250ms\njobs\nwait\n");+ assert!(out.contains("sleep started in the background"), "{}", out);+ assert!(out.contains("running start sleep 250ms"), "{}", out);+ assert!(out.contains("exit code 0"), "{}", out);+ assert_eq!(status, ExitStatus::Exited(0)); // end of script: last status+}++/// Exited processes give their memory back (page tables, code, stacks):+/// starting 20 more processes doesn't use up more frames.+#[test_case]+fn processes_give_their_memory_back() {+ let run_child = || {+ let mut spawn = SpawnArgs::kernel("child");+ spawn.args.push(String::from("0"));+ let p = process::spawn_path("/mnt/disk0/bin/child", spawn).unwrap();+ assert_eq!(process::wait(&p), ExitStatus::Exited(0));+ };+ run_child(); // warm up (e.g. caches filled on first use)+ let before = memory::frames_in_use();+ for _ in 0..20 {+ run_child();+ }+ assert_eq!(memory::frames_in_use(), before);+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+ rust_kernel::test_panic_handler(info)+}
@@ -123,13 +123,27 @@ fn child_stdout_into_a_pipe() { assert_eq!(process::wait(&p), ExitStatus::Exited(0)); } +/// wait with WNOHANG: EAGAIN while the child runs (the shell uses this to+/// check on background jobs without blocking).+#[test_case]+fn wait_nohang_does_not_block() {+ let (reader, writer) = fs::pipe::new();+ let mut spawn = SpawnArgs::kernel("child");+ spawn.args.push(String::from("cat")); // copies stdin until EOF+ spawn.stdio[0] = Arc::new(reader);+ let p = process::spawn_path("/mnt/disk0/bin/child", spawn).unwrap();+ assert_eq!(process::wait_child(0, p.pid, process::WNOHANG), Err(Errno::EAGAIN));+ drop(writer); // EOF for the child: it exits+ assert_eq!(process::wait(&p), ExitStatus::Exited(0));+}+ #[test_case] fn spawn_errors() { let spawn = || SpawnArgs::kernel("x"); assert_eq!(process::spawn_path("/mnt/disk0/bin/nope", spawn()).err(), Some(Errno::ENOENT)); assert_eq!(process::spawn_path("/mnt/disk0/readme.txt", spawn()).err(), Some(Errno::ENOEXEC)); assert_eq!(process::spawn_path("/mnt/disk0/bin", spawn()).err(), Some(Errno::EISDIR));- assert_eq!(process::wait_child(12345, 1).err(), Some(Errno::ECHILD));+ assert_eq!(process::wait_child(12345, 1, 0).err(), Some(Errno::ECHILD)); } #[test_case]
@@ -0,0 +1,41 @@+//! cat [file...] -- print files (or stdin, also for "-"). Shell alias: type.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++/// Copy everything from `fd` to stdout.+fn copy(fd: i64) -> i64 {+ let mut buf = [0u8; 512];+ loop {+ let n = read(fd, &mut buf);+ if n <= 0 {+ return n; // 0 = end of file, < 0 = error+ }+ write_all(1, &buf[..n as usize]);+ }+}++fn main() -> i64 {+ if argc() < 2 {+ return if copy(0) < 0 { 1 } else { 0 };+ }+ let mut status = 0;+ for i in 1..argc() {+ let name = arg(i).unwrap_or("");+ let fd = if name == "-" { 0 } else { open(name, 0) };+ let r = if fd < 0 { fd } else { copy(fd) };+ if r < 0 {+ eprintln!("cat: {}: {}", name, strerror(r));+ status = 1;+ }+ if fd > 0 {+ close(fd);+ }+ }+ status+}
@@ -0,0 +1,52 @@+//! cp source dest -- copy a file. If `dest` is a directory the copy goes+//! inside it with the same name (shell alias: copy).+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::{format, string::String};+use ulib::*;++fn main() -> i64 {+ let (Some(src), Some(dst), 3) = (arg(1), arg(2), argc()) else {+ eprintln!("usage: cp source dest");+ return 2;+ };+ let mut st = Stat::default();+ let mut dst = String::from(dst);+ if stat(&dst, &mut st) == 0 && st.kind == KIND_DIR {+ let name = src.rsplit('/').next().unwrap_or(src);+ dst = format!("{}/{}", dst.trim_end_matches('/'), name);+ }+ let from = open(src, 0);+ if from < 0 {+ eprintln!("cp: {}: {}", src, strerror(from));+ return 1;+ }+ let to = open(&dst, O_CREAT | O_TRUNC);+ if to < 0 {+ eprintln!("cp: {}: {}", dst, strerror(to));+ close(from);+ return 1;+ }+ let mut buf = [0u8; 512];+ let mut status = 0;+ loop {+ let n = read(from, &mut buf);+ if n <= 0 || write_all(to, &buf[..n.max(0) as usize]) < 0 {+ if n < 0 {+ eprintln!("cp: {}: {}", src, strerror(n));+ status = 1;+ }+ break;+ }+ }+ close(from);+ close(to);+ status+}
@@ -58,7 +58,7 @@ fn main() -> i64 { // --- list a directory with getdents64 ----------------------------------- let dir = open("bin", 0);- let mut dents = [0u8; 512];+ let mut dents = [0u8; 1024]; let len = getdents64(dir, &mut dents); check(len > 0, "getdents64 bin"); let (mut saw_demo, mut saw_child) = (false, false);
user/echo.rs
@@ -1,23 +0,0 @@-//! 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,85 @@+//! grep [-i] [-v] [-n] pattern [file...] -- print lines containing+//! `pattern` (plain text, no regular expressions). -i: ignore case,+//! -v: lines that do *not* match, -n: show line numbers.+//! Exit code 0 if something matched, 1 if not, 2 on errors (like grep).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn contains(line: &[u8], pattern: &[u8], ignore_case: bool) -> bool {+ pattern.is_empty()+ || line.windows(pattern.len()).any(|w| match ignore_case {+ true => w.eq_ignore_ascii_case(pattern),+ false => w == pattern,+ })+}++fn main() -> i64 {+ let (mut ignore_case, mut invert, mut numbers) = (false, false, false);+ let mut i = 1;+ while let Some(a) = arg(i).filter(|a| a.starts_with('-') && a.len() > 1) {+ for flag in a[1..].chars() {+ match flag {+ 'i' => ignore_case = true,+ 'v' => invert = true,+ 'n' => numbers = true,+ _ => {+ eprintln!("grep: unknown option -{}", flag);+ return 2;+ }+ }+ }+ i += 1;+ }+ let Some(pattern) = arg(i) else {+ eprintln!("usage: grep [-i] [-v] [-n] pattern [file...]");+ return 2;+ };+ let files = i + 1..argc();+ let many = files.len() > 1;+ let (mut matched, mut status) = (false, 0);+ let mut search = |fd: i64, name: &str| {+ let mut number = 0;+ for_each_line(fd, |line| {+ number += 1;+ if contains(line, pattern.as_bytes(), ignore_case) != invert {+ matched = true;+ if many {+ print!("{}:", name);+ }+ if numbers {+ print!("{}:", number);+ }+ write_all(1, line);+ write(1, b"\n");+ }+ })+ };+ if files.is_empty() {+ search(0, "");+ }+ for f in files {+ let name = arg(f).unwrap_or("");+ let fd = open(name, 0);+ let r = if fd < 0 { fd } else { search(fd, name) };+ if fd >= 0 {+ close(fd);+ }+ if r < 0 {+ eprintln!("grep: {}: {}", name, strerror(r));+ status = 2;+ }+ }+ if status != 0 {+ status+ } else if matched {+ 0+ } else {+ 1+ }+}
@@ -17,7 +17,12 @@ 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+ /* Align the location counter itself, not just .data: a program with no+ .data at all (e.g. sleep) would otherwise get its .bss on the+ read-only .rodata page (the linker drops empty sections, alignment+ included) and crash on its first write to a static. */+ . = ALIGN(4K);+ .data : { *(.data .data.* .ldata .ldata.* .got .got.*) } :data .bss : ALIGN(8) { *(.bss .bss.* .lbss .lbss.* COMMON) } :data /DISCARD/ : { *(.comment) *(.eh_frame*) *(.note*) } }
@@ -0,0 +1,126 @@+//! ls [-l] [path...] -- list directories (shell alias: dir).+//!+//! On the console the names are printed in columns; into a pipe or file+//! one per line (like the real `ls`, so `ls | wc -l` counts files).+//! -l adds sizes, DOS style: `<DIR>` or the size in bytes.+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::{format, string::String, vec::Vec};+use ulib::*;++struct Entry {+ name: String,+ dir: bool,+ size: u64,+}++fn main() -> i64 {+ let mut long = false;+ let mut paths = Vec::new();+ for i in 1..argc() {+ match arg(i) {+ Some("-l") => long = true,+ Some(a) => paths.push(a),+ None => {}+ }+ }+ if paths.is_empty() {+ paths.push(".");+ }+ let mut st = Stat::default();+ let columns = !long && fstat(1, &mut st) == 0 && st.kind == KIND_CONSOLE;++ let mut status = 0;+ for (i, path) in paths.iter().enumerate() {+ let r = stat(path, &mut st);+ if r < 0 {+ eprintln!("ls: {}: {}", path, strerror(r));+ status = 1;+ continue;+ }+ if st.kind != KIND_DIR {+ print_entries(&[Entry { name: String::from(*path), dir: false, size: st.size }], long, columns);+ continue;+ }+ if paths.len() > 1 {+ println!("{}{}:", if i > 0 { "\n" } else { "" }, path);+ }+ match read_dir(path, long) {+ Ok(entries) => print_entries(&entries, long, columns),+ Err(e) => {+ eprintln!("ls: {}: {}", path, strerror(e));+ status = 1;+ }+ }+ }+ status+}++/// All entries of a directory, sorted by name.+fn read_dir(path: &str, long: bool) -> Result<Vec<Entry>, i64> {+ let fd = open(path, 0);+ if fd < 0 {+ return Err(fd);+ }+ let mut entries = Vec::new();+ let mut buf = [0u8; 1024];+ loop {+ // Each call returns as many records as fit; 0 = no more.+ let n = getdents64(fd, &mut buf);+ if n <= 0 {+ break;+ }+ for_each_dirent(&buf, n as usize, |name, d_type| {+ if name != "." && name != ".." {+ entries.push(Entry { name: String::from(name), dir: d_type == 4, size: 0 });+ }+ });+ }+ close(fd);+ entries.sort_by(|a, b| a.name.cmp(&b.name));+ if long {+ let mut st = Stat::default();+ for e in entries.iter_mut().filter(|e| !e.dir) {+ if stat(&format!("{}/{}", path, e.name), &mut st) == 0 {+ e.size = st.size;+ }+ }+ }+ Ok(entries)+}++fn print_entries(entries: &[Entry], long: bool, columns: bool) {+ if long {+ for e in entries {+ if e.dir {+ println!(" <DIR> {}/", e.name);+ } else {+ println!("{:>10} {}", e.size, e.name);+ }+ }+ return;+ }+ let shown = |e: &Entry| if e.dir { format!("{}/", e.name) } else { e.name.clone() };+ if !columns {+ entries.iter().for_each(|e| println!("{}", shown(e)));+ return;+ }+ // Columns across an 80-character screen.+ let width = entries.iter().map(|e| shown(e).len() + 2).max().unwrap_or(1);+ let per_line = (79 / width).max(1);+ for (i, e) in entries.iter().enumerate() {+ let last = i + 1 == entries.len() || (i + 1) % per_line == 0;+ if last {+ println!("{}", shown(e));+ } else {+ print!("{:<w$}", shown(e), w = width);+ }+ }+}
@@ -0,0 +1,26 @@+//! mkdir dir... -- create directories (shell alias: md).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ if argc() < 2 {+ eprintln!("usage: mkdir dir...");+ return 2;+ }+ let mut status = 0;+ for i in 1..argc() {+ let dir = arg(i).unwrap_or("");+ let r = mkdir(dir);+ if r < 0 {+ eprintln!("mkdir: {}: {}", dir, strerror(r));+ status = 1;+ }+ }+ status+}
@@ -0,0 +1,30 @@+//! rm path... -- delete files, and empty directories (shell alias: del).+//! (Real rm needs -r for directories; this one just tries rmdir.)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ if argc() < 2 {+ eprintln!("usage: rm path...");+ return 2;+ }+ let mut status = 0;+ for i in 1..argc() {+ let path = arg(i).unwrap_or("");+ let mut r = unlink(path);+ if r == -EISDIR {+ r = rmdir(path); // fails with ENOTEMPTY if there's something in it+ }+ if r < 0 {+ eprintln!("rm: {}: {}", path, strerror(r));+ status = 1;+ }+ }+ status+}
@@ -0,0 +1,554 @@+//! sh -- the Rustbelt shell, a normal user program (/bin/sh on the disk).+//!+//! sh interactive: prompt, line editing, history+//! sh script.sh run the commands in a file (echoing each one)+//!+//! It reads a line, parses it (shparse.rs: words, `|`, `<`, `>`, `>>`, `&`,+//! drive letters), then either runs a *builtin* (cd, pwd, echo, ... -- these+//! must change the shell itself, or are just tiny) or *spawns* a program+//! from a `bin` directory, connecting pipes and files to its stdin/stdout.+//! Everything goes through the same syscalls any program could use.++#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;+#[path = "shparse.rs"]+mod shparse;++use alloc::{+ format,+ string::{String, ToString},+ vec::Vec,+};+use core::fmt::Write;+use shparse::Drive;+use ulib::*;++/// Built-in aliases (DOS names for the Unix-y programs).+const DEFAULT_ALIASES: &[(&str, &str)] = &[("dir", "ls"), ("type", "cat"), ("del", "rm"), ("md", "mkdir"), ("copy", "cp")];++const HELP: &str = "\+Rustbelt shell commands+ builtins cd [dir] pwd echo [text] ver drives alias [name=value]+ history jobs wait [pid] cls / clear help exit [code]+ programs ls [-l] cat wc grep [-i] [-v] mkdir rm touch cp sleep+ (bin dirs) hello demo evil spin child sh+ run [--trace] prog args run a program (--trace prints its syscalls)+ start prog args run it in the background (or: prog args &)+ a | b | c pipes; > file >> file < file redirection+ C: is /, D: E: ... are the disks (see `drives`): cd D:, cat D:/readme.txt+";++/// A background job started with `start` or `&`.+struct Job {+ pid: i64,+ command: String,+}++/// One command of a pipeline, after alias, drive letter and `run`/`start`+/// processing.+struct Stage {+ argv: Vec<String>,+ stdin: Option<String>,+ stdout: Option<(String, bool)>, // (file, append)+ trace: bool,+ program_only: bool, // `run x` always runs a program, never a builtin+}++struct Shell {+ drives: Vec<Drive>,+ aliases: Vec<(String, String)>,+ history: Vec<String>,+ jobs: Vec<Job>,+ status: i64, // exit code of the last command+ exit: Option<i64>, // set by the `exit` builtin+}++fn main() -> i64 {+ let mut sh = Shell {+ drives: Vec::new(),+ aliases: DEFAULT_ALIASES.iter().map(|(a, b)| (a.to_string(), b.to_string())).collect(),+ history: Vec::new(),+ jobs: Vec::new(),+ status: 0,+ exit: None,+ };++ // Where do commands come from? A script file, or stdin. If stdin is the+ // console we're interactive; otherwise (a pipe, `sh < file`) we read+ // lines just like from a script.+ let fd = match arg(1) {+ Some(path) => {+ let fd = open(path, 0);+ if fd < 0 {+ eprintln!("sh: {}: {}", path, strerror(fd));+ return 127;+ }+ fd+ }+ None => 0,+ };+ let mut st = Stat::default();+ let interactive = fd == 0 && fstat(0, &mut st) == 0 && st.kind == KIND_CONSOLE;+ let mut input = LineReader { fd, buf: Vec::new(), eof: false };++ sh.refresh_drives();+ if interactive {+ let disks: Vec<String> = sh.drives.iter().map(|d| format!("{}: = {}", d.letter, d.path)).collect();+ println!("Rustbelt shell. Type `help` for commands. Drives: {}", disks.join(", "));+ }++ while sh.exit.is_none() {+ sh.refresh_drives();+ sh.reap_jobs();+ let prompt = sh.prompt();+ let line = if interactive {+ sh.edit_line(&prompt)+ } else {+ match input.read_line() {+ Some(line) => {+ println!("{}{}", prompt, line); // show what's being run+ line+ }+ None => break, // end of the script+ }+ };+ sh.run_line(&line, interactive);+ }+ sh.exit.unwrap_or(sh.status)+}++impl Shell {+ /// Ask the kernel for the mount table and (re)assign drive letters.+ fn refresh_drives(&mut self) {+ let mut buf = [0u8; 1024];+ let n = mounts(&mut buf).max(0) as usize;+ self.drives = shparse::assign_drives(core::str::from_utf8(&buf[..n]).unwrap_or(""));+ }++ fn cwd(&self) -> String {+ let mut buf = [0u8; 256];+ let n = getcwd(&mut buf).max(0) as usize;+ String::from_utf8_lossy(&buf[..n]).into_owned()+ }++ /// `C:/ $ `, `D:/notes $ `+ fn prompt(&self) -> String {+ format!("{} $ ", shparse::display(&self.cwd(), &self.drives))+ }++ /// Print "[done]" for background jobs that have finished (without+ /// blocking: WNOHANG returns -EAGAIN while a job is still running).+ fn reap_jobs(&mut self) {+ self.jobs.retain(|job| {+ let code = try_wait(job.pid);+ if code == -EAGAIN {+ return true;+ }+ println!("[done] pid {} ({}), exit code {}", job.pid, job.command, code);+ false+ });+ }++ /// Read a line from the keyboard with editing. The console is put in+ /// raw mode so we get every key (and arrow keys as ESC [ A..D) and do+ /// the echo ourselves; back to normal ("cooked") before running+ /// anything, so programs see the usual line-at-a-time input.+ fn edit_line(&mut self, prompt: &str) -> String {+ print!("{}", prompt);+ set_raw(0, true);+ let mut line: Vec<u8> = Vec::new();+ let mut hist = self.history.len(); // position while browsing history+ let mut esc = 0; // 0: normal, 1: after ESC, 2: after ESC [+ let mut buf = [0u8; 16];+ 'read: loop {+ let n = read(0, &mut buf);+ if n <= 0 {+ break;+ }+ for &b in &buf[..n as usize] {+ match (esc, b) {+ (0, 0x1b) => esc = 1,+ (1, b'[') => esc = 2,+ (1, _) => esc = 0,+ (2, b'0'..=b'9' | b';') => {} // parameters, e.g. ESC [ 3 ~+ (2, key) => {+ esc = 0;+ // Up / Down: replace the line with an older/newer one.+ let new = match key {+ b'A' if hist > 0 => hist - 1,+ b'B' if hist < self.history.len() => hist + 1,+ _ => continue, // Left/Right/Home/... not supported+ };+ hist = new;+ let text = self.history.get(hist).map(|s| s.as_bytes()).unwrap_or(b"");+ erase(line.len());+ line = text.to_vec();+ write(1, &line);+ }+ (_, b'\n' | b'\r') => {+ println!();+ break 'read;+ }+ (_, 0x08 | 0x7f) => {+ if line.pop().is_some() {+ erase(1);+ }+ }+ (_, 0x20..=0x7e) => {+ line.push(b);+ write(1, &[b]); // echo+ }+ _ => {} // Tab, other control keys: ignored+ }+ }+ }+ set_raw(0, false);+ String::from_utf8_lossy(&line).into_owned()+ }++ fn run_line(&mut self, line: &str, interactive: bool) {+ let line = line.trim();+ if line.is_empty() {+ return;+ }+ if interactive && self.history.last().map(|h| h.as_str()) != Some(line) {+ self.history.push(line.to_string());+ }+ match shparse::parse(line) {+ Ok(Some(pipeline)) => self.status = self.run_pipeline(pipeline, line),+ Ok(None) => {} // just a comment+ Err(e) => {+ eprintln!("sh: {}", e);+ self.status = 2;+ }+ }+ }++ /// Words -> argv strings: drive letters translated (unless quoted),+ /// aliases expanded, `run [--trace]` / `start` prefixes removed.+ fn prepare(&self, cmd: &shparse::Command, background: &mut bool) -> Result<Stage, String> {+ let path = |w: &shparse::Word| match w.quoted {+ false => shparse::translate(&w.text, &self.drives).unwrap_or_else(|| w.text.clone()),+ true => w.text.clone(),+ };+ let mut argv: Vec<String> = cmd.words.iter().map(path).collect();+ let mut stage = Stage {+ argv: Vec::new(),+ stdin: cmd.stdin.as_ref().map(path),+ stdout: cmd.stdout.as_ref().map(|(w, append)| (path(w), *append)),+ trace: false,+ program_only: false,+ };+ // `run` / `start` prefixes (`start run --trace x` works too).+ loop {+ match argv.first().map(|s| s.as_str()) {+ Some("run") => {+ argv.remove(0);+ stage.program_only = true;+ if argv.first().map(|s| s.as_str()) == Some("--trace") {+ argv.remove(0);+ stage.trace = true;+ }+ }+ Some("start") => {+ argv.remove(0);+ stage.program_only = true;+ *background = true;+ }+ _ => break,+ }+ }+ if argv.is_empty() {+ return Err(String::from("run/start: which program?"));+ }+ // Aliases: replace the command name (a few rounds, for aliases of+ // aliases; the limit stops `alias a=a` from looping forever).+ for _ in 0..5 {+ let Some((_, value)) = self.aliases.iter().find(|(name, _)| *name == argv[0]) else { break };+ let mut words: Vec<String> = value.split_whitespace().map(|s| s.to_string()).collect();+ if words.is_empty() || words[0] == argv[0] {+ break;+ }+ words.extend(argv.drain(1..));+ argv = words;+ }+ stage.argv = argv;+ Ok(stage)+ }++ /// Run `a < in | b | c > out`: create the pipes, spawn every program+ /// with the right stdin/stdout, then wait for them (unless background).+ /// Returns the exit code of the last command.+ fn run_pipeline(&mut self, pipeline: shparse::Pipeline, line: &str) -> i64 {+ let mut background = pipeline.background;+ let mut stages = Vec::new();+ for cmd in &pipeline.commands {+ match self.prepare(cmd, &mut background) {+ Ok(stage) => stages.push(stage),+ Err(e) => {+ eprintln!("sh: {}", e);+ return 2;+ }+ }+ }++ let mut opened: Vec<i64> = Vec::new(); // fds to close when done+ let mut children: Vec<(i64, String)> = Vec::new();+ let mut status = 0;+ let mut prev_pipe = -1; // read end of the pipe from the previous stage+ let mut last_pid = None; // the last stage, if it's a program+ for (i, stage) in stages.iter().enumerate() {+ // stdin: `< file`, else the previous pipe, else ours (-1).+ let mut stdin = prev_pipe;+ if let Some(file) = &stage.stdin {+ stdin = open(file, 0);+ if stdin < 0 {+ eprintln!("sh: {}: {}", file, strerror(stdin));+ status = 1;+ break;+ }+ opened.push(stdin);+ }+ // stdout: `> file` / `>> file`, else a new pipe to the next+ // stage, else ours (-1).+ let mut stdout = -1;+ prev_pipe = -1;+ if i + 1 < stages.len() {+ let (r, w) = pipe().unwrap_or((-1, -1));+ opened.extend([r, w]);+ stdout = w;+ prev_pipe = r;+ }+ if let Some((file, append)) = &stage.stdout {+ let flags = O_CREAT | if *append { O_APPEND } else { O_TRUNC };+ stdout = open(file, flags);+ if stdout < 0 {+ eprintln!("sh: {}: {}", file, strerror(stdout));+ status = 1;+ break;+ }+ opened.push(stdout);+ }++ if !stage.program_only {+ if let Some(code) = self.builtin(&stage.argv, if stdout < 0 { 1 } else { stdout }) {+ status = code;+ continue;+ }+ }+ match self.spawn(stage, stdin, stdout) {+ Ok(pid) => {+ children.push((pid, stage.argv[0].clone()));+ if i + 1 == stages.len() {+ last_pid = Some(pid);+ }+ }+ Err(code) => {+ status = code;+ break;+ }+ }+ }+ // Close our copies of the pipe ends and files: a reader only sees+ // end-of-file once *every* write end is closed, including ours.+ for fd in opened {+ close(fd);+ }++ if background {+ for (pid, name) in children {+ println!("[pid {}] {} started in the background", pid, name);+ self.jobs.push(Job { pid, command: line.to_string() });+ }+ return 0;+ }+ for (pid, _) in &children {+ let code = wait(*pid);+ if Some(*pid) == last_pid {+ status = code; // the last stage's exit code is the pipeline's+ }+ }+ if status != 0 && last_pid.is_some() {+ println!("[exit code {}]", status);+ }+ status+ }++ /// Find a program: names with a `/` are paths; others are looked up in+ /// the `bin` directory of every drive (our PATH): C:/bin, D:/bin, ...+ fn find_program(&self, name: &str) -> Option<String> {+ if name.contains('/') {+ return Some(name.to_string());+ }+ let mut st = Stat::default();+ self.drives.iter().map(|d| format!("{}/bin/{}", d.path.trim_end_matches('/'), name)).find(|p| stat(p, &mut st) == 0 && st.kind == KIND_FILE)+ }++ fn spawn(&self, stage: &Stage, stdin: i64, stdout: i64) -> Result<i64, i64> {+ let name = &stage.argv[0];+ let Some(path) = self.find_program(name) else {+ eprintln!("sh: {}: command not found (try `help`)", name);+ return Err(127);+ };+ // spawn takes the arguments as one NUL-separated string.+ let argv = stage.argv.join("\0");+ let opts = SpawnOptions { stdin, stdout, stderr: -1, flags: if stage.trace { SPAWN_TRACE } else { 0 } };+ let pid = spawn(&path, &argv, &opts);+ if pid < 0 {+ eprintln!("sh: {}: {}", name, strerror(pid));+ return Err(126);+ }+ Ok(pid)+ }++ /// Run a builtin, writing its output to `out`. `None` if `argv[0]`+ /// isn't one.+ fn builtin(&mut self, argv: &[String], out: i64) -> Option<i64> {+ let mut out = Fd(out);+ let arg1 = argv.get(1).map(|s| s.as_str());+ let code = match argv[0].as_str() {+ "cd" => {+ let dir = arg1.unwrap_or("/");+ let r = chdir(dir);+ if r < 0 {+ match shparse::drive_letter(dir) {+ // Still has its letter: translate() didn't know it.+ Some(letter) => eprintln!("cd: there is no drive {}: (see `drives`)", letter),+ None => eprintln!("cd: {}: {}", dir, strerror(r)),+ }+ 1+ } else {+ 0+ }+ }+ "pwd" => {+ let _ = writeln!(out, "{}", self.cwd());+ 0+ }+ "echo" => {+ let _ = writeln!(out, "{}", argv[1..].join(" "));+ 0+ }+ "help" => {+ let _ = write!(out, "{}", HELP);+ 0+ }+ "exit" => {+ self.exit = Some(arg1.and_then(|s| s.parse().ok()).unwrap_or(self.status));+ 0+ }+ "cls" | "clear" => {+ let _ = write!(out, "\x0c"); // form feed: the console clears the screen+ 0+ }+ "ver" => {+ let mut buf = [0u8; 6 * 65];+ let [sysname, _node, release, version, machine, _] = uname(&mut buf);+ let _ = writeln!(out, "{} {} ({}) {}", sysname, release, version, machine);+ 0+ }+ "drives" => {+ for d in &self.drives {+ let _ = writeln!(out, "{}: {:<12} {}", d.letter, d.path, d.fs);+ }+ 0+ }+ "alias" => match arg1 {+ None => {+ for (name, value) in &self.aliases {+ let _ = writeln!(out, "{} = {}", name, value);+ }+ 0+ }+ Some(def) => match def.split_once('=') {+ Some((name, value)) => {+ self.aliases.retain(|(n, _)| n != name);+ self.aliases.push((name.to_string(), value.to_string()));+ 0+ }+ None => {+ eprintln!("alias: use alias name=value");+ 2+ }+ },+ },+ "history" => {+ for (i, line) in self.history.iter().enumerate() {+ let _ = writeln!(out, "{:4} {}", i + 1, line);+ }+ 0+ }+ "jobs" => {+ self.reap_jobs();+ for job in &self.jobs {+ let _ = writeln!(out, "[pid {}] running {}", job.pid, job.command);+ }+ 0+ }+ "wait" => {+ // `wait` waits for all background jobs, `wait 7` for pid 7.+ let only: Option<i64> = arg1.and_then(|s| s.parse().ok());+ let mut code = 0;+ self.jobs.retain(|job| {+ if only.is_some_and(|pid| pid != job.pid) {+ return true;+ }+ code = wait(job.pid);+ let _ = writeln!(out, "[done] pid {} ({}), exit code {}", job.pid, job.command, code);+ false+ });+ code+ }+ _ => return None,+ };+ Some(code)+ }+}++/// Erase `n` characters before the cursor (the console treats \x08 as+/// "back one column and blank it").+fn erase(n: usize) {+ for _ in 0..n {+ write(1, b"\x08");+ }+}++/// Reads a file (or pipe) line by line.+struct LineReader {+ fd: i64,+ buf: Vec<u8>,+ eof: bool,+}++impl LineReader {+ fn read_line(&mut self) -> Option<String> {+ loop {+ if let Some(i) = self.buf.iter().position(|&b| b == b'\n') {+ let line: Vec<u8> = self.buf.drain(..=i).collect();+ return Some(String::from_utf8_lossy(&line[..i]).trim_end_matches('\r').to_string());+ }+ if self.eof {+ // The last line may lack a final newline.+ if self.buf.is_empty() {+ return None;+ }+ let line = String::from_utf8_lossy(&self.buf).into_owned();+ self.buf.clear();+ return Some(line);+ }+ let mut chunk = [0u8; 512];+ let n = read(self.fd, &mut chunk);+ if n <= 0 {+ self.eof = true;+ } else {+ self.buf.extend_from_slice(&chunk[..n as usize]);+ }+ }+ }+}
@@ -0,0 +1,236 @@+//! The shell's parser: command line -> tokens -> pipeline, and the drive+//! letter mapping (`D:/notes` <-> `/mnt/disk0/notes`).+//!+//! Pure functions with no syscalls, so the same file is compiled into the+//! shell (user/sh.rs) *and* into the kernel's test suite (tests/shell.rs),+//! where it's tested without starting any processes.+//!+//! Grammar (a small subset of what a Unix shell accepts):+//!+//! line = pipeline [ "&" ] [ "#" comment ]+//! pipeline = command { "|" command }+//! command = { word | "<" word | ">" word | ">>" word }+//! word = { plain-char | 'single quoted' | "double quoted" }+//!+//! `|`, `<`, `>`, `>>` and `&` are operators even without spaces around+//! them (`ls|wc` works). Quotes just group: no `$variables` or escapes.++#![allow(dead_code)]++use alloc::{format, string::String, vec::Vec};++/// One word of a command. `quoted` words are never drive-translated, so+/// `grep "C:" file` searches for the text C:.+#[derive(Debug, Clone, PartialEq)]+pub struct Word {+ pub text: String,+ pub quoted: bool,+}++#[derive(Debug, Clone, PartialEq)]+pub enum Token {+ Word(Word),+ Pipe, // |+ In, // <+ Out, // >+ Append, // >>+ Background, // &+}++/// Split a line into tokens. Errors on an unterminated quote.+pub fn tokenize(line: &str) -> Result<Vec<Token>, String> {+ let mut tokens = Vec::new();+ let mut chars = line.chars().peekable();+ // The word being built, if any (a word can mix plain and quoted parts:+ // a"b c"d is the single word `ab cd`).+ let mut word: Option<Word> = None;++ while let Some(c) = chars.next() {+ let op = match c {+ '|' => Some(Token::Pipe),+ '<' => Some(Token::In),+ '&' => Some(Token::Background),+ '>' if chars.peek() == Some(&'>') => {+ chars.next();+ Some(Token::Append)+ }+ '>' => Some(Token::Out),+ _ => None,+ };+ if op.is_some() || c.is_whitespace() || (c == '#' && word.is_none()) {+ // Anything here ends the current word.+ if let Some(w) = word.take() {+ tokens.push(Token::Word(w));+ }+ if let Some(op) = op {+ tokens.push(op);+ } else if c == '#' {+ break; // comment: ignore the rest of the line+ }+ continue;+ }+ let w = word.get_or_insert(Word { text: String::new(), quoted: false });+ if c == '\'' || c == '"' {+ w.quoted = true;+ loop {+ match chars.next() {+ Some(q) if q == c => break,+ Some(other) => w.text.push(other),+ None => return Err(format!("unterminated {} quote", c)),+ }+ }+ } else {+ w.text.push(c);+ }+ }+ if let Some(w) = word {+ tokens.push(Token::Word(w));+ }+ Ok(tokens)+}++/// One program (or builtin) with its arguments and redirections.+#[derive(Debug, Clone, Default, PartialEq)]+pub struct Command {+ pub words: Vec<Word>, // words[0] is the command name+ pub stdin: Option<Word>, // < file+ pub stdout: Option<(Word, bool)>, // > file (false) or >> file (true)+}++/// `a | b | c`, maybe with a trailing `&` (run in the background).+#[derive(Debug, Clone, PartialEq)]+pub struct Pipeline {+ pub commands: Vec<Command>,+ pub background: bool,+}++/// Parse a line. `Ok(None)` for an empty line or a comment.+pub fn parse(line: &str) -> Result<Option<Pipeline>, String> {+ let tokens = tokenize(line)?;+ if tokens.is_empty() {+ return Ok(None);+ }+ let mut commands = Vec::new();+ let mut current = Command::default();+ let mut background = false;+ let mut iter = tokens.into_iter().peekable();++ while let Some(token) = iter.next() {+ if background {+ return Err(String::from("`&` must be at the end of the line"));+ }+ match token {+ Token::Word(w) => current.words.push(w),+ Token::Pipe => {+ if current.words.is_empty() {+ return Err(String::from("missing command before `|`"));+ }+ commands.push(core::mem::take(&mut current));+ }+ Token::Background => background = true,+ Token::In | Token::Out | Token::Append => {+ let target = match iter.next() {+ Some(Token::Word(w)) => w,+ _ => return Err(String::from("missing file name after redirection")),+ };+ match token {+ Token::In => current.stdin = Some(target),+ Token::Out => current.stdout = Some((target, false)),+ _ => current.stdout = Some((target, true)),+ }+ }+ }+ }+ if current.words.is_empty() {+ // e.g. `> file`, `&` or `ls |`: nothing to run.+ return Err(String::from(if commands.is_empty() { "missing command" } else { "missing command after `|`" }));+ }+ commands.push(current);+ Ok(Some(Pipeline { commands, background }))+}++// --- drive letters -----------------------------------------------------------++/// A drive letter and the directory it stands for.+#[derive(Debug, Clone, PartialEq)]+pub struct Drive {+ pub letter: char, // always upper case+ pub path: String, // absolute, no trailing slash except "/"+ pub fs: String, // file system type, e.g. "fat32"+}++/// Assign letters from the kernel's mount table (the text from the+/// `mounts` syscall: one "path fstype" per line, in mount order).+/// C: is always the root; D:, E:, ... go to the mounts under /mnt (the+/// disks), in the order they were mounted. Other mounts such as /tmp are+/// reachable through C: (C:/tmp).+pub fn assign_drives(mount_table: &str) -> Vec<Drive> {+ let mut drives = Vec::new();+ let root_fs = mount_table+ .lines()+ .find_map(|l| l.split_once(' ').filter(|(p, _)| *p == "/").map(|(_, fs)| fs))+ .unwrap_or("ramfs");+ drives.push(Drive { letter: 'C', path: String::from("/"), fs: String::from(root_fs.trim()) });+ let mut letter = b'D';+ for line in mount_table.lines() {+ if let Some((path, fs)) = line.split_once(' ') {+ if path.starts_with("/mnt/") && letter <= b'Z' {+ drives.push(Drive { letter: letter as char, path: String::from(path), fs: String::from(fs.trim()) });+ letter += 1;+ }+ }+ }+ drives+}++/// Does `word` start with a drive letter ("D:", "d:/x", "E:notes")?+pub fn drive_letter(word: &str) -> Option<char> {+ let b = word.as_bytes();+ if b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':' {+ Some((b[0] as char).to_ascii_uppercase())+ } else {+ None+ }+}++/// `D:/notes/todo.txt` -> `/mnt/disk0/notes/todo.txt`. Letters are case+/// insensitive; `D:` and `D:/` are the drive's root, and `D:notes` means+/// `D:/notes` (drives have no current directory of their own). `None` if+/// `word` doesn't start with a known drive letter.+pub fn translate(word: &str, drives: &[Drive]) -> Option<String> {+ let letter = drive_letter(word)?;+ let drive = drives.iter().find(|d| d.letter == letter)?;+ let rest = word[2..].trim_start_matches('/');+ Some(if rest.is_empty() {+ drive.path.clone()+ } else if drive.path == "/" {+ format!("/{}", rest)+ } else {+ format!("{}/{}", drive.path, rest)+ })+}++/// The other direction, for the prompt: `/mnt/disk0/bin` -> `D:/bin`,+/// `/tmp` -> `C:/tmp`. Picks the drive with the longest matching path.+pub fn display(path: &str, drives: &[Drive]) -> String {+ let mut best: Option<(&Drive, &str)> = None;+ for d in drives {+ let rest = if d.path == "/" {+ path.strip_prefix('/')+ } else {+ // "/mnt/disk0" matches "/mnt/disk0" and "/mnt/disk0/x", not "/mnt/disk01".+ path.strip_prefix(d.path.as_str())+ .filter(|r| r.is_empty() || r.starts_with('/'))+ .map(|r| r.trim_start_matches('/'))+ };+ if let Some(rest) = rest {+ if best.map_or(true, |(b, _)| d.path.len() > b.path.len()) {+ best = Some((d, rest));+ }+ }+ }+ match best {+ Some((d, rest)) => format!("{}:/{}", d.letter, rest),+ None => String::from(path),+ }+}
@@ -0,0 +1,28 @@+//! sleep N -- wait N seconds (or `sleep 250ms`). The kernel blocks the+//! thread until the timer says it's time; no CPU is used meanwhile.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ let a = arg(1).unwrap_or("");+ let ms = match a.strip_suffix("ms") {+ Some(n) => n.parse::<u64>().ok(),+ None => a.parse::<u64>().ok().map(|s| s * 1000),+ };+ match ms {+ Some(ms) => {+ sleep_ms(ms);+ 0+ }+ None => {+ eprintln!("usage: sleep seconds | sleep 250ms");+ 2+ }+ }+}
@@ -0,0 +1,25 @@+//! touch file... -- create empty files (existing files are left alone;+//! there are no timestamps to update yet).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ let mut status = 0;+ for i in 1..argc() {+ let path = arg(i).unwrap_or("");+ let fd = open(path, O_CREAT); // no O_TRUNC: keep the contents+ if fd < 0 {+ eprintln!("touch: {}: {}", path, strerror(fd));+ status = 1;+ } else {+ close(fd);+ }+ }+ status+}
@@ -1,5 +1,6 @@ //! A tiny "libc" for user programs: syscall wrappers, print!/println!,-//! the `_start` entry point and a panic handler.+//! eprintln!, a small heap (so `Vec` and `String` work), the `_start` entry+//! point and a panic handler. //! //! Each user program includes it with //! #[macro_use] #[path = "ulib.rs"] mod ulib;@@ -9,9 +10,11 @@ //! SSE/floating point disabled. That matters: the kernel doesn't save SSE //! registers on a context switch. -#![allow(dead_code)]+#![allow(dead_code, unused_macros)] +use core::alloc::{GlobalAlloc, Layout}; use core::arch::asm;+use core::cell::UnsafeCell; use core::fmt; pub const SYS_READ: u64 = 0;@@ -21,12 +24,14 @@ pub const SYS_CLOSE: u64 = 3; pub const SYS_STAT: u64 = 4; pub const SYS_FSTAT: u64 = 5; pub const SYS_LSEEK: u64 = 8;+pub const SYS_IOCTL: u64 = 16; pub const SYS_PIPE: u64 = 22; pub const SYS_SCHED_YIELD: u64 = 24; pub const SYS_SLEEP_MS: u64 = 35; pub const SYS_GETPID: u64 = 39; pub const SYS_EXIT: u64 = 60; pub const SYS_WAIT: u64 = 61;+pub const SYS_UNAME: u64 = 63; pub const SYS_GETCWD: u64 = 79; pub const SYS_CHDIR: u64 = 80; pub const SYS_MKDIR: u64 = 83;@@ -34,15 +39,20 @@ pub const SYS_RMDIR: u64 = 84; pub const SYS_UNLINK: u64 = 87; pub const SYS_GETDENTS64: u64 = 217; pub const SYS_SPAWN: u64 = 500;+pub const SYS_MOUNTS: u64 = 501; pub const O_CREAT: u64 = 0o100; pub const O_TRUNC: u64 = 0o1000;+pub const O_APPEND: u64 = 0o2000;+pub const TTY_SET_RAW: u64 = 1;+pub const WNOHANG: u64 = 1; pub const SEEK_SET: u64 = 0; pub const SEEK_CUR: u64 = 1; pub const SEEK_END: u64 = 2; pub const SPAWN_TRACE: u64 = 1; pub const ENOENT: i64 = 2;+pub const EAGAIN: i64 = 11; pub const EBADF: i64 = 9; pub const ECHILD: i64 = 10; pub const EFAULT: i64 = 14;@@ -163,6 +173,29 @@ pub fn exit(code: i64) -> ! { pub fn wait(pid: i64) -> i64 { syscall3(SYS_WAIT, pid as u64, 0, 0) }+/// Like `wait`, but don't block: -EAGAIN while the child is still running.+pub fn try_wait(pid: i64) -> i64 {+ syscall3(SYS_WAIT, pid as u64, WNOHANG, 0)+}+/// Console raw mode on/off (keys arrive one by one, no echo). Returns the+/// old mode, or -ENOTTY if `fd` isn't the console.+pub fn set_raw(fd: i64, raw: bool) -> i64 {+ syscall3(SYS_IOCTL, fd as u64, TTY_SET_RAW, raw as u64)+}+/// Linux's `struct utsname`: sysname, nodename, release, version, machine,+/// domainname. Returns them as strings.+pub fn uname(buf: &mut [u8; 6 * 65]) -> [&str; 6] {+ syscall3(SYS_UNAME, buf.as_mut_ptr() as u64, 0, 0);+ core::array::from_fn(|i| {+ let field = &buf[i * 65..(i + 1) * 65];+ let len = field.iter().position(|&b| b == 0).unwrap_or(65);+ core::str::from_utf8(&field[..len]).unwrap_or("?")+ })+}+/// The mount table as text ("path fstype" lines); returns the length.+pub fn mounts(buf: &mut [u8]) -> i64 {+ syscall3(SYS_MOUNTS, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+} /// Current directory into `buf`; returns its length (or -errno). pub fn getcwd(buf: &mut [u8]) -> i64 { syscall3(SYS_GETCWD, buf.as_mut_ptr() as u64, buf.len() as u64, 0)@@ -229,6 +262,79 @@ pub fn arg(i: usize) -> Option<&'static str> { } } +/// Write all of `buf` (pipes and files may take less than all at once).+pub fn write_all(fd: i64, mut buf: &[u8]) -> i64 {+ while !buf.is_empty() {+ let n = write(fd, buf);+ if n <= 0 {+ return n;+ }+ buf = &buf[n as usize..];+ }+ 0+}++/// Call `f` for each line read from `fd` (without the '\n'). Lines longer+/// than 1 KiB come in pieces. Returns 0, or -errno if a read fails.+pub fn for_each_line(fd: i64, mut f: impl FnMut(&[u8])) -> i64 {+ let mut line = [0u8; 1024];+ let mut len = 0;+ let mut buf = [0u8; 512];+ loop {+ let n = read(fd, &mut buf);+ if n < 0 {+ return n;+ }+ if n == 0 {+ if len > 0 {+ f(&line[..len]); // last line without a newline+ }+ return 0;+ }+ for &b in &buf[..n as usize] {+ if b == b'\n' || len == line.len() {+ f(&line[..len]);+ len = 0;+ if b == b'\n' {+ continue;+ }+ }+ line[len] = b;+ len += 1;+ }+ }+}++/// `ls`, `cat` etc. print errors like "cat: x: No such file or directory".+pub fn strerror(errno: i64) -> &'static str {+ match errno.abs() {+ 2 => "No such file or directory",+ 5 => "I/O error",+ 7 => "Argument list too long",+ 8 => "Exec format error",+ 9 => "Bad file descriptor",+ 10 => "No child processes",+ 11 => "Try again",+ 12 => "Out of memory",+ 14 => "Bad address",+ 16 => "Device or resource busy",+ 17 => "File exists",+ 20 => "Not a directory",+ 21 => "Is a directory",+ 22 => "Invalid argument",+ 24 => "Too many open files",+ 25 => "Not a terminal",+ 28 => "No space left on device",+ 29 => "Illegal seek",+ 32 => "Broken pipe",+ 34 => "Result too large",+ 36 => "File name too long",+ 38 => "Function not implemented",+ 39 => "Directory not empty",+ _ => "Unknown error",+ }+}+ pub struct Stdout; impl fmt::Write for Stdout {@@ -245,6 +351,32 @@ macro_rules! print { }}; } +pub struct Stderr;++impl fmt::Write for Stderr {+ fn write_str(&mut self, s: &str) -> fmt::Result {+ write(2, s.as_bytes());+ Ok(())+ }+}++/// Write formatted text to any fd: `fprint!(fd, "...")`.+pub struct Fd(pub i64);++impl fmt::Write for Fd {+ fn write_str(&mut self, s: &str) -> fmt::Result {+ write_all(self.0, s.as_bytes());+ Ok(())+ }+}++macro_rules! eprintln {+ ($($arg:tt)*) => {{+ use core::fmt::Write;+ let _ = writeln!($crate::ulib::Stderr, $($arg)*);+ }};+}+ macro_rules! println { ($($arg:tt)*) => {{ use core::fmt::Write;@@ -268,3 +400,75 @@ fn panic(info: &core::panic::PanicInfo) -> ! { println!("[user panic] {}", info); exit(101) }++// --- heap ----------------------------------------------------------------+//+// A tiny allocator so programs can use `Vec`, `String`, `format!` (with+// `extern crate alloc;`). Memory comes from a 64 KiB array in .bss, which+// the ELF loader maps as zeroed pages -- no `brk`/`mmap` syscall needed.+// Same idea as the kernel's fixed-size-block allocator: sizes are rounded up+// to a power of two (16 B .. 32 KiB) and freed blocks go on one free list+// per size, to be reused by the next allocation of that size. Programs are+// single-threaded, so no lock.++const HEAP_SIZE: usize = 64 * 1024;+const CLASSES: usize = 12; // 16 << 11 = 32 KiB++#[repr(C, align(4096))]+struct Heap {+ memory: UnsafeCell<[u8; HEAP_SIZE]>,+ used: UnsafeCell<usize>, // bump pointer: bytes handed out so far+ free: UnsafeCell<[usize; CLASSES]>, // free list heads (addresses, 0 = empty)+}++unsafe impl Sync for Heap {} // single-threaded programs++#[global_allocator]+static HEAP: Heap = Heap {+ memory: UnsafeCell::new([0; HEAP_SIZE]),+ used: UnsafeCell::new(0),+ free: UnsafeCell::new([0; CLASSES]),+};++/// Size class for a layout: 0 = 16 bytes, 1 = 32, ...+fn class_of(layout: Layout) -> usize {+ let size = layout.size().max(layout.align()).max(16).next_power_of_two();+ size.trailing_zeros() as usize - 4+}++unsafe impl GlobalAlloc for Heap {+ unsafe fn alloc(&self, layout: Layout) -> *mut u8 {+ let class = class_of(layout);+ if class >= CLASSES {+ return core::ptr::null_mut(); // too big: "out of memory"+ }+ unsafe {+ let free = &mut *self.free.get();+ if free[class] != 0 {+ // Pop a freed block; its first 8 bytes hold the next one.+ let block = free[class] as *mut u8;+ free[class] = *(block as *const usize);+ return block;+ }+ // Carve a new block. Blocks are aligned to their size, which+ // covers any alignment up to that size.+ let size = 16 << class;+ let used = &mut *self.used.get();+ let start = (*used).next_multiple_of(size);+ if start + size > HEAP_SIZE {+ return core::ptr::null_mut();+ }+ *used = start + size;+ (self.memory.get() as *mut u8).add(start)+ }+ }++ unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {+ let class = class_of(layout);+ unsafe {+ let free = &mut *self.free.get();+ *(ptr as *mut usize) = free[class];+ free[class] = ptr as usize;+ }+ }+}
@@ -0,0 +1,83 @@+//! wc [-l] [file...] -- count lines, words and bytes (stdin if no files).+//! -l: only lines.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++/// (lines, words, bytes) of everything readable from `fd`.+fn count(fd: i64) -> Result<[u64; 3], i64> {+ let mut c = [0u64; 3];+ let mut in_word = false;+ let mut buf = [0u8; 512];+ loop {+ let n = read(fd, &mut buf);+ if n < 0 {+ return Err(n);+ }+ if n == 0 {+ return Ok(c);+ }+ for &b in &buf[..n as usize] {+ c[2] += 1;+ if b == b'\n' {+ c[0] += 1;+ }+ let space = b == b' ' || b == b'\n' || b == b'\t' || b == b'\r';+ if !space && !in_word {+ c[1] += 1; // a word starts here+ }+ in_word = !space;+ }+ }+}++fn show(c: [u64; 3], lines_only: bool, name: &str) {+ let sep = if name.is_empty() { "" } else { " " };+ if lines_only {+ println!("{}{}{}", c[0], sep, name);+ } else {+ println!("{:>7} {:>7} {:>7}{}{}", c[0], c[1], c[2], sep, name);+ }+}++fn main() -> i64 {+ let lines_only = arg(1) == Some("-l");+ let first = if lines_only { 2 } else { 1 };+ if argc() <= first {+ return match count(0) {+ Ok(c) => {+ show(c, lines_only, "");+ 0+ }+ Err(_) => 1,+ };+ }+ let (mut total, mut status) = ([0u64; 3], 0);+ for i in first..argc() {+ let name = arg(i).unwrap_or("");+ let fd = open(name, 0);+ let r = if fd < 0 { Err(fd) } else { count(fd) };+ if fd >= 0 {+ close(fd);+ }+ match r {+ Ok(c) => {+ show(c, lines_only, name);+ (0..3).for_each(|k| total[k] += c[k]);+ }+ Err(e) => {+ eprintln!("wc: {}: {}", name, strerror(e));+ status = 1;+ }+ }+ }+ if argc() - first > 1 {+ show(total, lines_only, "total");+ }+ status+}