Files
huntedbytheirsandSisyphus 5487bbc808 Add documentation
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]>
2026-08-30 04:09:49 -04:00

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 — 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())