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]>
This commit is contained in:
@@ -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
|
||||
<https://git.spectoria.dev/The-Null-Linux-Project/nulsl-libc>.
|
||||
@@ -1,3 +1,91 @@
|
||||
# nulsl-libc
|
||||
|
||||
A extremely lightweight and memory conservative LibC implementation.
|
||||
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.*
|
||||
|
||||
@@ -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()`)
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user