RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorTyler Fowler <tyler@localhost>2026-10-30 11:11:22 -0700
commitf3ac93a5e66e1b828a642807220156affbf9d100
parente8a1598981b9d4c220ff475716dda5dd7df785f3
treebrowse files at this commit
WIP 15.1: rawabi, a no-libc static Linux C binary (gcc -nostdlib), + test
Arawabi.c+274 -0

1 file changed, 274 insertions, 0 deletions

@@ -0,0 +1,274 @@+/*+ * rawabi.c - a tiny static Linux program with no libc at all (stage 15.1).+ *+ * This file knows nothing about Rustbelt: it is written against the plain+ * Linux x86_64 system call ABI (numbers, argument registers, C strings,+ * struct layouts) the way musl's own syscall layer is. If it passes here+ * and on a real Linux machine, musl's startup code and its file calls+ * have a good chance of working too.+ *+ * Build (build.rs does this with the host's gcc):+ *   cc -O2 -static -nostdlib -ffreestanding -fno-stack-protector -fpie -no-pie+ *      -Wl,-Ttext-segment=0x8000400000 -o rawabi rawabi.c+ * -fpie makes the code RIP-relative, so it works at Rustbelt's user base+ * (0x80_0000_0000, above 4 GiB) while staying an ordinary ET_EXEC file.+ *+ * Run: rawabi [scratch-dir]   (default /tmp). Exit status = failed checks.+ */++typedef unsigned long u64;+typedef long i64;++/* ---- raw system calls: number in rax, args in rdi rsi rdx r10 r8 r9 ---- */++static i64 sys6(i64 n, i64 a, i64 b, i64 c, i64 d, i64 e, i64 f) {+    register i64 r10 __asm__("r10") = d;+    register i64 r8 __asm__("r8") = e;+    register i64 r9 __asm__("r9") = f;+    i64 ret;+    __asm__ volatile("syscall"+                     : "=a"(ret)+                     : "a"(n), "D"(a), "S"(b), "d"(c), "r"(r10), "r"(r8), "r"(r9)+                     : "rcx", "r11", "memory");+    return ret;+}+#define sys0(n) sys6(n, 0, 0, 0, 0, 0, 0)+#define sys1(n, a) sys6(n, (i64)(a), 0, 0, 0, 0, 0)+#define sys2(n, a, b) sys6(n, (i64)(a), (i64)(b), 0, 0, 0, 0)+#define sys3(n, a, b, c) sys6(n, (i64)(a), (i64)(b), (i64)(c), 0, 0, 0)+#define sys4(n, a, b, c, d) sys6(n, (i64)(a), (i64)(b), (i64)(c), (i64)(d), 0, 0)++/* Linux x86_64 syscall numbers (arch/x86/entry/syscalls/syscall_64.tbl). */+enum {+    NR_read = 0, NR_write = 1, NR_close = 3, NR_stat = 4, NR_fstat = 5,+    NR_lseek = 8, NR_mmap = 9, NR_munmap = 11, NR_brk = 12,+    NR_rt_sigaction = 13, NR_rt_sigprocmask = 14, NR_ioctl = 16,+    NR_writev = 20, NR_access = 21, NR_getpid = 39, NR_fork = 57,+    NR_exit = 60, NR_wait4 = 61, NR_kill = 62, NR_uname = 63,+    NR_getcwd = 79, NR_chdir = 80, NR_rename = 82, NR_mkdir = 83,+    NR_rmdir = 84, NR_unlink = 87, NR_getuid = 102, NR_arch_prctl = 158,+    NR_getdents64 = 217, NR_set_tid_address = 218, NR_clock_gettime = 228,+    NR_clock_nanosleep = 230, NR_exit_group = 231, NR_openat = 257,+    NR_newfstatat = 262, NR_unlinkat = 263,+};++#define AT_FDCWD (-100)+#define AT_REMOVEDIR 0x200+#define O_RDONLY 0+#define O_WRONLY 1+#define O_CREAT 0100+#define O_TRUNC 01000+#define O_DIRECTORY 0200000+#define ENOENT 2+#define EFAULT 14+#define SIGUSR1 10+#define SA_RESTORER 0x04000000+#define ARCH_SET_FS 0x1002+#define S_IFMT 0170000+#define S_IFREG 0100000+#define S_IFDIR 0040000++/* Linux's x86_64 `struct stat` (144 bytes). */+struct kstat {+    u64 dev, ino, nlink;+    unsigned mode, uid, gid, pad0;+    u64 rdev;+    i64 size, blksize, blocks;+    i64 atime, atime_ns, mtime, mtime_ns, ctime, ctime_ns;+    i64 unused[3];+};+struct timespec { i64 sec, nsec; };+struct iovec { const void *base; u64 len; };+struct utsname { char f[6][65]; };          /* sysname, nodename, release, ... */+struct kaction { void (*handler)(int); u64 flags; void (*restorer)(void); u64 mask; };++/* gcc may emit calls to these even with -ffreestanding. */+void *memset(void *d, int c, u64 n) { char *p = d; while (n--) *p++ = (char)c; return d; }+void *memcpy(void *d, const void *s, u64 n) { char *p = d; const char *q = s; while (n--) *p++ = *q++; return d; }++/* ---- tiny output helpers ---- */++static u64 slen(const char *s) { u64 n = 0; while (s[n]) n++; return n; }+static void puts_(const char *s) { sys3(NR_write, 1, s, slen(s)); }+static void putu(u64 v) {+    char b[24]; int i = 23; b[i] = 0;+    do { b[--i] = (char)('0' + v % 10); v /= 10; } while (v);+    puts_(b + i);+}+static int seq(const char *a, const char *b) { while (*a && *a == *b) a++, b++; return *a == *b; }+/* Join dir and name into out (no bounds games: test names are short). */+static const char *join(char *out, const char *dir, const char *name) {+    char *p = out;+    while (*dir) *p++ = *dir++;+    *p++ = '/';+    while (*name) *p++ = *name++;+    *p = 0;+    return out;+}++static int failures;+static void check(int ok, const char *what) {+    puts_(ok ? "rawabi: ok   " : "rawabi: FAIL ");+    puts_(what);+    puts_("\n");+    if (!ok) failures++;+}++/* ---- SSE math: plain C doubles compile to SSE2 instructions on x86_64 ---- */++/* pi from the Leibniz series; `volatile` keeps the loop in xmm registers+ * doing real work instead of being folded at compile time. */+static double leibniz_pi(int terms) {+    volatile double sum = 0.0;+    for (int k = 0; k < terms; k++)+        sum += (k & 1 ? -4.0 : 4.0) / (2.0 * k + 1.0);+    return sum;+}+static double sqrt_sd(double x) { double r; __asm__("sqrtsd %1, %0" : "=x"(r) : "x"(x)); return r; }+/* Print a double with 6 decimals using integer math only. */+static void putf(double v) {+    if (v < 0) { puts_("-"); v = -v; }+    u64 whole = (u64)v, frac = (u64)((v - (double)whole) * 1e6 + 0.5);+    if (frac >= 1000000) { whole++; frac -= 1000000; }+    putu(whole); puts_(".");+    for (u64 d = 100000; d > frac && d > 1; d /= 10) puts_("0");+    putu(frac);+}++/* ---- a signal handler that does its own SSE math ---- */++static volatile int got_signal;+static volatile double handler_result;+static void on_usr1(int sig) {+    /* Clobbers xmm registers the interrupted code may be using; the kernel+     * must save them in the signal frame and put them back at sigreturn. */+    handler_result = sqrt_sd(16.0) * sqrt_sd(0.25);   /* = 2, exactly */+    got_signal = sig;+}+/* The restorer: what the handler "returns" into. musl uses the same. */+__asm__(".text\n.global rawabi_restorer\nrawabi_restorer:\n"+        "  mov $15, %eax\n  syscall\n  hlt\n");+__attribute__((visibility("hidden"))) void rawabi_restorer(void);++static u64 tls_block[4];   /* a stand-in thread pointer, like musl's */++static int run(int argc, char **argv) {+    const char *dir = argc > 1 ? argv[1] : "/tmp";+    char work[256], f1[256], f2[256], cwd[256];++    puts_("rawabi: a static Linux binary with no libc, pid ");+    putu((u64)sys0(NR_getpid));+    puts_("\n");++    /* What musl's __init_tls and __init_libc do first. */+    tls_block[0] = (u64)tls_block;  /* x86_64 TLS: fs:0 points to itself */+    i64 r = sys2(NR_arch_prctl, ARCH_SET_FS, tls_block);+    u64 self;+    __asm__ volatile("mov %%fs:0, %0" : "=r"(self));+    check(r == 0 && self == (u64)tls_block, "arch_prctl(ARCH_SET_FS) + %fs:0");+    check(sys1(NR_set_tid_address, &tls_block[1]) == sys0(NR_getpid), "set_tid_address returns the tid");+    u64 oldmask = ~0ul;+    check(sys4(NR_rt_sigprocmask, 0, 0, &oldmask, 8) == 0 && oldmask == 0, "rt_sigprocmask(.., oldset, 8)");+    i64 uid = sys0(NR_getuid);   /* 0 on Rustbelt (no users yet) */+    check(uid >= 0, "getuid");++    struct utsname u;+    check(sys1(NR_uname, &u) == 0 && seq(u.f[0], "Rustbelt") | seq(u.f[0], "Linux"), "uname");+    puts_("rawabi: uname: "); puts_(u.f[0]); puts_(" "); puts_(u.f[2]); puts_("\n");++    /* Memory: brk and an anonymous mmap, both written to. */+    u64 cur = (u64)sys1(NR_brk, 0);+    u64 grown = (u64)sys1(NR_brk, cur + 65536);+    if (grown == cur + 65536) ((volatile char *)cur)[65535] = 1;+    check(grown == cur + 65536, "brk grows the heap");+    char *m = (char *)sys6(NR_mmap, 0, 8192, 3, 0x22, -1, 0); /* RW, PRIVATE|ANON */+    if ((u64)m < (u64)-4096) m[8191] = 7;+    check((u64)m < (u64)-4096 && sys2(NR_munmap, m, 8192) == 0, "mmap/munmap anonymous");++    /* Files, with C-string paths and the *at() calls musl uses. */+    join(work, dir, "rawabi.d");+    join(f1, work, "a.txt");+    join(f2, work, "b.txt");+    sys1(NR_unlink, f1); sys1(NR_unlink, f2); sys1(NR_rmdir, work);   /* leftovers */+    check(sys2(NR_mkdir, work, 0755) == 0, "mkdir");+    int fd = (int)sys4(NR_openat, AT_FDCWD, f1, O_WRONLY | O_CREAT | O_TRUNC, 0644);+    struct iovec iov[2] = { { "hello ", 6 }, { "from rawabi\n", 12 } };+    check(fd >= 0 && sys3(NR_writev, fd, iov, 2) == 18, "openat(AT_FDCWD, O_CREAT) + writev");+    sys1(NR_close, fd);++    struct kstat st;+    check(sys2(NR_stat, f1, &st) == 0 && (st.mode & S_IFMT) == S_IFREG && st.size == 18, "stat: regular file, 18 bytes");+    check(sys4(NR_newfstatat, AT_FDCWD, work, &st, 0) == 0 && (st.mode & S_IFMT) == S_IFDIR, "newfstatat: directory");+    check(sys2(NR_access, f1, 4) == 0 && sys2(NR_access, f2, 0) == -ENOENT, "access: R_OK / ENOENT");+    check(sys2(NR_stat, (char *)0, &st) == -EFAULT, "stat(NULL) = -EFAULT");++    check(sys2(NR_rename, f1, f2) == 0 && sys2(NR_stat, f1, &st) == -ENOENT, "rename");+    char buf[64];+    fd = (int)sys4(NR_openat, AT_FDCWD, f2, O_RDONLY, 0);+    i64 n = sys3(NR_read, fd, buf, sizeof buf);+    struct kstat fst;+    check(n == 18 && buf[0] == 'h' && buf[17] == '\n', "read back the renamed file");+    check(sys2(NR_fstat, fd, &fst) == 0 && fst.size == 18 && sys3(NR_lseek, fd, 6, 0) == 6, "fstat + lseek");+    sys1(NR_close, fd);++    /* getdents64 on the directory finds b.txt. */+    fd = (int)sys4(NR_openat, AT_FDCWD, work, O_RDONLY | O_DIRECTORY, 0);+    char dents[512];+    int found = 0;+    n = sys3(NR_getdents64, fd, dents, sizeof dents);+    for (i64 off = 0; off < n;) {+        unsigned short reclen = *(unsigned short *)(dents + off + 16);+        if (seq(dents + off + 19, "b.txt")) found = 1;+        off += reclen;+    }+    sys1(NR_close, fd);+    check(found, "getdents64 lists b.txt");++    check(sys1(NR_chdir, work) == 0 && sys2(NR_getcwd, cwd, sizeof cwd) > 0 && seq(cwd, work), "chdir + getcwd");+    check(sys3(NR_unlinkat, AT_FDCWD, "b.txt", 0) == 0, "unlinkat(AT_FDCWD, relative path)");+    sys1(NR_chdir, dir);+    check(sys3(NR_unlinkat, AT_FDCWD, work, AT_REMOVEDIR) == 0, "unlinkat(AT_REMOVEDIR)");++    /* Time. */+    struct timespec t0, t1, nap = { 0, 20 * 1000 * 1000 };+    sys2(NR_clock_gettime, 1, &t0);+    i64 slept = sys4(NR_clock_nanosleep, 1, 0, &nap, 0);+    sys2(NR_clock_gettime, 1, &t1);+    i64 ms = (t1.sec - t0.sec) * 1000 + (t1.nsec - t0.nsec) / 1000000;+    check(slept == 0 && ms >= 19, "clock_nanosleep(CLOCK_MONOTONIC, 20 ms)");++    /* SSE math, then a signal whose handler clobbers xmm registers. */+    double pi = leibniz_pi(200000);+    double root = sqrt_sd(pi * pi);+    puts_("rawabi: pi ~ "); putf(pi); puts_(", sqrt(pi^2) = "); putf(root); puts_("\n");+    check(pi > 3.14158 && pi < 3.14160 && root == pi, "SSE double math");+    struct kaction sa = { on_usr1, SA_RESTORER, rawabi_restorer, 0 };+    check(sys4(NR_rt_sigaction, SIGUSR1, &sa, 0, 8) == 0, "rt_sigaction with SA_RESTORER");+    double before = pi * 2.0;          /* lives in an xmm register across... */+    sys2(NR_kill, sys0(NR_getpid), SIGUSR1);   /* ...the signal */+    double after = before / 2.0;+    check(got_signal == SIGUSR1 && handler_result == 2.0 && after == pi, "xmm state survives a signal handler");++    /* fork + wait4 with a rusage buffer, the way musl's waitpid calls it. */+    i64 pid = sys0(NR_fork);+    if (pid == 0) {+        double c = leibniz_pi(1000);   /* the child has its own FPU state */+        sys1(NR_exit, c > 3.0 ? 7 : 1);+    }+    int status = 0;+    char rusage[144];+    check(pid > 0 && sys4(NR_wait4, pid, &status, 0, rusage) == pid && ((status >> 8) & 0xff) == 7,+          "fork + wait4(pid, &status, 0, &rusage)");++    puts_(failures ? "rawabi: some checks FAILED\n" : "rawabi: all checks passed\n");+    return failures;+}++/* The ELF entry point. The kernel leaves argc at (%rsp), then argv[];+ * the stack is 16-byte aligned here, so align it the C way and call run(). */+__attribute__((used)) static void start_c(u64 *sp) {+    int argc = (int)sp[0];+    sys1(NR_exit_group, run(argc, (char **)(sp + 1)));+}+__asm__(".text\n.global _start\n_start:\n"+        "  xor %ebp, %ebp\n  mov %rsp, %rdi\n  and $-16, %rsp\n  call start_c\n  hlt\n");