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]>
120 lines
5.8 KiB
Markdown
120 lines
5.8 KiB
Markdown
# 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()`)
|