RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

foundry / rawabi.c
12004 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
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");