From 558cf8fd128aaaa9a6ba969a826aae9978878974 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Thu, 3 Sep 2026 19:01:51 -0400 Subject: [PATCH] feat(start): _start, __libc_start_main, exit/atexit/environ --- crt/x86_64/crt1.s | 34 ++++ src/start/environ.c | 33 ++++ src/start/exit.c | 110 ++++++++++++ src/start/libc_start_main.c | 29 ++++ src/start/start.h | 67 ++++++++ src/start/tcb.h | 87 ++++++++++ src/start/tls.c | 293 ++++++++++++++++++++++++++++++++ tests/test_startup.c | 330 ++++++++++++++++++++++++++++++++++++ 8 files changed, 983 insertions(+) create mode 100644 crt/x86_64/crt1.s create mode 100644 src/start/environ.c create mode 100644 src/start/exit.c create mode 100644 src/start/libc_start_main.c create mode 100644 src/start/start.h create mode 100644 src/start/tcb.h create mode 100644 src/start/tls.c create mode 100644 tests/test_startup.c diff --git a/crt/x86_64/crt1.s b/crt/x86_64/crt1.s new file mode 100644 index 0000000..109ab2d --- /dev/null +++ b/crt/x86_64/crt1.s @@ -0,0 +1,34 @@ +/* + * vlibc — x86_64 _start (todo 3). + * + * The kernel enters the program here with the initial stack holding, in + * order from %rsp: argc, argv[0..argc-1], a NULL argv terminator, the + * environment strings, a NULL envp terminator, and the auxiliary vector. + * The kernel leaves %rsp 16-byte aligned at entry. + * + * _start forwards (main, argc, argv, envp) to __libc_start_main, matching + * vlibc's startup signature (src/start/start.h). The frame pointer is + * zeroed first so the outermost frame terminates backtraces, and %rsp is + * 16-byte aligned at the call site per the SysV AMD64 ABI. __libc_start_main + * never returns (it ends in exit), so the hlt loop below is unreachable; it + * exists only to satisfy the assembler and to fault loudly (hlt in user + * mode raises #GP) if a broken build ever falls through. + */ + + .text + .global _start + .type _start, @function +_start: + xor %ebp, %ebp # outermost frame: zero frame pointer + mov %rsp, %r8 # keep the raw initial stack for envp + pop %rsi # rsi = argc + mov %rsp, %rdx # rdx = argv + lea 16(%r8, %rsi, 8), %rcx # rcx = envp = initial rsp + 8 + 8*(argc+1) + lea main(%rip), %rdi # rdi = &main + and $-16, %rsp # ABI: rsp % 16 == 0 at the call site + call __libc_start_main # _Noreturn: routes main's result into exit +1: hlt + jmp 1b + .size _start, . - _start + + .section .note.GNU-stack, "", @progbits diff --git a/src/start/environ.c b/src/start/environ.c new file mode 100644 index 0000000..66396a4 --- /dev/null +++ b/src/start/environ.c @@ -0,0 +1,33 @@ +#include "../internal/libc.h" +#include "start.h" + +/* + * vlibc — environ (todo 3). + * + * POSIX exposes the process environment as the global `environ`: a pointer + * to the array of "name=value" strings, terminated by a NULL pointer. The + * kernel places the initial environment on the stack at exec time; crt1.s + * forwards the envp pointer to __libc_start_main, which stores it here + * before main runs, so consumers see a fully initialized environment from + * the first line of main. + * + * environ is the single storage object. __environ is an internal hidden + * alias of that same storage: library code (getenv/setenv, todo 13) goes + * through the internal name so the public symbol is never referenced from + * inside the library, and setenv mutating environ is visible through + * __environ and vice versa. A plain exported mutable global is exactly what + * POSIX requires (setenv replaces the array pointer). + * + * exit() deliberately does not touch environ: the strings and pointer array + * live on the kernel-provided initial stack, not the heap — there is + * nothing to free. + */ + +char **environ = 0; + +/* + * Internal alias of environ: same storage, hidden visibility. The alias is + * what makes the two spellings indistinguishable to setenv/getenv. + */ +hidden extern __typeof(environ) __environ + __attribute__((__alias__("environ"))); // NOLINT(bugprone-reserved-identifier) diff --git a/src/start/exit.c b/src/start/exit.c new file mode 100644 index 0000000..6a7e558 --- /dev/null +++ b/src/start/exit.c @@ -0,0 +1,110 @@ +#include "../internal/syscall.h" +#include "start.h" + +/* + * vlibc — exit/_Exit/_exit/quick_exit/atexit/at_quick_exit (todo 3). + * + * Handler storage is a FIXED pre-allocated array (VLIBC_EXIT_HANDLERS_MAX + * entries): exit must work when malloc cannot, and the allocator is a later + * todo — no allocation happens here, ever. The atexit list and the + * at_quick_exit list are STRICTLY SEPARATE per POSIX: exit() runs only the + * atexit list, quick_exit() only the at_quick_exit list, never both. + * + * Order: both lists run in reverse registration order (LIFO) — the handler + * registered last runs first (C11 7.22.4.4, POSIX exit()). + * + * Registration once termination has begun is undefined behavior per POSIX. + * Rather than corrupt the list or crash, atexit/at_quick_exit gracefully + * refuse: the terminating flag makes them return -1. Registering a 33rd + * handler (list full) also returns -1, the POSIX failure convention. A + * handler calling exit() again is likewise UB; the count-based loops below + * simply keep running the remaining handlers — deterministic, no crash. + * + * Termination always goes through SYS_exit_group, so every thread of the + * process dies — the correct semantics once threading lands (#44). + * + * exit() is required to flush all open streams and remove tmpfile() + * creations before terminating. No streams exist yet: the stdio flush hook + * belongs to the stdio todo (#28) and slots in at the marker below. + */ + +#define VLIBC_EXIT_HANDLERS_MAX 32 + +struct exit_handler_list +{ + void (*fns[VLIBC_EXIT_HANDLERS_MAX])(void); + int count; +}; + +static struct exit_handler_list atexit_list; +static struct exit_handler_list quick_list; +static int terminating; + +int +atexit(void (*func)(void)) +{ + if (func == 0 || terminating || atexit_list.count >= VLIBC_EXIT_HANDLERS_MAX) + { + return -1; + } + atexit_list.fns[atexit_list.count] = func; + atexit_list.count++; + return 0; +} + +int +at_quick_exit(void (*func)(void)) +{ + if (func == 0 || terminating || quick_list.count >= VLIBC_EXIT_HANDLERS_MAX) + { + return -1; + } + quick_list.fns[quick_list.count] = func; + quick_list.count++; + return 0; +} + +void +exit(int status) +{ + terminating = 1; + while (atexit_list.count > 0) + { + atexit_list.count--; + atexit_list.fns[atexit_list.count](); + } + /* stdio hook (#28): flush + close all streams, remove tmpfile files. */ + _exit(status); +} + +void +quick_exit(int status) +{ + terminating = 1; + while (quick_list.count > 0) + { + quick_list.count--; + quick_list.fns[quick_list.count](); + } + _exit(status); +} + +void +_exit(int status) +{ + __syscall1(SYS_exit_group, (long)status); + /* + * exit_group never returns for a valid status; the loop is defensive + * (and satisfies the noreturn contract without unreachable-code UB). + */ + for (;;) + { + __syscall1(SYS_exit, (long)status); + } +} + +void +_Exit(int status) +{ + _exit(status); +} diff --git a/src/start/libc_start_main.c b/src/start/libc_start_main.c new file mode 100644 index 0000000..3c28d54 --- /dev/null +++ b/src/start/libc_start_main.c @@ -0,0 +1,29 @@ +#include "start.h" + +/* + * vlibc — __libc_start_main (todo 3). + * + * Called by crt1.s's _start with (main, argc, argv, envp) after the kernel + * has placed argc/argv/envp/auxv on the initial stack. In order: + * + * 1. install environ from the kernel-provided envp — before main runs, so + * consumers see the environment from the first line of main; + * 2. bootstrap main-thread TLS (static case; init_main_tls skips itself + * when the FS thread pointer is already set, the dynamic-loader #62 + * case); + * 3. call main(argc, argv); + * 4. route main's return value into exit(). + * + * Never returns. The signature is vlibc's own; crt1.s and the future + * dynamic loader (#62) must match it. + */ + +void +__libc_start_main( + int (*main_fn)(int, char **, char **), int argc, char **argv, + char **envp) // NOLINT(bugprone-reserved-identifier,bugprone-easily-swappable-parameters) +{ + environ = envp; + init_main_tls(envp); + exit(main_fn(argc, argv, envp)); +} diff --git a/src/start/start.h b/src/start/start.h new file mode 100644 index 0000000..2a82b4c --- /dev/null +++ b/src/start/start.h @@ -0,0 +1,67 @@ +#ifndef VLIBC_INTERNAL_START_H +#define VLIBC_INTERNAL_START_H + +/* + * vlibc — program startup / termination surface (todo 3). + * + * The canonical prototypes of the startup ABI. These names are mandated by + * the C standard and POSIX, not invented by vlibc: exit/_Exit/atexit/ + * at_quick_exit belong to and _exit to , environ is + * the POSIX process environment global, and __libc_start_main is the CRT + * ABI entry point that crt1.s calls. Once / land (later + * todos) their declarations move there; internal code keeps including this + * header until then, and consumers declare the names themselves. + * + * The standard mandates several names in the implementation- or + * standard-reserved namespace (_Exit, _exit, at_quick_exit, + * __libc_start_main, __environ); the NOLINT waiver below covers exactly + * those mandated names, nothing else. + */ + +#include "../internal/libc.h" + +// NOLINTBEGIN(bugprone-reserved-identifier,bugprone-easily-swappable-parameters) + +/* Termination (src/start/exit.c). Never return. */ +void +exit(int status) __attribute__((__noreturn__)); +void +_Exit(int status) __attribute__((__noreturn__)); +void +_exit(int status) __attribute__((__noreturn__)); +void +quick_exit(int status) __attribute__((__noreturn__)); + +/* Handler registration (src/start/exit.c). 0 on success, -1 on failure. */ +int +atexit(void (*func)(void)); +int +at_quick_exit(void (*func)(void)); + +/* + * CRT ABI entry point (src/start/libc_start_main.c), called by crt1.s with + * (main, argc, argv, envp). Never returns: it routes main's result into + * exit(). The future dynamic loader (#62) must use the same signature. + */ +void +__libc_start_main(int (*main_fn)(int, char **, char **), int argc, char **argv, char **envp) + __attribute__((__noreturn__)); + +/* The POSIX process environment (src/start/environ.c). */ +extern char **environ; + +/* Internal alias of the same storage, hidden visibility (src/start/environ.c). */ +extern char **__environ; + +// NOLINTEND(bugprone-reserved-identifier,bugprone-easily-swappable-parameters) + +/* + * Main-thread TLS bootstrap (src/start/tls.c): parse the auxv for the + * program headers, find PT_TLS, allocate the static TLS block + TCB, copy + * the image, and install the FS thread pointer via arch_prctl(ARCH_SET_FS). + * Skips itself when FS is already set (the dynamic-loader case, #62). + */ +hidden void +init_main_tls(char **envp); + +#endif /* VLIBC_INTERNAL_START_H */ diff --git a/src/start/tcb.h b/src/start/tcb.h new file mode 100644 index 0000000..b395951 --- /dev/null +++ b/src/start/tcb.h @@ -0,0 +1,87 @@ +#ifndef VLIBC_INTERNAL_START_TCB_H +#define VLIBC_INTERNAL_START_TCB_H + +/* + * vlibc — AUTHORITATIVE x86_64 TCB / thread-pointer layout (todo 3). + * + * This header defines the thread control block (TCB) layout ONCE, for the + * whole library. Later layers reference these constants and NEVER redefine + * them: + * + * #44 threading substrate: clones this layout for every new thread and + * initializes the fenv slot per thread. + * #45 sizes new-thread TLS from the static TLS block arithmetic below. + * #62 ld.so: performs the dynamic-case equivalent of this bootstrap (the + * FS-already-set guard in tls.c skips this code in that case). + * #42 fenv: reads/writes the reserved fenv slot through the fixed + * offsets below; the slot is per-thread lazy storage for the x87 + * control word and MXCSR. + * + * ABI model — ELF TLS variant II on x86_64: + * + * The thread pointer (TP) is delivered by the %fs segment and points + * DIRECTLY at the TCB (this is what "variant II" means; variant I, used by + * e.g. 32-bit x86, points at the end of the TLS data instead). Static TLS + * data live at NEGATIVE offsets from TP (%fs:N with N < 0, the LE model); + * the TCB fields live at positive offsets. Slot 0 is required to hold the + * TCB's own address because __builtin_thread_pointer() reads %fs:0 — that + * is the entire mechanism by which C code recovers TP from a register. + * + * TCB field map (offsets are byte offsets from TP, i.e. from %fs:0): + * + * TP + 0 VLIBC_TCB_SELF_OFF TCB self pointer (TP itself). + * TP + 8 VLIBC_TCB_ERRNO_OFF per-thread errno slot: 8 bytes, errno + * occupies the low 4. This offset is OWNED + * by src/internal/errno.h (task 1 fixed it + * as an ABI constant) and is consumed + * here, never re-derived. + * TP + 16 VLIBC_TCB_FENV_OFF per-thread fenv reservation, 8 bytes: + * +16 VLIBC_TCB_FENV_X87CW_OFF uint16 x87 control word, + * reset default 0x037F (double + * precision, round-to-nearest) + * +18 (pad, reserved) + * +20 VLIBC_TCB_FENV_MXCSR_OFF uint32 MXCSR, reset default + * 0x1F80 (all exceptions masked) + * TP + 24 end of the fixed layout; VLIBC_TCB_SIZE = 24. + * + * The layout grows FORWARD from TP if later layers need more TCB fields; + * the fixed slots above are never renumbered. A DTV pointer is deliberately + * NOT reserved: the static TLS model needs no DTV (every access is + * %fs-relative with a link-time offset; there is no __tls_get_addr). Should + * #62's dynamic TLS ever require a DTV, add it at TP + VLIBC_TCB_SIZE — + * glibc's variant-II convention keeps the DTV at a negative offset relative + * to a separately allocated block; vlibc deliberately keeps the simpler + * grow-upward model documented here. + * + * Static TLS placement contract (tls.c implements this; the linker bakes + * its TPOFF arithmetic against the same formula, verified empirically with + * GNU ld): + * + * - tp = base + round_up(p_memsz, p_align), with base page-aligned. + * - the PT_TLS image is copied to [base, base + p_filesz), the BSS tail + * [base + p_filesz, base + p_memsz) is zeroed, and the TCB occupies + * [tp, tp + VLIBC_TCB_SIZE). + * - TP alignment equals p_align EXACTLY (never bumped to 16): the linker + * computes tpoff = var_offset - round_up(p_memsz, p_align), so any + * other rounding breaks every %fs offset when p_memsz is not a + * multiple of the runtime's alignment. When the program has no PT_TLS + * at all, p_align is taken as 16 and the block is TCB-only. + * + * TCB fields only need 8-byte alignment, and the linker guarantees + * p_align >= the alignment of every TLS object, so the exact-p_align + * rule is always sufficient. + */ + +#include "../internal/errno.h" /* VLIBC_TCB_ERRNO_OFF */ + +#define VLIBC_TCB_SELF_OFF 0 + +/* VLIBC_TCB_ERRNO_OFF (8) is consumed from src/internal/errno.h. */ + +#define VLIBC_TCB_FENV_OFF 16 +#define VLIBC_TCB_FENV_X87CW_OFF (VLIBC_TCB_FENV_OFF + 0) +#define VLIBC_TCB_FENV_MXCSR_OFF (VLIBC_TCB_FENV_OFF + 4) + +#define VLIBC_TCB_SIZE 24 + +#endif /* VLIBC_INTERNAL_START_TCB_H */ diff --git a/src/start/tls.c b/src/start/tls.c new file mode 100644 index 0000000..472c95d --- /dev/null +++ b/src/start/tls.c @@ -0,0 +1,293 @@ +#include +#include + +#include "../internal/syscall.h" +#include "start.h" +#include "tcb.h" + +/* + * vlibc — main-thread TLS bootstrap (todo 3). + * + * Makes the TCB-slot errno (and any other TCB state) work from the very + * first libc call in a STATICALLY linked program. The dynamic case is + * explicitly out of scope here: when the FS thread pointer is already set + * (the dynamic loader #62 installed it before transferring control), this + * function detects that and returns without touching anything. + * + * Mechanism: + * - The kernel puts the auxiliary vector on the initial stack right after + * envp's NULL terminator; AT_PHDR/AT_PHENT/AT_PHNUM locate the ELF + * program headers, which are searched for PT_TLS. + * - One anonymous private mmap holds the whole static TLS area: the TLS + * image at the base, the TCB at the top. The placement formula (tp = + * base + round_up(p_memsz, p_align)) is the exact arithmetic the linker + * baked into every %fs-relative offset; see tcb.h for the contract. + * - The image is copied from memory at p_vaddr: for a static non-PIE + * executable GNU ld lays PT_TLS out inside the RW PT_LOAD, so the + * template is already mapped (verified empirically). When no PT_LOAD + * covers the segment — a layout this project does not produce — the + * file fallback reads the segment from /proc/self/exe via pread. + * - The TCB's self pointer, errno slot, and fenv defaults are written, + * then arch_prctl(ARCH_SET_FS) installs TP. From that instruction on, + * __builtin_thread_pointer() (which reads %fs:0) returns TP and errno + * works. + * + * No errno is touched before arch_prctl: errno lives in the TCB, which does + * not exist until this function creates it — a pre-TLS errno read would + * dereference %fs:0 with the kernel's post-exec FS base of 0 and fault on + * the unmapped NULL page. Failures therefore terminate hard (exit 127 via + * exit_group) instead of reporting through errno. + */ + +/* arch_prctl codes (kernel UAPI asm/prctl.h). */ +#define ARCH_SET_FS 0x1002 +#define ARCH_GET_FS 0x1003 + +/* auxv a_type values (kernel UAPI asm/auxvec.h). AT_RANDOM is deliberately + * not consumed: nothing in this todo needs a per-process random seed. */ +#define AT_NULL 0 +#define AT_PHDR 3 +#define AT_PHENT 4 +#define AT_PHNUM 5 + +/* ELF program header p_type values (kernel UAPI linux/elf.h). */ +#define PT_LOAD 1 +#define PT_TLS 7 + +/* mmap/openat/pread constants (kernel UAPI). */ +#define PROT_READ 0x1 +#define PROT_WRITE 0x2 +#define MAP_PRIVATE 0x2 +#define MAP_ANONYMOUS 0x20 +#define AT_FDCWD (-100) +#define O_RDONLY 0 +#define O_CLOEXEC 0x80000 + +/* ELF64 program header, exactly as laid out in the file/memory image. */ +struct elf64_phdr +{ + uint32_t p_type; + uint32_t p_flags; + uint64_t p_offset; + uint64_t p_vaddr; + uint64_t p_paddr; + uint64_t p_filesz; + uint64_t p_memsz; + uint64_t p_align; +}; + +/* + * Round v up to a multiple of a. a must be a power of two, which ELF + * p_align of a PT_TLS segment always is. + */ +static size_t +round_up(size_t v, size_t a) +{ + return (v + a - 1) & ~(a - 1); +} + +/* + * Hard-stop for unrecoverable bootstrap failures (mmap ENOMEM, a TLS image + * that is neither mapped nor readable). exit 127 — errno is unavailable + * here, because the TCB that would hold it does not exist yet. + */ +static _Noreturn void +tls_fail(void) +{ + __syscall1(SYS_exit_group, 127); + __builtin_unreachable(); +} + +/* + * The helpers below take structurally similar out-parameters (auxv and ELF + * layout values), so the easily-swappable-parameters check is waived for + * this section: these are kernel/ELF ABI shapes, not caller-facing APIs. + */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters) + +/* + * Walk the auxiliary vector on the initial stack: envp points at the first + * environment string, the auxv follows envp's NULL terminator as pairs of + * (type, value) longs, ending at AT_NULL. The kernel wrote these on the + * stack at exec time, so the ORIGINAL envp (never the possibly-mutated + * environ global) must be used. + */ +static void +find_auxv(char **envp, uintptr_t *phdr, uintptr_t *phent, uintptr_t *phnum) +{ + while (*envp != 0) + { + envp++; + } + envp++; /* step over the NULL terminator */ + for (uintptr_t *aux = (uintptr_t *)envp; aux[0] != AT_NULL; aux += 2) + { + if (aux[0] == AT_PHDR) + { + *phdr = aux[1]; + } + else if (aux[0] == AT_PHENT) + { + *phent = aux[1]; + } + else if (aux[0] == AT_PHNUM) + { + *phnum = aux[1]; + } + } +} + +/* + * Search the program headers for PT_TLS and check whether the image is + * already mapped (some PT_LOAD covers [p_vaddr, p_vaddr + p_filesz)). + * Returns 1 when PT_TLS exists, filling the out parameters. + */ +static int +find_tls(uintptr_t phdr, uintptr_t phent, uintptr_t phnum, uint64_t *offset, uint64_t *vaddr, + size_t *filesz, size_t *memsz, size_t *align, int *mapped) +{ + int found = 0; + uint64_t lo = 0; + uint64_t hi = 0; + + for (uintptr_t i = 0; i < phnum; i++) + { + const struct elf64_phdr *p = (const struct elf64_phdr *)(phdr + i * phent); + if (p->p_type == PT_TLS) + { + *offset = p->p_offset; + *vaddr = p->p_vaddr; + *filesz = (size_t)p->p_filesz; + *memsz = (size_t)p->p_memsz; + *align = (size_t)p->p_align; + lo = p->p_vaddr; + hi = p->p_vaddr + p->p_filesz; + found = 1; + } + } + + *mapped = 0; + if (found) + { + for (uintptr_t i = 0; i < phnum; i++) + { + const struct elf64_phdr *p = (const struct elf64_phdr *)(phdr + i * phent); + if (p->p_type == PT_LOAD && p->p_vaddr <= lo && hi <= p->p_vaddr + p->p_memsz) + { + *mapped = 1; + break; + } + } + } + return found; +} + +/* + * Copy the initial TLS image (the .tdata template; .tbss is zeroed by the + * caller). Primary path: memcpy from the mapped image. Fallback: pread the + * segment out of /proc/self/exe — immune to chdir, works even for deleted + * executables, no host libc involved. + */ +static void +copy_tls_image(char *dst, uint64_t offset, uint64_t vaddr, size_t filesz, int mapped) +{ + if (mapped) + { + memcpy(dst, (const void *)vaddr, filesz); + return; + } + + long fd = __syscall3(SYS_openat, AT_FDCWD, (long)"/proc/self/exe", O_RDONLY | O_CLOEXEC); + if (fd < 0) + { + tls_fail(); + } + size_t done = 0; + while (done < filesz) + { + long r = __syscall4(SYS_pread64, fd, (long)(dst + done), (long)(filesz - done), + (long)(offset + done)); + if (r <= 0) + { + tls_fail(); + } + done += (size_t)r; + } + __syscall1(SYS_close, fd); +} + +// NOLINTEND(bugprone-easily-swappable-parameters) + +void +init_main_tls(char **envp) +{ + uintptr_t fs_base = 0; + uintptr_t phdr = 0, phent = 0, phnum = 0; + uint64_t offset = 0, vaddr = 0; + size_t filesz = 0, memsz = 0, align = 16; + int mapped = 0; + size_t block, total; + uintptr_t base, tp; + char *image; + + /* + * Static-case guard. arch_prctl(ARCH_GET_FS) is used instead of + * __builtin_thread_pointer() because the latter dereferences %fs:0, + * and with the kernel's post-exec FS base of 0 that read faults on the + * unmapped NULL page. A non-zero FS base means the dynamic loader (#62) + * already installed a TCB: leave it alone. + */ + __syscall2(SYS_arch_prctl, ARCH_GET_FS, (long)&fs_base); + if (fs_base != 0) + { + return; + } + + find_auxv(envp, &phdr, &phent, &phnum); + if (phdr != 0 && phent != 0 && + !find_tls(phdr, phent, phnum, &offset, &vaddr, &filesz, &memsz, &align, &mapped)) + { + /* No TLS in this program: TCB-only block, arbitrary alignment. */ + filesz = 0; + memsz = 0; + align = 16; + } + + /* + * Single allocation: TLS image at the base, TCB at tp. tp uses exactly + * p_align (never bumped): the linker bakes tpoff against + * round_up(p_memsz, p_align) and any other rounding breaks the + * %fs-relative offsets (see tcb.h). + */ + block = round_up(memsz, align); + total = block + VLIBC_TCB_SIZE; + + base = (uintptr_t)__syscall6(SYS_mmap, 0, (long)total, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if ((long)base < 0 && (long)base > -4096) + { + /* ENOMEM and friends: no errno possible yet (the TCB IS errno). */ + tls_fail(); + } + + tp = base + block; + image = (char *)base; + + if (filesz > 0) + { + copy_tls_image(image, offset, vaddr, filesz, mapped); + } + if (memsz > filesz) + { + memset(image + filesz, 0, memsz - filesz); + } + + /* Build the TCB at TP (authoritative layout in tcb.h). */ + ((uintptr_t *)tp)[VLIBC_TCB_SELF_OFF / sizeof(uintptr_t)] = tp; + *(int *)(tp + VLIBC_TCB_ERRNO_OFF) = 0; + *(uint16_t *)(tp + VLIBC_TCB_FENV_X87CW_OFF) = 0x037F; /* x87 reset default */ + *(uint32_t *)(tp + VLIBC_TCB_FENV_MXCSR_OFF) = 0x1F80; /* MXCSR reset default */ + + /* From here on the TCB (and errno) is live. Install the thread pointer. */ + __syscall2(SYS_arch_prctl, ARCH_SET_FS, (long)tp); +} diff --git a/tests/test_startup.c b/tests/test_startup.c new file mode 100644 index 0000000..3c6abe4 --- /dev/null +++ b/tests/test_startup.c @@ -0,0 +1,330 @@ +/* + * vlibc — startup test (todo 3). + * + * Exercises the static program-startup path end to end. This test is + * COMPILED INTO A STATIC VLIBC BINARY (harness commands in + * .omo/evidence/task-3-full-posix.log): glibc is never linked, so every + * exit()/atexit()/environ reference resolves to vlibc's own symbols via + * crt1.s and the src/start objects. Running it under the host libc would + * silently test glibc, not vlibc — the static link is the test. + * + * Modes (argv[1]): + * (none) atexit LIFO: register A, B, C on the atexit list, Q on the + * at_quick_exit list, then return 42 from main. exit() must run + * C, B, A in that order (reverse registration), must NOT run Q, + * and the process must exit with status 42. + * -q quick_exit(5): the at_quick_exit handler runs, the atexit + * handler must NOT. Status must be 5. + * -z _exit(7): neither handler list runs at all. Status 7. + * -e argv/env plumbing: prints argc and argv, checks + * VLIBC_TEST_VAR=hello in environ. Status 0 on match. + * -t main-thread TLS + errno: a file-scope __thread int set in main + * must read back through a separate function (exercises the + * PT_TLS copy path), and errno must round-trip through the TCB + * slot (proves the bootstrap ran before the first libc call). + * -x handler-list limits: 32 registrations fit, the 33rd returns + * -1, and atexit() during exit is gracefully refused (-1). + * + * No host headers: -Iinclude shadows GCC's internal headers (see + * tests/test_strerror.c). / below are vlibc's own. + * All diagnostics go through raw SYS_write. + */ + +#include +#include + +#include "../src/internal/syscall.h" + +/* + * Manual declarations of the startup surface under test: and + * are later todos; consumers today declare exactly these names, + * matching src/start/start.h. + */ +[[noreturn]] void +exit(int status); +[[noreturn]] void +_Exit(int status); +[[noreturn]] void +_exit(int status); +[[noreturn]] void +quick_exit(int status); +int +atexit(void (*func)(void)); +int +at_quick_exit(void (*func)(void)); +extern char **environ; + +static int failures; + +/* Write a NUL-terminated string to fd via the raw syscall layer. */ +static void +say(int fd, const char *s) +{ + long n = 0; + + while (s[n] != '\0') + { + n++; + } + __syscall3(SYS_write, fd, (long)s, n); +} + +/* Write v in decimal to fd. */ +static void +say_dec(int fd, unsigned long v) // NOLINT(bugprone-easily-swappable-parameters) +{ + char buf[24]; + int i = (int)sizeof(buf); + + buf[--i] = '\0'; + do + { + buf[--i] = (char)('0' + (v % 10)); + v /= 10; + } while (v != 0); + __syscall3(SYS_write, fd, (long)(buf + i), (long)(sizeof(buf) - 1 - i)); +} + +static void +check(int cond, const char *what) +{ + if (!cond) + { + say(2, "FAIL: "); + say(2, what); + say(2, "\n"); + failures++; + } +} + +/* True when s is exactly lit. Hand-rolled: this test keeps deps minimal. */ +static int +str_is(const char *s, const char *lit) +{ + int i = 0; + + while (lit[i] != '\0' && s[i] == lit[i]) + { + i++; + } + return lit[i] == '\0' && s[i] == '\0'; +} + +/* True when environ holds an entry spelling "name=want". */ +static int // NOLINT(bugprone-easily-swappable-parameters) +env_matches(const char *name, const char *want) +{ + for (char **e = environ; e != 0 && *e != 0; e++) + { + const char *s = *e; + int i = 0; + + while (name[i] != '\0' && s[i] == name[i]) + { + i++; + } + if (name[i] == '\0' && s[i] == '=') + { + int j = 0; + + while (want[j] != '\0' && s[i + 1 + j] == want[j]) + { + j++; + } + if (want[j] == '\0' && s[i + 1 + j] == '\0') + { + return 1; + } + } + } + return 0; +} + +/* Exit handlers. Each prints one distinguishing marker line. */ +static void +hA(void) +{ + say(1, "A\n"); +} + +static void +hB(void) +{ + say(1, "B\n"); +} + +static void +hC(void) +{ + say(1, "C\n"); +} + +static void +hQ(void) +{ + say(1, "Q\n"); +} + +static void +hNoop(void) +{} + +/* Registered as one of the 32 in -x mode: registration during exit must fail. */ +static void +hRegisterDuringExit(void) +{ + if (atexit(hA) == 0) + { + say(2, "FAIL: atexit() during exit accepted (want graceful -1)\n"); + failures++; + } +} + +/* + * File-scope __thread objects: live in PT_TLS, every access is %fs-relative + * with a link-time offset. tls_preinit tests the .tdata image copy (nonzero + * initial value), tls_counter tests the .tbss zeroing plus the write/read + * round trip. The reads go through noinline helpers on opaque pointers: + * a direct read of a static TLS object whose initializer the compiler can + * see gets constant-folded, which would delete the variable (and with it + * the PT_TLS image) and silently untest the copy path. The address-of and + * the helper-force a real TLS relocation and a real memory load. + */ +static __thread int tls_preinit = 42; +static __thread int tls_counter; + +static int __attribute__((noinline)) +tls_deref(int *p) +{ + /* Opaque barrier: prevents interprocedural folding of *p to 42, which + * would delete the TLS image (see the block comment above). */ + __asm__ volatile("" : "+r"(p)); + return *p; +} + +static int __attribute__((noinline)) +tls_readback(void) +{ + return tls_counter; +} + +/* + * Scenario 1: atexit LIFO. exit() must run the LAST registered handler + * first and skip the at_quick_exit list entirely. + */ +static int +lifo_scenario(void) +{ + atexit(hA); + atexit(hB); + atexit(hC); + at_quick_exit(hQ); /* must NOT run: no "Q" on stdout */ + return 42; /* routed into exit(42) by __libc_start_main */ +} + +/* Scenario 2: quick_exit runs ONLY at_quick_exit handlers. */ +static int +quick_scenario(void) +{ + atexit(hA); /* must NOT run: no "A" on stdout */ + at_quick_exit(hQ); + quick_exit(5); + return 1; /* unreachable */ +} + +/* Scenario 3: _exit runs NO handlers at all. */ +static int +raw_exit_scenario(void) +{ + atexit(hA); + at_quick_exit(hQ); + _exit(7); + return 1; /* unreachable */ +} + +/* Scenario 4: argc/argv/environ plumbing from the kernel stack. */ +static int +env_scenario(int argc, char **argv) +{ + if (argc != 3) + { + say(2, "FAIL: argc != 3 (harness must pass -e plus one argument)\n"); + return 1; + } + say(1, "argc="); + say_dec(1, (unsigned long)argc); + say(1, "\nargv0="); + say(1, argv[0]); + say(1, "\nargv1="); + say(1, argv[1]); + say(1, "\nargv2="); + say(1, argv[2]); + say(1, "\n"); + check(str_is(argv[1], "-e"), "argv[1] is the -e mode flag"); + check(str_is(argv[2], "extraarg"), "argv[2] == \"extraarg\""); + check(env_matches("VLIBC_TEST_VAR", "hello"), "environ contains VLIBC_TEST_VAR=hello"); + return failures == 0 ? 0 : 1; +} + +/* Scenario 5: main-thread TLS image + TCB errno slot. */ +static int +tls_scenario(void) +{ + check(tls_deref(&tls_preinit) == 42, "initialized TLS image copied (.tdata value 42)"); + check(tls_counter == 0, "TLS BSS tail zeroed (.tbss value 0)"); + tls_counter = 777; + check(tls_readback() == 777, "__thread round trip across a function call"); + errno = 123; + check(errno == 123, "errno round trip through the TCB slot"); + return failures == 0 ? 0 : 1; +} + +/* Scenario 6: fixed handler-list limits and the exiting guard. */ +static int +limit_scenario(void) +{ + for (int i = 0; i < 31; i++) + { + if (atexit(hNoop) != 0) + { + say(2, "FAIL: atexit slot accounting broke before the list was full\n"); + return 1; + } + } + if (atexit(hRegisterDuringExit) != 0) + { + say(2, "FAIL: 32nd atexit registration rejected (list should just fit)\n"); + return 1; + } + check(atexit(hA) == -1, "33rd atexit() returns -1 (fixed list full)"); + return failures == 0 ? 0 : 1; +} + +int +main(int argc, char **argv) +{ + if (argc >= 2) + { + if (argv[1][0] == '-' && argv[1][1] == 'q') + { + return quick_scenario(); + } + if (argv[1][0] == '-' && argv[1][1] == 'z') + { + return raw_exit_scenario(); + } + if (argv[1][0] == '-' && argv[1][1] == 'e') + { + return env_scenario(argc, argv); + } + if (argv[1][0] == '-' && argv[1][1] == 't') + { + return tls_scenario(); + } + if (argv[1][0] == '-' && argv[1][1] == 'x') + { + return limit_scenario(); + } + } + return lifo_scenario(); +}