README introduces the project and build workflow; CONTRIBUTING covers the stub convention and syscall wrapper process; docs/ explains the architecture and the kernel ABI surface. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]>
5.8 KiB
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— variadicsyscall(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 insrc/string.c).src/crt/crt0.S—_start, the process entry point. Sets up a valid frame, callsmain(argc, argv), hands the return value toexit(). Kept out oflibc.aon 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:
- be declared in the public header with its standard signature;
- return its documented error value (
-1,NULL,EOF,0...); - set
errno = ENOSYS; - 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=c17is forced everywhere; configure refuses non-C17 compilers. - Freestanding. Everything is compiled with
-ffreestanding -fno-builtin, so the compiler never injects its ownmemcpy/strlenand 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 fduntil a buffering layer exists (src/internal.howns 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())