RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-30 01:22:29 -0700
commitcd21aabc566c397fcafc3de71454a81b47f7656f
parent20756a688a45abfec809ec6ee710c35307350834
treebrowse 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
MCargo.lock+1 -1
MCargo.toml+8 -4
MREADME.md+423 -159
Mboot.png+0 -0
Mbuild.rs+6 -1
Mdisk/notes/todo.txt+2 -2
Mdisk/readme.txt+1 -0
Adisk/scripts/selftest.sh+32 -0
Mscripts/run-headless.sh+38 -4
Mscripts/screenshot.sh+48 -26
Msrc/elf.rs+13 -0
Msrc/errno.rs+15 -0
Msrc/fs/file.rs+10 -1
Msrc/interrupts.rs+4 -13
Msrc/main.rs+108 -45
Msrc/memory.rs+79 -4
Msrc/process.rs+50 -8
Msrc/syscall.rs+106 -5
Msrc/task/keyboard.rs+13 -7
Msrc/tty.rs+106 -24
Msrc/vga_buffer.rs+12 -0
Mtests/fat.rs+1 -1
Atests/shell.rs+218 -0
Mtests/user_mode.rs+15 -1
Auser/cat.rs+41 -0
Auser/cp.rs+52 -0
Muser/demo.rs+1 -1
Duser/echo.rs+0 -23
Auser/grep.rs+85 -0
Muser/link.ld+6 -1
Auser/ls.rs+126 -0
Auser/mkdir.rs+26 -0
Auser/rm.rs+30 -0
Auser/sh.rs+554 -0
Auser/shparse.rs+236 -0
Auser/sleep.rs+28 -0
Auser/touch.rs+25 -0
Muser/ulib.rs+206 -2
Auser/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. -![boot screenshot](boot.png)+![Rustbelt shell session: ls, cat with a drive letter, a pipe, run --trace](boot.png)  ## 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+    }+}
@@ -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+}