foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 00:52:14 -0700 |
|---|---|---|
| commit | 519a3fb17204c73a6a4fa94ef6433ef32866c3bb | |
| parent | 431b990267804972598570345cfc0bd999b87de3 | |
| tree | browse files at this commit | |
Stage 6: PCI, virtio-blk, partitions, FAT32, spawn/wait and shell syscalls - PCI bus scan (printed at boot); BlockDevice trait + device registry - legacy virtio-blk driver (polled virtqueue, bounce buffer); RamDisk for tests - MBR + GPT parsing, partitions exposed as block devices (vda1...) - FAT32 via fatfs (git, no_std+alloc) behind a BlockIo adapter; mounted at /mnt/disk0; build.rs builds target/disk.img (sfdisk + mkfs.vfat + mtools) with text files and the user program ELFs; programs now load from disk - syscalls: stat, fstat, lseek, pipe, sleep_ms, wait, getcwd, chdir, mkdir, rmdir, unlink, getdents64, spawn(path, argv, stdio, trace) - real blocking: blocked threads, wake_at, WaitQueue; console read, pipes and wait block instead of spinning - 64 KiB thread stacks with a canary, finished stacks freed, 4 MiB heap, allocator takes its lock with interrupts off - demo + child user programs; tests for block, partitions, FAT, spawn/wait - README: disks section, syscall table, spawn vs fork/exec, shell design
| A | child.rs | +41 -0 |
| A | demo.rs | +152 -0 |
| M | ulib.rs | +167 -2 |
3 files changed, 360 insertions, 2 deletions
@@ -0,0 +1,41 @@+//! Helper program that `demo` (and the tests) spawn:+//! child -> print my arguments, exit 0+//! child <number> -> exit with that number (tests wait() exit codes)+//! child pipe -> write a line to stdout (demo reads it via a pipe)+//! child cat -> copy stdin to stdout until EOF+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+ match arg(1) {+ Some("pipe") => {+ println!("hello through a pipe from pid {}", getpid());+ 0+ }+ Some("cat") => {+ let mut buf = [0u8; 64];+ loop {+ let n = read(0, &mut buf);+ if n <= 0 {+ return n;+ }+ write(1, &buf[..n as usize]);+ }+ }+ Some(number) if number.parse::<i64>().is_ok() => number.parse().unwrap(),+ _ => {+ print!("[child] pid {} got {} args:", getpid(), argc());+ for i in 0..argc() {+ print!(" {:?}", arg(i).unwrap_or("?"));+ }+ println!();+ 0+ }+ }+}
@@ -0,0 +1,152 @@+//! Stage 6 demo: this program was itself loaded from the FAT32 disk.+//! It exercises the syscalls the shell (next stage) will need: cwd, reading+//! a file on the FAT disk, stat, lseek, getdents64, mkdir/unlink/rmdir,+//! pipes, and spawn + wait. Exit code = number of failed checks.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++static mut FAILURES: i64 = 0;++fn check(ok: bool, what: &str) {+ if !ok {+ println!("[demo] CHECK FAILED: {}", what);+ unsafe { FAILURES += 1 };+ }+}++fn cwd_is(expected: &str) -> bool {+ let mut buf = [0u8; 64];+ let n = getcwd(&mut buf);+ n >= 0 && &buf[..n as usize] == expected.as_bytes()+}++fn main() -> i64 {+ println!("[demo] pid {} started from /mnt/disk0/bin/demo", getpid());++ // --- current directory --------------------------------------------------+ check(cwd_is("/"), "cwd starts at /");+ check(chdir("/mnt/disk0") == 0, "chdir /mnt/disk0");+ check(cwd_is("/mnt/disk0"), "getcwd after chdir");++ // --- read a file from the FAT disk (relative path!) ---------------------+ let fd = open("readme.txt", 0);+ check(fd >= 3, "open readme.txt");+ let mut buf = [0u8; 256];+ let n = read(fd, &mut buf).max(0) as usize;+ let text = core::str::from_utf8(&buf[..n]).unwrap_or("?");+ print!("[demo] readme.txt from FAT32: {}", text.lines().next().unwrap_or(""));+ println!();+ check(text.starts_with("Hello from the FAT32 disk!"), "readme.txt contents");++ // --- stat / fstat / lseek ------------------------------------------------+ let mut st = Stat::default();+ check(stat("readme.txt", &mut st) == 0 && st.kind == KIND_FILE && st.size == n as u64, "stat readme.txt");+ check(fstat(1, &mut st) == 0 && st.kind == KIND_CONSOLE, "fstat stdout = console");+ check(stat("bin", &mut st) == 0 && st.kind == KIND_DIR, "stat bin is a directory");+ check(lseek(fd, 6, SEEK_SET) == 6, "lseek SEEK_SET");+ let mut word = [0u8; 4];+ check(read(fd, &mut word) == 4 && &word == b"from", "read after lseek");+ check(lseek(fd, 0, SEEK_END) == n as i64, "lseek SEEK_END = size");+ check(lseek(1, 0, SEEK_SET) == -ESPIPE, "can't seek the console");+ close(fd);++ // --- list a directory with getdents64 -----------------------------------+ let dir = open("bin", 0);+ let mut dents = [0u8; 512];+ let len = getdents64(dir, &mut dents);+ check(len > 0, "getdents64 bin");+ let (mut saw_demo, mut saw_child) = (false, false);+ print!("[demo] /mnt/disk0/bin:");+ for_each_dirent(&dents, len.max(0) as usize, |name, d_type| {+ print!(" {}{}", name, if d_type == 4 { "/" } else { "" });+ saw_demo |= name == "demo";+ saw_child |= name == "child";+ });+ println!();+ check(saw_demo && saw_child, "bin contains demo and child");+ check(getdents64(dir, &mut dents) == 0, "getdents64 at end returns 0");+ close(dir);++ // --- relative paths, "..", errors ----------------------------------------+ check(chdir("notes") == 0 && cwd_is("/mnt/disk0/notes"), "chdir notes");+ check(chdir("../..") == 0 && cwd_is("/mnt"), "chdir ../..");+ check(chdir("disk0/readme.txt") == -ENOTDIR, "chdir to a file -> ENOTDIR");+ check(chdir("/nope") == -ENOENT, "chdir to nothing -> ENOENT");++ // --- create and delete (ramfs /tmp and the FAT disk) ---------------------+ for base in ["/tmp", "/mnt/disk0/notes"] {+ let mut path_buf = [0u8; 64];+ let dir = join(&mut path_buf, base, "/newdir");+ check(mkdir(dir) == 0, "mkdir");+ check(mkdir(dir) == -EEXIST, "mkdir again -> EEXIST");+ let mut file_buf = [0u8; 64];+ let file = join(&mut file_buf, dir, "/f.txt");+ let fd = open(file, O_CREAT);+ check(write(fd, b"bytes") == 5, "write new file");+ close(fd);+ check(rmdir(dir) == -ENOTEMPTY, "rmdir non-empty -> ENOTEMPTY");+ check(unlink(dir) == -EISDIR, "unlink a directory -> EISDIR");+ check(unlink(file) == 0, "unlink file");+ check(rmdir(dir) == 0, "rmdir empty dir");+ check(stat(dir, &mut st) == -ENOENT, "gone after rmdir");+ }+ check(rmdir("/tmp") == -EBUSY, "rmdir a mount point -> EBUSY");+ println!("[demo] mkdir/unlink/rmdir work on ramfs and FAT32");++ // --- spawn a child with its stdout connected to a pipe -------------------+ chdir("/mnt/disk0");+ let (r, w) = pipe().unwrap_or((-1, -1));+ let opts = SpawnOptions { stdout: w, ..SpawnOptions::INHERIT };+ let pid = spawn("bin/child", "child\0pipe", &opts);+ check(pid > 0, "spawn child (pipe)");+ close(w); // only the child holds the write end now: EOF when it exits+ let mut got = [0u8; 128];+ let mut total = 0;+ loop {+ let n = read(r, &mut got[total..]); // blocks until data or EOF+ if n <= 0 {+ break;+ }+ total += n as usize;+ }+ close(r);+ let line = core::str::from_utf8(&got[..total]).unwrap_or("?").trim_end();+ println!("[demo] read from pipe: {:?}", line);+ check(line.starts_with("hello through a pipe"), "pipe contents");+ check(wait(pid) == 0, "child (pipe) exit code 0");++ // --- spawn + wait: exit codes ---------------------------------------------+ let pid = spawn("bin/child", "child\0with\0three args", &SpawnOptions::INHERIT);+ check(wait(pid) == 0, "child with args exits 0");+ let pid = spawn("bin/child", "child\042", &SpawnOptions::INHERIT);+ let code = wait(pid);+ println!("[demo] spawned child 42 (pid {}), wait -> exit code {}", pid, code);+ check(code == 42, "wait returns the exit code");+ check(wait(pid) == -ECHILD, "second wait -> ECHILD");+ let pid = spawn("/mnt/disk0/bin/evil", "", &SpawnOptions::INHERIT);+ check(wait(pid) == 139, "killed child -> 139");+ check(spawn("bin/nope", "", &SpawnOptions::INHERIT) == -ENOENT, "spawn missing -> ENOENT");+ check(spawn("readme.txt", "", &SpawnOptions::INHERIT) == -8, "spawn non-ELF -> ENOEXEC");+ check(wait(1) == -ECHILD, "wait for a non-child -> ECHILD");++ sleep_ms(100); // blocks in the kernel (no busy loop)++ let failures = unsafe { FAILURES };+ if failures == 0 {+ println!("[demo] all stage 6 checks passed, exiting with 0");+ }+ failures+}++/// Concatenate two strings into `buf` (no heap in user programs).+fn join<'a>(buf: &'a mut [u8], a: &str, b: &str) -> &'a str {+ buf[..a.len()].copy_from_slice(a.as_bytes());+ buf[a.len()..a.len() + b.len()].copy_from_slice(b.as_bytes());+ core::str::from_utf8(&buf[..a.len() + b.len()]).unwrap()+}
@@ -18,14 +18,65 @@ pub const SYS_READ: u64 = 0; pub const SYS_WRITE: u64 = 1; pub const SYS_OPEN: u64 = 2; pub const SYS_CLOSE: u64 = 3;+pub const SYS_STAT: u64 = 4;+pub const SYS_FSTAT: u64 = 5;+pub const SYS_LSEEK: u64 = 8;+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_GETCWD: u64 = 79;+pub const SYS_CHDIR: u64 = 80;+pub const SYS_MKDIR: u64 = 83;+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 O_CREAT: u64 = 0o100;+pub const O_TRUNC: u64 = 0o1000;+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 EBADF: i64 = 9;+pub const ECHILD: i64 = 10; pub const EFAULT: i64 = 14;+pub const EBUSY: i64 = 16;+pub const EEXIST: i64 = 17;+pub const ENOTDIR: i64 = 20;+pub const EISDIR: i64 = 21;+pub const ESPIPE: i64 = 29;+pub const ENOTEMPTY: i64 = 39;++/// Same layout as the kernel's `fs::file::Stat`.+#[repr(C)]+#[derive(Default, Clone, Copy)]+pub struct Stat {+ pub kind: u64, // KIND_*+ pub size: u64,+}+pub const KIND_FILE: u64 = 1;+pub const KIND_DIR: u64 = 2;+pub const KIND_CONSOLE: u64 = 3;+pub const KIND_PIPE: u64 = 4;++/// Same layout as the kernel's `syscall::SpawnOptions`.+#[repr(C)]+pub struct SpawnOptions {+ pub stdin: i64, // my fd to give the child as its fd 0 (-1 = my fd 0)+ pub stdout: i64,+ pub stderr: i64,+ pub flags: u64, // SPAWN_TRACE+}++impl SpawnOptions {+ pub const INHERIT: SpawnOptions = SpawnOptions { stdin: -1, stdout: -1, stderr: -1, flags: 0 };+} /// Raw system call: number in rax, args in rdi/rsi/rdx, result in rax. /// The `syscall` instruction overwrites rcx and r11.@@ -46,6 +97,26 @@ pub fn syscall3(nr: u64, a1: u64, a2: u64, a3: u64) -> i64 { ret } +/// 5-argument version (4th argument goes in r10, 5th in r8).+pub fn syscall5(nr: u64, a1: u64, a2: u64, a3: u64, a4: u64, a5: u64) -> i64 {+ let ret: i64;+ unsafe {+ asm!(+ "syscall",+ inlateout("rax") nr as i64 => ret,+ in("rdi") a1,+ in("rsi") a2,+ in("rdx") a3,+ in("r10") a4,+ in("r8") a5,+ lateout("rcx") _,+ lateout("r11") _,+ options(nostack),+ );+ }+ ret+}+ pub fn read(fd: i64, buf: &mut [u8]) -> i64 { syscall3(SYS_READ, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64) }@@ -58,9 +129,29 @@ pub fn open(path: &str, flags: u64) -> i64 { pub fn close(fd: i64) -> i64 { syscall3(SYS_CLOSE, fd as u64, 0, 0) }+pub fn stat(path: &str, st: &mut Stat) -> i64 {+ syscall3(SYS_STAT, path.as_ptr() as u64, path.len() as u64, st as *mut Stat as u64)+}+pub fn fstat(fd: i64, st: &mut Stat) -> i64 {+ syscall3(SYS_FSTAT, fd as u64, st as *mut Stat as u64, 0)+}+pub fn lseek(fd: i64, offset: i64, whence: u64) -> i64 {+ syscall3(SYS_LSEEK, fd as u64, offset as u64, whence)+}+/// Returns (read end, write end), or the negative errno.+pub fn pipe() -> Result<(i64, i64), i64> {+ let mut fds = [0u32; 2];+ match syscall3(SYS_PIPE, fds.as_mut_ptr() as u64, 0, 0) {+ 0 => Ok((fds[0] as i64, fds[1] as i64)),+ e => Err(e),+ }+} pub fn sched_yield() { syscall3(SYS_SCHED_YIELD, 0, 0, 0); }+pub fn sleep_ms(ms: u64) {+ syscall3(SYS_SLEEP_MS, ms, 0, 0);+} pub fn getpid() -> i64 { syscall3(SYS_GETPID, 0, 0, 0) }@@ -68,6 +159,75 @@ pub fn exit(code: i64) -> ! { syscall3(SYS_EXIT, code as u64, 0, 0); loop {} // not reached }+/// Wait for child `pid` to end; returns its exit code (139 = killed).+pub fn wait(pid: i64) -> i64 {+ syscall3(SYS_WAIT, pid as u64, 0, 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)+}+pub fn chdir(path: &str) -> i64 {+ syscall3(SYS_CHDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn mkdir(path: &str) -> i64 {+ syscall3(SYS_MKDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn rmdir(path: &str) -> i64 {+ syscall3(SYS_RMDIR, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn unlink(path: &str) -> i64 {+ syscall3(SYS_UNLINK, path.as_ptr() as u64, path.len() as u64, 0)+}+pub fn getdents64(fd: i64, buf: &mut [u8]) -> i64 {+ syscall3(SYS_GETDENTS64, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64)+}+/// Start program `path` with arguments `argv` (NUL-separated, e.g.+/// "child\0hi"). Returns the child's pid (or -errno).+pub fn spawn(path: &str, argv: &str, opts: &SpawnOptions) -> i64 {+ syscall5(+ SYS_SPAWN,+ path.as_ptr() as u64,+ path.len() as u64,+ argv.as_ptr() as u64,+ argv.len() as u64,+ opts as *const SpawnOptions as u64,+ )+}++/// Walk the `linux_dirent64` records getdents64 put in `buf[..len]`,+/// calling `f(name, d_type)` for each (d_type: 4 = directory, 8 = file).+pub fn for_each_dirent(buf: &[u8], len: usize, mut f: impl FnMut(&str, u8)) {+ let mut pos = 0;+ while pos + 19 < len {+ let reclen = u16::from_le_bytes([buf[pos + 16], buf[pos + 17]]) as usize;+ let d_type = buf[pos + 18];+ let name = &buf[pos + 19..pos + reclen];+ let name = &name[..name.iter().position(|&b| b == 0).unwrap_or(name.len())];+ f(core::str::from_utf8(name).unwrap_or("?"), d_type);+ pos += reclen;+ }+}++// --- program arguments ---------------------------------------------------++/// `argv` as the kernel passes it: (pointer, length) pairs.+static mut ARGC: usize = 0;+static mut ARGV: *const [usize; 2] = core::ptr::null();++/// The program's arguments; `arg(0)` is the program name.+pub fn argc() -> usize {+ unsafe { ARGC }+}+pub fn arg(i: usize) -> Option<&'static str> {+ unsafe {+ if i >= ARGC {+ return None;+ }+ let [ptr, len] = *ARGV.add(i);+ core::str::from_utf8(core::slice::from_raw_parts(ptr as *const u8, len)).ok()+ }+} pub struct Stdout; @@ -92,9 +252,14 @@ macro_rules! println { }}; } -/// The kernel jumps here (see process.rs). Run main, exit with its result.+/// The kernel jumps here (see process.rs) with rdi = argc, rsi = argv.+/// Save the arguments, run main, exit with its result. #[unsafe(no_mangle)]-pub extern "C" fn _start() -> ! {+pub extern "C" fn _start(argc: usize, argv: *const [usize; 2]) -> ! {+ unsafe {+ ARGC = argc;+ ARGV = argv;+ } exit(crate::main()) }