/* * 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");