test: Add userspace tests

This commit is contained in:
2026-10-27 23:11:15 +01:00
parent 196543c3d3
commit ba039a3e0a
13 changed files with 927 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: ISC
.intel_syntax noprefix
.text
.globl _start
.type _start, @function
_start:
xor ebp, ebp
and rsp, -16
call main
mov edi, eax
xor eax, eax
syscall
ud2
.size _start, . - _start
.section .note.GNU-stack, "", @progbits
+229
View File
@@ -0,0 +1,229 @@
// SPDX-License-Identifier: ISC
#pragma once
#include <stddef.h>
#include <stdint.h>
enum syscall_number : uintptr_t {
#define SYSCALL_DECLARE(number, name, num_args) SYS_##name = number,
#include <zinnia/syscall_list.inc>
#undef SYSCALL_DECLARE
};
enum syslog_kind : uintptr_t {
SYSLOG_ERR = 3,
SYSLOG_INFO = 6,
};
struct sys_ret {
uintptr_t value;
uintptr_t error;
};
static inline struct sys_ret syscall6(
enum syscall_number num,
uintptr_t a0,
uintptr_t a1,
uintptr_t a2,
uintptr_t a3,
uintptr_t a4,
uintptr_t a5
) {
register uintptr_t r10 asm("r10") = a3;
register uintptr_t r8 asm("r8") = a4;
register uintptr_t r9 asm("r9") = a5;
uintptr_t value;
uintptr_t error;
asm volatile("syscall"
: "=a"(value), "=d"(error)
: "a"(num), "D"(a0), "S"(a1), "d"(a2), "r"(r10), "r"(r8), "r"(r9)
: "rcx", "r11", "memory");
return (struct sys_ret){value, error};
}
static inline struct sys_ret syscall3(enum syscall_number num, uintptr_t a0, uintptr_t a1, uintptr_t a2) {
return syscall6(num, a0, a1, a2, 0, 0, 0);
}
static inline struct sys_ret syscall4(enum syscall_number num, uintptr_t a0, uintptr_t a1, uintptr_t a2, uintptr_t a3) {
return syscall6(num, a0, a1, a2, a3, 0, 0);
}
static inline struct sys_ret syscall5(
enum syscall_number num,
uintptr_t a0,
uintptr_t a1,
uintptr_t a2,
uintptr_t a3,
uintptr_t a4
) {
return syscall6(num, a0, a1, a2, a3, a4, 0);
}
[[noreturn]]
static inline void sys_exit(int status) {
syscall3(SYS_exit, (uintptr_t)status, 0, 0);
__builtin_unreachable();
}
static inline struct sys_ret sys_syslog(enum syslog_kind kind, const char* msg, size_t len) {
return syscall3(SYS_syslog, kind, (uintptr_t)msg, len);
}
static inline struct sys_ret sys_mmap(void* addr, size_t len, uint32_t prot, uint32_t flags, int fd, int64_t off) {
return syscall6(SYS_mmap, (uintptr_t)addr, len, prot, flags, (uintptr_t)fd, (uintptr_t)off);
}
static inline struct sys_ret sys_munmap(void* addr, size_t len) {
return syscall3(SYS_munmap, (uintptr_t)addr, len, 0);
}
static inline struct sys_ret sys_openat(int dirfd, const char* path, uint32_t flags, uint32_t mode) {
return syscall4(SYS_openat, (uintptr_t)dirfd, (uintptr_t)path, flags, mode);
}
static inline struct sys_ret sys_close(int fd) {
return syscall3(SYS_close, (uintptr_t)fd, 0, 0);
}
static inline struct sys_ret sys_pread(int fd, void* buf, size_t len, uint64_t off) {
return syscall4(SYS_pread, (uintptr_t)fd, (uintptr_t)buf, len, off);
}
static inline struct sys_ret sys_fstat(int fd, void* buf) {
return syscall3(SYS_fstat, (uintptr_t)fd, (uintptr_t)buf, 0);
}
static inline struct sys_ret sys_getuname(void* buf) {
return syscall3(SYS_getuname, (uintptr_t)buf, 0, 0);
}
static inline struct sys_ret sys_setuname(const void* buf) {
return syscall3(SYS_setuname, (uintptr_t)buf, 0, 0);
}
static inline struct sys_ret sys_mprotect(void* addr, size_t len, uint32_t prot) {
return syscall3(SYS_mprotect, (uintptr_t)addr, len, prot);
}
static inline struct sys_ret sys_madvise(void* addr, size_t len, int advice) {
return syscall3(SYS_madvise, (uintptr_t)addr, len, (uintptr_t)advice);
}
static inline struct sys_ret sys_msync(void* addr, size_t len, int flags) {
return syscall3(SYS_msync, (uintptr_t)addr, len, (uintptr_t)flags);
}
static inline struct sys_ret sys_pwrite(int fd, const void* buf, size_t len, uint64_t off) {
return syscall4(SYS_pwrite, (uintptr_t)fd, (uintptr_t)buf, len, off);
}
static inline struct sys_ret sys_readv(int fd, const void* iov, size_t count) {
return syscall3(SYS_readv, (uintptr_t)fd, (uintptr_t)iov, count);
}
static inline struct sys_ret sys_writev(int fd, const void* iov, size_t count) {
return syscall3(SYS_writev, (uintptr_t)fd, (uintptr_t)iov, count);
}
static inline struct sys_ret sys_seek(int fd, int64_t off, int whence) {
return syscall3(SYS_seek, (uintptr_t)fd, (uintptr_t)off, (uintptr_t)whence);
}
static inline struct sys_ret sys_fstatat(int dirfd, const char* path, void* buf, uint32_t flags) {
return syscall4(SYS_fstatat, (uintptr_t)dirfd, (uintptr_t)path, (uintptr_t)buf, flags);
}
static inline struct sys_ret sys_faccessat(int dirfd, const char* path, uint32_t mode, uint32_t flags) {
return syscall4(SYS_faccessat, (uintptr_t)dirfd, (uintptr_t)path, mode, flags);
}
static inline struct sys_ret sys_fcntl(int fd, int cmd, uintptr_t arg) {
return syscall3(SYS_fcntl, (uintptr_t)fd, (uintptr_t)cmd, arg);
}
static inline struct sys_ret sys_ftruncate(int fd, int64_t len) {
return syscall3(SYS_ftruncate, (uintptr_t)fd, (uintptr_t)len, 0);
}
static inline struct sys_ret sys_truncate(const char* path, int64_t len) {
return syscall3(SYS_truncate, (uintptr_t)path, (uintptr_t)len, 0);
}
static inline struct sys_ret sys_getcwd(char* buf, size_t len) {
return syscall3(SYS_getcwd, (uintptr_t)buf, len, 0);
}
static inline struct sys_ret sys_chdir(const char* path) {
return syscall3(SYS_chdir, (uintptr_t)path, 0, 0);
}
static inline struct sys_ret sys_fchdir(int fd) {
return syscall3(SYS_fchdir, (uintptr_t)fd, 0, 0);
}
static inline struct sys_ret sys_mkdirat(int dirfd, const char* path, uint32_t mode) {
return syscall3(SYS_mkdirat, (uintptr_t)dirfd, (uintptr_t)path, mode);
}
static inline struct sys_ret sys_rmdirat(int dirfd, const char* path) {
return syscall3(SYS_rmdirat, (uintptr_t)dirfd, (uintptr_t)path, 0);
}
static inline struct sys_ret sys_unlinkat(int dirfd, const char* path, uint32_t flags) {
return syscall3(SYS_unlinkat, (uintptr_t)dirfd, (uintptr_t)path, flags);
}
static inline struct sys_ret sys_getdents(int fd, void* buf, size_t len) {
return syscall3(SYS_getdents, (uintptr_t)fd, (uintptr_t)buf, len);
}
static inline struct sys_ret sys_renameat(int old_dirfd, const char* old_path, int new_dirfd, const char* new_path) {
return syscall4(SYS_renameat, (uintptr_t)old_dirfd, (uintptr_t)old_path, (uintptr_t)new_dirfd, (uintptr_t)new_path);
}
static inline struct sys_ret sys_linkat(
int old_dirfd,
const char* old_path,
int new_dirfd,
const char* new_path,
uint32_t flags
) {
return syscall5(
SYS_linkat,
(uintptr_t)old_dirfd,
(uintptr_t)old_path,
(uintptr_t)new_dirfd,
(uintptr_t)new_path,
flags
);
}
static inline struct sys_ret sys_symlinkat(const char* target, int dirfd, const char* path) {
return syscall3(SYS_symlinkat, (uintptr_t)target, (uintptr_t)dirfd, (uintptr_t)path);
}
static inline struct sys_ret sys_readlinkat(int dirfd, const char* path, char* buf, size_t len) {
return syscall4(SYS_readlinkat, (uintptr_t)dirfd, (uintptr_t)path, (uintptr_t)buf, len);
}
static inline struct sys_ret sys_dup(int fd) {
return syscall3(SYS_dup, (uintptr_t)fd, 0, 0);
}
static inline struct sys_ret sys_dup3(int old_fd, int new_fd, uint32_t flags) {
return syscall3(SYS_dup3, (uintptr_t)old_fd, (uintptr_t)new_fd, flags);
}
static inline struct sys_ret sys_umask(uint32_t mask) {
return syscall3(SYS_umask, mask, 0, 0);
}
static inline struct sys_ret sys_pipe(int* fds, uint32_t flags) {
return syscall3(SYS_pipe, (uintptr_t)fds, flags, 0);
}
static inline struct sys_ret sys_ppoll(void* fds, size_t nfds, const void* timeout, const void* sigmask) {
return syscall4(SYS_ppoll, (uintptr_t)fds, nfds, (uintptr_t)timeout, (uintptr_t)sigmask);
}
+210
View File
@@ -0,0 +1,210 @@
// SPDX-License-Identifier: ISC
#include "utest.h"
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "syscall.h"
constexpr size_t LINE_MAX = 240;
extern const struct utest_case __start_utest_cases[];
extern const struct utest_case __stop_utest_cases[];
size_t strlen(const char* str) {
size_t len = 0;
while (str[len])
len++;
return len;
}
int memcmp(const void* s1, const void* s2, size_t size) {
const uint8_t* a = s1;
const uint8_t* b = s2;
for (size_t i = 0; i < size; i++) {
if (a[i] != b[i])
return a[i] < b[i] ? -1 : 1;
}
return 0;
}
void* memcpy(void* restrict dest, const void* restrict src, size_t n) {
uint8_t* d = dest;
const uint8_t* s = src;
for (size_t i = 0; i < n; i++)
d[i] = s[i];
return dest;
}
void* memset(void* dest, int value, size_t n) {
uint8_t* d = dest;
for (size_t i = 0; i < n; i++)
d[i] = (uint8_t)value;
return dest;
}
bool utest_streq(const char* a, const char* b) {
if (!a || !b)
return a == b;
while (*a && *a == *b) {
a++;
b++;
}
return *a == *b;
}
struct line {
char buf[LINE_MAX];
size_t len;
};
static void put(struct line* line, char c) {
if (line->len < sizeof(line->buf) - 1)
line->buf[line->len++] = c;
}
static void put_str(struct line* line, const char* str) {
for (; *str; str++)
put(line, *str);
}
static void put_num(struct line* line, unsigned long long value, unsigned base, bool negative, bool prefix) {
char digits[24];
size_t n = 0;
do {
digits[n++] = "0123456789abcdef"[value % base];
value /= base;
} while (value);
if (negative)
put(line, '-');
if (prefix)
put_str(line, "0x");
while (n)
put(line, digits[--n]);
}
static void format(struct line* line, const char* fmt, va_list args) {
for (; *fmt; fmt++) {
if (*fmt != '%') {
put(line, *fmt);
continue;
}
fmt++;
bool alt = false;
if (*fmt == '#') {
alt = true;
fmt++;
}
int longs = 0;
bool size = false;
for (;; fmt++) {
if (*fmt == 'l')
longs++;
else if (*fmt == 'z')
size = true;
else
break;
}
switch (*fmt) {
case 'd':
case 'i': {
long long v = size ? (long long)va_arg(args, ptrdiff_t)
: longs ? va_arg(args, long long)
: va_arg(args, int);
put_num(line, v < 0 ? -(unsigned long long)v : (unsigned long long)v, 10, v < 0, false);
break;
}
case 'u':
case 'x': {
unsigned long long v = size ? va_arg(args, size_t)
: longs ? va_arg(args, unsigned long long)
: va_arg(args, unsigned int);
put_num(line, v, *fmt == 'x' ? 16 : 10, false, alt && *fmt == 'x');
break;
}
case 'p':
put_num(line, (uintptr_t)va_arg(args, void*), 16, false, true);
break;
case 's': {
const char* s = va_arg(args, const char*);
put_str(line, s ? s : "(null)");
break;
}
case 'c':
put(line, (char)va_arg(args, int));
break;
case '%':
put(line, '%');
break;
default:
put(line, '?');
break;
}
}
}
static void emit(struct line* line) {
line->buf[line->len] = 0;
sys_syslog(SYSLOG_INFO, line->buf, line->len);
}
void utest_log(const char* fmt, ...) {
struct line line = {};
va_list args;
va_start(args, fmt);
format(&line, fmt, args);
va_end(args);
emit(&line);
}
void utest_fail(struct utest* test, const char* file, int line_nr, const char* fmt, ...) {
struct line line = {};
put_str(&line, "# ");
put_str(&line, test->suite);
put(&line, '.');
put_str(&line, test->name);
put_str(&line, ": ");
put_str(&line, file);
put(&line, ':');
put_num(&line, (unsigned)line_nr, 10, false, false);
put_str(&line, ": ");
va_list args;
va_start(args, fmt);
format(&line, fmt, args);
va_end(args);
emit(&line);
test->failed = true;
}
void utest_run_all() {
size_t index = 0;
for (const struct utest_case* c = __start_utest_cases; c < __stop_utest_cases; c++) {
struct utest test = {.suite = c->suite, .name = c->name};
c->func(&test);
index++;
if (test.skipped)
utest_log("ok %zu %s.%s # SKIP %s", index, c->suite, c->name, test.skip_reason ? test.skip_reason : "");
else if (c->xfail_reason)
utest_log(
"%s %zu %s.%s # TODO %s",
test.failed ? "not ok" : "ok",
index,
c->suite,
c->name,
c->xfail_reason
);
else
utest_log("%s %zu %s.%s", test.failed ? "not ok" : "ok", index, c->suite, c->name);
}
utest_log("1..%zu", index);
}
int main() {
utest_run_all();
for (;;)
;
}
+90
View File
@@ -0,0 +1,90 @@
// SPDX-License-Identifier: ISC
#pragma once
#include <stddef.h> // IWYU pragma: keep
#include <stdint.h> // IWYU pragma: keep
struct utest {
const char* suite;
const char* name;
bool failed;
bool skipped;
const char* skip_reason;
};
struct utest_case {
const char* suite;
const char* name;
const char* xfail_reason;
void (*func)(struct utest* test);
};
#define _UTEST(suite_name, case_name, reason) \
static void suite_name##_##case_name(struct utest* test); \
[[gnu::used, gnu::retain, gnu::section("utest_cases"), gnu::aligned(8)]] \
static const struct utest_case suite_name##_##case_name##_case = { \
.suite = #suite_name, \
.name = #case_name, \
.xfail_reason = (reason), \
.func = suite_name##_##case_name, \
}; \
static void suite_name##_##case_name(struct utest* test)
// Expected success
#define UTEST(suite_name, case_name) _UTEST(suite_name, case_name, nullptr)
// Expected failure
#define UTEST_XFAIL(suite_name, case_name, reason) _UTEST(suite_name, case_name, reason)
[[gnu::format(printf, 1, 2)]]
void utest_log(const char* fmt, ...);
[[gnu::format(printf, 4, 5)]]
void utest_fail(struct utest* test, const char* file, int line, const char* fmt, ...);
void utest_run_all();
bool utest_streq(const char* a, const char* b);
#define _UTEST_CHECK(cond, on_fail, ...) \
do { \
if (!(cond)) { \
utest_fail(test, __FILE__, __LINE__, __VA_ARGS__); \
on_fail; \
} \
} while (0)
#define _UTEST_CMP(a, op, b, on_fail) \
do { \
const typeof(true ? (a) : (b)) _utest_a = (a); \
const typeof(true ? (a) : (b)) _utest_b = (b); \
_UTEST_CHECK( \
_utest_a op _utest_b, \
on_fail, \
"expected %s %s %s, got %#llx and %#llx", \
#a, \
#op, \
#b, \
(unsigned long long)_utest_a, \
(unsigned long long)_utest_b \
); \
} while (0)
#define EXPECT_TRUE(cond) _UTEST_CHECK(cond, (void)0, "expected %s", #cond)
#define EXPECT_FALSE(cond) _UTEST_CHECK(!(cond), (void)0, "expected !(%s)", #cond)
#define EXPECT_EQ(a, b) _UTEST_CMP(a, ==, b, (void)0)
#define EXPECT_NE(a, b) _UTEST_CMP(a, !=, b, (void)0)
#define EXPECT_GT(a, b) _UTEST_CMP(a, >, b, (void)0)
#define EXPECT_GE(a, b) _UTEST_CMP(a, >=, b, (void)0)
#define EXPECT_STREQ(a, b) _UTEST_CHECK(utest_streq((a), (b)), (void)0, "expected %s == \"%s\"", #a, (b))
#define ASSERT_TRUE(cond) _UTEST_CHECK(cond, return, "expected %s", #cond)
#define ASSERT_EQ(a, b) _UTEST_CMP(a, ==, b, return)
#define ASSERT_NE(a, b) _UTEST_CMP(a, !=, b, return)
#define SKIP(reason) \
do { \
test->skipped = true; \
test->skip_reason = (reason); \
return; \
} while (0)