# Architecture nulsl-libc is a from-scratch, C17, static-only libc for Linux, built for Null Linux's 32 MB target. This document explains how the pieces fit together. The short version: **the kernel is the API, everything else is a wrapper, and nothing is allowed to make the process bigger than it needs to be.** ## Why static-only A dynamically linked process carries the dynamic linker (`ld.so`) and its relocation machinery in memory for its entire lifetime. On a 32 MB budget that is pure overhead. nulsl-libc therefore builds only `libc.a`, links every program with `-nostdlib -static`, and ships its own `crt0.o` as the process entry point. There is no `PT_INTERP` in anything we build, and there is nothing to load. This is also the project guideline: *"If it's small, if it's lean, you have ZERO reason to link with libC at ALL. Linux specific syscalls can do you well."* — so the libc itself goes straight to the kernel. ## The syscall layer ``` ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ stdio.c │ │ stdlib.c │ │ unistd.c │ │ puts, putchar│ │ exit, atoi │ │ read, write │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ └──────────────────┼──────────────────┘ ▼ ┌─────────────────┐ │ syscall.c │ <-- the ONLY file that executes │ raw syscall() │ the `syscall` instruction └────────┬────────┘ ▼ ┌────────────┐ │ Linux │ │ kernel │ └────────────┘ ``` - `src/syscall.c` — variadic `syscall(long number, ...)`, the only architecture-specific C code. On error the kernel returns `-errno`; syscall() translates that to the libc convention (`-1` + `errno`). - `src/unistd.c`, `src/stdio.c`, `src/stdlib.c` — wrappers and the hand-rolled pieces (string core in `src/string.c`). - `src/crt/crt0.S` — `_start`, the process entry point. Sets up a valid frame, calls `main(argc, argv)`, hands the return value to `exit()`. Kept out of `libc.a` on purpose: archive members are only extracted when referenced, and nothing references `_start`. ## Modules | Module | Real today | Stubbed (roadmap) | |-------------------|-----------------------------------------------|----------------------------| | `src/string.c` | strlen, strcmp, strncmp, strcpy, strncpy, | — | | | memcpy, memmove, memset, memcmp | | | `src/unistd.c` | read, write, close, getpid, _exit | unlink | | `src/stdio.c` | puts, putchar, fflush (trivially), FILE stubs | printf, fopen/fread/... | | `src/stdlib.c` | exit, abort, atoi | malloc/calloc/realloc, | | | | free, strtol | | `src/syscall.c` | raw syscall() (x86_64) | other architectures | | `src/crt/crt0.S` | _start (x86_64) | other architectures | ## Stub convention A function that is not implemented yet must: 1. be declared in the public header with its standard signature; 2. return its documented error value (`-1`, `NULL`, `EOF`, `0`...); 3. set `errno = ENOSYS`; 4. carry a `/* TODO: ... */` comment naming what it needs. This keeps every stub link-clean and its failure mode explicit — programs fail loudly with a clear errno instead of silently misbehaving. ## Conventions - **C17 only.** `-std=c17` is forced everywhere; configure refuses non-C17 compilers. - **Freestanding.** Everything is compiled with `-ffreestanding -fno-builtin`, so the compiler never injects its own `memcpy`/`strlen` and the code you read is the code that runs. - **errno** is a plain global for now (single-threaded). If threads ever land, it becomes a TLS slot behind the same header. - **FILE** is a struct with one `int fd` until a buffering layer exists (`src/internal.h` owns the definition; the public header only forward- declares it). - **No dependencies.** No libtool, no glibc, no kernel UAPI headers — the few syscall numbers we need live in `include/sys/syscall.h`. ## Build layout ``` Makefile driver (committed): make release / make debug / ... configure.ac autotools source (C17 enforced, static-only) autogen.sh autoreconf -i bin/release/ out-of-tree build, CFLAGS='-O2' bin/debug/ out-of-tree build, CFLAGS='-O0 -g' include/ public headers (nothing but declarations) src/ implementations (headers live elsewhere) src/crt/crt0.S process entry point (separate object) benchmarks/ make bench — must stay lean tests/ make check — smoke test links -nostdlib -static ``` The repository root is never configured in-tree: `configure.ac` refuses it so the driver `Makefile` cannot be clobbered. Release is always `-O2`. ## Roadmap - brk()-based allocator (malloc/calloc/realloc/free) - printf engine - open()/close()/read()/write() file I/O and a small buffering layer - environ, getenv - strtol with full base/errno semantics - more architectures under `src/arch/` - signals (only then: a real `abort()`)