diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..872385d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,68 @@ +# Contributing to nulsl-libc + +Thank you for helping Null Linux get a libc that fits in /dev/null. + +## Ground rules + +1. **C17, and only C17.** No GNU-isms in the language you write (inline + asm stays in the arch files where it belongs). The build forces + `-std=c17 -ffreestanding -fno-builtin`; make sure your code compiles + clean under `-Wall -Wextra -Wshadow -Wpointer-arith`. +2. **Stay lean.** Null Linux runs a GUI in 32 MB. If a change adds RAM + per process, it needs a better justification than convenience. +3. **AI drafts, humans ship.** AI-generated code is welcome as a draft, + but it must be reviewed, understood, and improved by a human before it + lands. If you cannot explain every line, do not open the PR. +4. **The kernel is the API.** Prefer a raw syscall wrapper over + inventing library machinery. `syscall()` is the only ABI surface — + keep it that way (see [docs/syscalls.md](docs/syscalls.md)). + +## Structure + +- `include/` — public headers. Declarations only; the `struct nulsl_file` + layout and friends stay private in `src/internal.h`. +- `src/` — implementations. One file per header/domain; keep files small + (a `#include` short of 250 lines is a good ceiling). +- `src/crt/crt0.S` — the entry point. Kept out of `libc.a` on purpose. +- `tests/` — anything you add must be exercised (`make check`). +- `benchmarks/` — anything performance-relevant needs a benchmark + (`make bench`); if it isn't memory-lean, it doesn't ship. + +## The stub convention + +Not implemented yet? It still needs its standard signature in the public +header, a documented error return, `errno = ENOSYS`, and a `/* TODO */` +comment naming what it needs. That is a feature, not a placeholder: every +stub fails loudly instead of silently misbehaving. + +## Adding a syscall wrapper + +1. Number in `include/sys/syscall.h` (guarded, ABI in comment). +2. Declaration in the right public header. +3. Thin wrapper in the right `src/` file — one `syscall()` call, nothing + else. If it needs to be a stub, follow the stub convention. +4. Update `docs/syscalls.md` (the table). +5. Extend a test in `tests/`. + +## Workflow + +```sh +./autogen.sh +make release && make check && make bench +make debug # for actual debugging (bin/debug/, -O0 -g) +``` + +- Style: `clang-format -i` on your diff (see `.clang-format`); keep diffs + formatted, small, and single-purpose. +- LSP: `make compile_commands` (needs `bear`) for clangd. +- Verify: `make check` must pass; run `make bench` before and after to + show you did not make anything slower. +- Commits: small, atomic, descriptive. This project is written by humans + and reviewed by humans; keep the history reviewable. +- PRs: one idea per PR, with tests. If the change is a stub → real + transition, say which roadmap item it completes. + +## Communication + +Open an issue or a PR on +. diff --git a/README.md b/README.md index 8d72168..4431228 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,91 @@ # nulsl-libc -A extremely lightweight and memory conservative LibC implementation. \ No newline at end of file +A very lightweight, from-scratch C17 libc for Linux, built for +[Null Linux](https://github.com/The-Null-Linux-Project) and its 32 MB RAM +target. + +**Status: skeleton. The structure is real, the code is honest stubs.** The +string core, the raw syscall layer, the entry point, and process teardown +work end to end; everything else declares its intent and returns `ENOSYS` +until it is implemented (see the [roadmap](docs/architecture.md#roadmap)). + +## Why another libc? + +Null Linux guideline #6: *"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."* + +- **Static-only, by design.** Everything links with `-nostdlib -static`. + No dynamic linker is loaded, relocated, or kept in memory for the + lifetime of a process — that memory belongs to the GUI. +- **The kernel is the API.** One variadic `syscall()` is the only piece + of code that touches the kernel ABI; `read`, `write`, `_exit`, and + friends are thin wrappers around it. +- **Freestanding C17.** `-ffreestanding -fno-builtin`: the compiler never + injects its own `memcpy`, so what you read is what runs. +- **No dependencies.** No libtool, no glibc, no kernel UAPI headers. + +## Requirements + +- Linux on x86_64 +- A C17 compiler (GCC ≥ 8 or Clang ≥ 6) +- autoconf ≥ 2.71, automake ≥ 1.16 +- `bear` (optional, for LSP `compile_commands.json`) + +## Building + +```sh +./autogen.sh # or just run make — it regenerates when needed +make release # -> bin/release/, CFLAGS='-O2' +make debug # -> bin/debug/, CFLAGS='-O0 -g' +make check # smoke test (fully static, no glibc, no ld.so) +make bench # benchmark the release build +``` + +Release is **always** `-O2`, per Null Linux guideline #4. `make bench` +and `make check` run against the release build. + +The repository root is never configured in-tree; `configure.ac` refuses it +so the committed driver `Makefile` (which dispatches into `bin/release` +and `bin/debug`) cannot be clobbered. `make distclean` removes both build +directories. + +## What works today + +| Area | Real | Stub (ENOSYS) | +|--------------|-----------------------------------------|-----------------------------| +| string | strlen, strcmp, strncmp, strcpy, | — | +| | strncpy, memcpy, memmove, memset, | | +| | memcmp | | +| syscall | syscall() (x86_64) | other architectures | +| unistd | read, write, close, getpid, _exit | unlink | +| stdio | puts, putchar, fflush (trivially) | printf, fopen, fclose, | +| | | fread, fwrite | +| stdlib | exit, abort, atoi | malloc, calloc, realloc, | +| | | free, strtol | +| crt | crt0.o `_start` (x86_64) | other architectures | + +Stubs follow one convention: standard signature, documented error return, +`errno = ENOSYS`, and a `/* TODO */` comment naming what they need. See +[docs/architecture.md](docs/architecture.md) for the design and roadmap, +[docs/syscalls.md](docs/syscalls.md) for the kernel interface. + +## Repository layout + +``` +Makefile driver: make release / make debug / make bench / ... +configure.ac autotools (C17 enforced, static-only) +autogen.sh autoreconf -i +include/ public headers — declarations only +src/ implementations; src/crt/crt0.S is the entry point +benchmarks/ make bench; must stay lean +tests/ make check; smoke test links -nostdlib -static +bin/release/ out-of-tree release build (-O2) +bin/debug/ out-of-tree debug build (-O0 -g) +docs/ architecture + syscall documentation +``` + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). Short version: C17, clang-format, +benchmarks for anything performance-relevant, and per project policy — +*AI drafts, humans ship.* diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..28f6734 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,119 @@ +# 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()`) diff --git a/docs/syscalls.md b/docs/syscalls.md new file mode 100644 index 0000000..fe5d259 --- /dev/null +++ b/docs/syscalls.md @@ -0,0 +1,74 @@ +# Syscalls + +This project's first principle (project guideline #6): the kernel is the +API. This document is the map of every syscall nulsl-libc touches, how +errors travel, and how to add the next one. + +## The one and only ABI surface + +`src/syscall.c` — the only file that executes the `syscall` instruction. + +``` +long syscall(long number, ...); +``` + +- Arguments 1-6 map to the platform's syscall argument registers. +- On success the kernel return value is returned as-is. +- On error the Linux kernel returns `-errno` (range `-1..-4095`). + `syscall()` translates it: `errno = -ret; return -1;` — the standard + libc convention. +- Numbers live in `include/sys/syscall.h` as `SYS_*` macros, + `#ifndef`-guarded so kernel UAPI headers can coexist. + +## The syscalls we use + +x86_64 Linux ABI: + +| Number | Name | Used by | Status | +|--------|-----------------|----------------------------------|----------| +| 0 | read | `read()` | wrapped | +| 1 | write | `write()`, `puts()`, `putchar()` | wrapped | +| 2 | open | — | constant | +| 3 | close | `close()` | wrapped | +| 39 | getpid | `getpid()`, bench_syscall | wrapped | +| 60 | exit | `_exit()`, `exit()`, `abort()` | wrapped | +| 87 | unlink | — (stub uses errno = ENOSYS) | constant | +| 228 | clock_gettime | benchmarks only | constant | + +"Wrapped" means there is a public function in `src/` that calls it. +"Constant" means the number is defined in the header but nothing wraps it +yet (benchmarks call it directly through `syscall()`). + +## Adding a new syscall wrapper + +1. Add the number to `include/sys/syscall.h` (guarded, with the ABI it + belongs to in a comment). +2. Add the public declaration to the right header (`include/unistd.h` for + POSIX stuff, `include/stdio.h` for stdio, ...). +3. Implement it in the matching `src/` file as a thin wrapper: + + ```c + ssize_t write(int fd, const void *buf, size_t count) + { + return (ssize_t)syscall(SYS_write, fd, buf, count); + } + ``` + + If the syscall is not implemented yet, follow the stub convention + instead (return the documented error value, `errno = ENOSYS`, + `/* TODO */` comment naming what it needs). +4. Update this table. +5. Add or extend a smoke test in `tests/` — if nothing exercises it, it + does not exist. + +## Porting to a new architecture + +- `src/syscall.c` — add the register mapping for the new ABI + (`#error` otherwise). +- `src/crt/crt0.S` — add `_start` for the new ABI. +- `include/sys/syscall.h` — the numbers are per-architecture; split the + table or move it to `src/arch/` once a second target exists. +- `configure.ac` — extend the `host_cpu` case. + +The project targets Linux on x86_64 today; everything above is written so +a second port is a contained, reviewable change.