feat(start): _start, __libc_start_main, exit/atexit/environ

This commit is contained in:
2026-09-03 19:01:51 -04:00
parent 2480c63fbd
commit 558cf8fd12
8 changed files with 983 additions and 0 deletions
+33
View File
@@ -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)
+110
View File
@@ -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);
}
+29
View File
@@ -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));
}
+67
View File
@@ -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 <stdlib.h> and _exit to <unistd.h>, environ is
* the POSIX process environment global, and __libc_start_main is the CRT
* ABI entry point that crt1.s calls. Once <stdlib.h>/<unistd.h> 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 */
+87
View File
@@ -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 */
+293
View File
@@ -0,0 +1,293 @@
#include <stdint.h>
#include <string.h>
#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);
}