Compare commits
6
Commits
b3b065c325
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5487bbc808 | ||
|
|
370e64c3a6 | ||
|
|
b9f8d9bf19 | ||
|
|
aca40a69b0 | ||
|
|
228ed097f0 | ||
|
|
6768ec36e7 |
@@ -0,0 +1,15 @@
|
||||
# .clang-format — nulsl-libc style.
|
||||
#
|
||||
# Rule of thumb: 4-space indent, 100 columns, allman braces off, keep it
|
||||
# boring. Run `clang-format -i` on your diff before opening a PR.
|
||||
|
||||
BasedOnStyle: LLVM
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 100
|
||||
PointerAlignment: Left
|
||||
DerivePointerAlignment: false
|
||||
SortIncludes: true
|
||||
BreakBeforeBraces: Attach
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# .clang-tidy — nulsl-libc lint configuration.
|
||||
#
|
||||
# The linter's job is to yell about bugs, not style (style belongs to
|
||||
# clang-format). Keep the check list tight and useful.
|
||||
|
||||
# A libc owns the reserved namespace by definition (_exit, environ, ...);
|
||||
# bugprone-reserved-identifier only makes sense for application code.
|
||||
Checks: >
|
||||
clang-analyzer-*,
|
||||
bugprone-*,
|
||||
performance-*,
|
||||
portability-*,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-bugprone-implicit-widening-of-multiplication-result,
|
||||
-bugprone-reserved-identifier
|
||||
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: 'include/.*'
|
||||
FormatStyle: file
|
||||
@@ -0,0 +1,19 @@
|
||||
# .clangd — LSP configuration for nulsl-libc.
|
||||
#
|
||||
# Fallback flags used when compile_commands.json is absent. After running
|
||||
# `make compile_commands` (requires bear), clangd uses the exact build
|
||||
# flags from bin/release/ instead, and these are merged on top.
|
||||
CompileFlags:
|
||||
Compiler: clang
|
||||
Add:
|
||||
- -std=c17
|
||||
- -Iinclude
|
||||
- -ffreestanding
|
||||
- -fno-builtin
|
||||
- -Wall
|
||||
- -Wextra
|
||||
|
||||
# Treat all headers as project headers so warnings apply inside them.
|
||||
Diagnostics:
|
||||
Suppress:
|
||||
- unknown-pragmas
|
||||
+37
-23
@@ -1,49 +1,50 @@
|
||||
# ---> C
|
||||
# Prerequisites
|
||||
*.d
|
||||
# Generated by autotools
|
||||
aclocal.m4
|
||||
autom4te.cache/
|
||||
build-aux/
|
||||
config.guess
|
||||
config.h.in
|
||||
config.log
|
||||
config.status
|
||||
config.sub
|
||||
configure
|
||||
configure.lineno
|
||||
depcomp
|
||||
install-sh
|
||||
ltmain.sh
|
||||
missing
|
||||
Makefile.in
|
||||
|
||||
# Object files
|
||||
# Build artifacts
|
||||
*.o
|
||||
*.a
|
||||
*.lo
|
||||
*.la
|
||||
.deps/
|
||||
.libs/
|
||||
|
||||
# Generic C artifacts (for experiments outside the autotools flow)
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Linker output
|
||||
*.ilk
|
||||
*.map
|
||||
*.exp
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
*.su
|
||||
*.idb
|
||||
*.pdb
|
||||
|
||||
# Kernel Module Compile Results
|
||||
*.mod*
|
||||
*.cmd
|
||||
.tmp_versions/
|
||||
@@ -52,3 +53,16 @@ Module.symvers
|
||||
Mkfile.old
|
||||
dkms.conf
|
||||
|
||||
# Out-of-tree build dirs (the driver Makefile owns these)
|
||||
bin/release/*
|
||||
!bin/release/.gitkeep
|
||||
bin/debug/*
|
||||
!bin/debug/.gitkeep
|
||||
|
||||
# LSP (generated by `make compile_commands`)
|
||||
compile_commands.json
|
||||
|
||||
# Editor/agent caches
|
||||
.cache/
|
||||
.omo/
|
||||
*~
|
||||
|
||||
@@ -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>.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Driver Makefile — nulsl-libc
|
||||
#
|
||||
# This is the HAND-WRITTEN top-level makefile (committed to git). It never
|
||||
# runs the autotools machinery in-tree; instead it drives two out-of-tree
|
||||
# builds, one per bin/ directory, exactly as the Null Linux guidelines want:
|
||||
#
|
||||
# make release -> ./configure CFLAGS='-O2' into bin/release/
|
||||
# make debug -> ./configure CFLAGS='-O0 -g' into bin/debug/
|
||||
#
|
||||
# Run `./autogen.sh` once after cloning (or just run `make release` — it
|
||||
# regenerates the configure script itself when needed).
|
||||
#
|
||||
# Never run `./configure` at the repository root: configure.ac refuses it.
|
||||
|
||||
.PHONY: all release debug bench check clean distclean compile_commands
|
||||
|
||||
all: release
|
||||
|
||||
# --- bootstrapping ---------------------------------------------------------
|
||||
|
||||
# Regenerates configure (and friends) when autogen.sh is newer.
|
||||
configure: autogen.sh
|
||||
./autogen.sh
|
||||
|
||||
# --- builds ----------------------------------------------------------------
|
||||
|
||||
# Release is ALWAYS -O2 (project guideline #4).
|
||||
bin/release/Makefile: configure
|
||||
@mkdir -p bin/release
|
||||
@cd bin/release && ../../configure CFLAGS='-O2'
|
||||
|
||||
bin/debug/Makefile: configure
|
||||
@mkdir -p bin/debug
|
||||
@cd bin/debug && ../../configure CFLAGS='-O0 -g'
|
||||
|
||||
release: bin/release/Makefile
|
||||
$(MAKE) -C bin/release
|
||||
|
||||
debug: bin/debug/Makefile
|
||||
$(MAKE) -C bin/debug
|
||||
|
||||
# --- developer conveniences -------------------------------------------------
|
||||
|
||||
bench: release
|
||||
$(MAKE) -C bin/release bench
|
||||
|
||||
check: release
|
||||
$(MAKE) -C bin/release check
|
||||
|
||||
# LSP support: compile_commands.json for clangd (requires `bear`).
|
||||
compile_commands: release
|
||||
@command -v bear >/dev/null 2>&1 || { \
|
||||
echo "error: 'bear' is not installed (sudo pacman -S bear, apt install bear, ...)"; exit 1; }
|
||||
@cd bin/release && bear --output ../../compile_commands.json -- $(MAKE) clean all
|
||||
|
||||
clean:
|
||||
@test ! -f bin/release/Makefile || $(MAKE) -C bin/release clean
|
||||
@test ! -f bin/debug/Makefile || $(MAKE) -C bin/debug clean
|
||||
|
||||
distclean: clean
|
||||
rm -rf bin/release bin/debug
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
# Makefile.am — nulsl-libc
|
||||
#
|
||||
# This is the AUTOTOOLS makefile. It is only ever instantiated by a configure
|
||||
# run inside bin/release/ or bin/debug/ (never at the repository root).
|
||||
#
|
||||
# Everything is a static library. No libtool, no shared objects, no dynamic
|
||||
# linker. See docs/architecture.md.
|
||||
#
|
||||
# Benchmarks and tests live in one Makefile on purpose: automake orders
|
||||
# targets by dependency, so libc.a (and crt0.o) are guaranteed to exist
|
||||
# before any -nostdlib binary links against them. With SUBDIRS, recursion
|
||||
# builds the subdirectories first and linking breaks.
|
||||
|
||||
AUTOMAKE_OPTIONS = foreign
|
||||
|
||||
AM_CPPFLAGS = -I$(top_srcdir)/include
|
||||
AM_CFLAGS = -std=c17 -ffreestanding -fno-builtin -fno-stack-protector \
|
||||
-Wall -Wextra -Wshadow -Wpointer-arith
|
||||
|
||||
lib_LIBRARIES = libc.a
|
||||
|
||||
libc_a_SOURCES = \
|
||||
src/errno.c \
|
||||
src/stdio.c \
|
||||
src/stdlib.c \
|
||||
src/string.c \
|
||||
src/syscall.c \
|
||||
src/unistd.c
|
||||
|
||||
# The C runtime entry point is deliberately NOT a member of libc.a: nothing
|
||||
# references _start, so the linker would never extract it from an archive.
|
||||
# It is built as a standalone object and linked explicitly by everything
|
||||
# that wants a runnable binary.
|
||||
noinst_DATA = crt0.o
|
||||
crt0.o: $(top_srcdir)/src/crt/crt0.S
|
||||
$(CC) $(AM_CPPFLAGS) $(CFLAGS) -c -o $@ $<
|
||||
CLEANFILES = crt0.o
|
||||
|
||||
EXTRA_DIST = autogen.sh
|
||||
|
||||
# --- benchmarks ------------------------------------------------------------
|
||||
# Fully static, -nostdlib: the numbers reflect exactly what ships.
|
||||
|
||||
noinst_PROGRAMS = benchmarks/bench_strlen benchmarks/bench_syscall
|
||||
|
||||
benchmarks_bench_strlen_SOURCES = benchmarks/bench_strlen.c
|
||||
benchmarks_bench_strlen_LDADD = crt0.o libc.a -lgcc
|
||||
benchmarks_bench_strlen_LDFLAGS = -nostdlib -static -no-pie -Wno-unused-command-line-argument
|
||||
|
||||
benchmarks_bench_syscall_SOURCES = benchmarks/bench_syscall.c
|
||||
benchmarks_bench_syscall_LDADD = crt0.o libc.a -lgcc
|
||||
benchmarks_bench_syscall_LDFLAGS = -nostdlib -static -no-pie -Wno-unused-command-line-argument
|
||||
|
||||
.PHONY: bench
|
||||
bench: all
|
||||
./benchmarks/bench_strlen
|
||||
./benchmarks/bench_syscall
|
||||
|
||||
# --- tests ------------------------------------------------------------------
|
||||
|
||||
check_PROGRAMS = tests/smoke
|
||||
TESTS = tests/smoke
|
||||
|
||||
tests_smoke_SOURCES = tests/smoke.c
|
||||
tests_smoke_LDADD = crt0.o libc.a -lgcc
|
||||
tests_smoke_LDFLAGS = -nostdlib -static -no-pie -Wno-unused-command-line-argument
|
||||
@@ -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.*
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
# autogen.sh — regenerate all autotools machinery.
|
||||
#
|
||||
# Usage: ./autogen.sh (then: make release / make debug / make bench)
|
||||
#
|
||||
# Never run ./configure at the repository root — use the driver Makefile
|
||||
# targets instead, which configure out-of-tree into bin/{release,debug}.
|
||||
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
autoreconf -i -f -v
|
||||
|
||||
echo
|
||||
echo "autotools machinery regenerated."
|
||||
echo "next: make release (bin/release/, CFLAGS='-O2')"
|
||||
echo " make debug (bin/debug/, CFLAGS='-O0 -g')"
|
||||
echo " make bench (benchmarks against the release build)"
|
||||
echo " make check (smoke tests)"
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* bench.h — tiny shared helpers for nulsl-libc benchmarks.
|
||||
*
|
||||
* Deliberately dependency-free beyond the libc itself: timing goes
|
||||
* through a raw clock_gettime syscall, and numbers are printed with
|
||||
* putchar/write because printf() is still a stub. If you write a new
|
||||
* benchmark, include this and stay this lean.
|
||||
*/
|
||||
|
||||
#ifndef _NULSL_BENCH_H
|
||||
#define _NULSL_BENCH_H
|
||||
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define BENCH_CLOCK_MONOTONIC 1
|
||||
|
||||
/* Monotonic time in nanoseconds, via raw clock_gettime(2). */
|
||||
static inline long long bench_now_ns(void)
|
||||
{
|
||||
struct {
|
||||
long tv_sec;
|
||||
long tv_nsec;
|
||||
} t;
|
||||
|
||||
syscall(SYS_clock_gettime, BENCH_CLOCK_MONOTONIC, &t);
|
||||
return t.tv_sec * 1000000000LL + t.tv_nsec;
|
||||
}
|
||||
|
||||
/* Print an unsigned integer plus a newline, without printf(). */
|
||||
static inline void bench_print_u64(unsigned long long v)
|
||||
{
|
||||
char buf[24];
|
||||
int i = sizeof buf;
|
||||
|
||||
buf[--i] = '\n';
|
||||
do {
|
||||
buf[--i] = (char)('0' + v % 10);
|
||||
v /= 10;
|
||||
} while (v);
|
||||
write(STDOUT_FILENO, buf + i, sizeof buf - i);
|
||||
}
|
||||
|
||||
#endif /* _NULSL_BENCH_H */
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* bench_strlen.c — how fast is our string core?
|
||||
*
|
||||
* Measures strlen() over a typical short string. 10M iterations keeps
|
||||
* the loop overhead negligible; results are printed as ns per call.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "bench.h"
|
||||
|
||||
#define ITERS 10000000UL
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const char s[] = "the quick brown fox jumps over the lazy dog";
|
||||
volatile size_t sink = 0;
|
||||
long long t0, t1;
|
||||
|
||||
t0 = bench_now_ns();
|
||||
for (unsigned long i = 0; i < ITERS; i++)
|
||||
sink += strlen(s);
|
||||
t1 = bench_now_ns();
|
||||
|
||||
(void)sink; /* keep the loop observable */
|
||||
bench_print_u64((unsigned long long)((t1 - t0) / ITERS));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* bench_syscall.c — raw syscall round-trip cost.
|
||||
*
|
||||
* This is the number that justifies the whole project: for a kernel-first
|
||||
* libc (project guideline #6), every wrapper is one `syscall` instruction
|
||||
* away from the kernel. getpid() is the cheapest syscall there is, so
|
||||
* this measures the floor. 1M iterations; results in ns per call.
|
||||
*/
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include "bench.h"
|
||||
|
||||
#define ITERS 1000000UL
|
||||
|
||||
int main(void)
|
||||
{
|
||||
volatile pid_t sink = 0;
|
||||
long long t0, t1;
|
||||
|
||||
t0 = bench_now_ns();
|
||||
for (unsigned long i = 0; i < ITERS; i++)
|
||||
sink += getpid();
|
||||
t1 = bench_now_ns();
|
||||
|
||||
(void)sink;
|
||||
bench_print_u64((unsigned long long)((t1 - t0) / ITERS));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
dnl configure.ac — nulsl-libc, a very lightweight C17 libc for Linux.
|
||||
dnl
|
||||
dnl Static-only on purpose: the dynamic linker is a memory tax we refuse to
|
||||
dnl pay (see docs/architecture.md). libc.a is a plain archive, so no libtool.
|
||||
dnl
|
||||
dnl IMPORTANT: never run ./configure at the repository root. The root holds a
|
||||
dnl hand-written driver Makefile that builds into bin/{release,debug}; the
|
||||
dnl guard below refuses to overwrite it.
|
||||
|
||||
AC_PREREQ([2.71])
|
||||
AC_INIT([nulsl-libc], [0.1.0], [], [nulsl-libc],
|
||||
[https://git.spectoria.dev/The-Null-Linux-Project/nulsl-libc])
|
||||
AC_CONFIG_SRCDIR([include/stdio.h])
|
||||
AC_CONFIG_AUX_DIR([build-aux])
|
||||
AC_CANONICAL_HOST
|
||||
|
||||
AM_INIT_AUTOMAKE([foreign subdir-objects])
|
||||
|
||||
dnl C17, and only C17 (project guideline #1). Modern autoconf ships no
|
||||
dnl AC_PROG_CC_C17 macro, so prove it with a compile test instead.
|
||||
AC_PROG_CC
|
||||
saved_CFLAGS="$CFLAGS"
|
||||
CFLAGS="$CFLAGS -std=c17"
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([#if __STDC_VERSION__ < 201710L
|
||||
#error "not C17"
|
||||
#endif
|
||||
], [])],
|
||||
[],
|
||||
[AC_MSG_ERROR([nulsl-libc requires a C17 compiler (GCC >= 8 or Clang >= 6); -std=c17 was rejected])])
|
||||
CFLAGS="$saved_CFLAGS"
|
||||
|
||||
dnl Target validation: Linux on x86_64, for now.
|
||||
AS_CASE([$host_os],
|
||||
[linux*], [],
|
||||
[AC_MSG_ERROR([nulsl-libc currently targets Linux only (configured for $host_os)])])
|
||||
AS_CASE([$host_cpu],
|
||||
[x86_64], [],
|
||||
[AC_MSG_ERROR([nulsl-libc currently targets x86_64 only (configured for $host_cpu); src/arch/ is where new ports go])])
|
||||
|
||||
dnl Plain archives need ar/ranlib; no libtool, no shared objects.
|
||||
AC_CHECK_TOOL([AR], [ar])
|
||||
AC_PROG_RANLIB
|
||||
|
||||
dnl Refuse in-tree configuration: it would clobber the committed driver
|
||||
dnl Makefile that dispatches into bin/{release,debug}.
|
||||
AS_IF([test "x$srcdir" = "x." && test -f Makefile],
|
||||
[AC_MSG_ERROR([in-tree builds are disabled — run 'make release' or 'make debug' from the repository root instead])])
|
||||
|
||||
AC_CONFIG_FILES([Makefile])
|
||||
AC_OUTPUT
|
||||
@@ -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.
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* errno.h — error codes for nulsl-libc.
|
||||
*
|
||||
* Single-threaded for now: errno is a plain global. If nulsl-libc ever
|
||||
* grows threads, this becomes a TLS slot or an __errno_location()
|
||||
* indirection without changing any caller.
|
||||
*/
|
||||
|
||||
#ifndef _NULSL_ERRNO_H
|
||||
#define _NULSL_ERRNO_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern int errno;
|
||||
|
||||
/* The classic 1-34, values locked by Linux's errno(3) man page. */
|
||||
#define EPERM 1 /* Operation not permitted */
|
||||
#define ENOENT 2 /* No such file or directory */
|
||||
#define ESRCH 3 /* No such process */
|
||||
#define EINTR 4 /* Interrupted system call */
|
||||
#define EIO 5 /* I/O error */
|
||||
#define ENXIO 6 /* No such device or address */
|
||||
#define E2BIG 7 /* Argument list too long */
|
||||
#define ENOEXEC 8 /* Exec format error */
|
||||
#define EBADF 9 /* Bad file descriptor */
|
||||
#define ECHILD 10 /* No child processes */
|
||||
#define EAGAIN 11 /* Try again */
|
||||
#define ENOMEM 12 /* Out of memory */
|
||||
#define EACCES 13 /* Permission denied */
|
||||
#define EFAULT 14 /* Bad address */
|
||||
#define ENOTBLK 15 /* Block device required */
|
||||
#define EBUSY 16 /* Device or resource busy */
|
||||
#define EEXIST 17 /* File exists */
|
||||
#define EXDEV 18 /* Cross-device link */
|
||||
#define ENODEV 19 /* No such device */
|
||||
#define ENOTDIR 20 /* Not a directory */
|
||||
#define EISDIR 21 /* Is a directory */
|
||||
#define EINVAL 22 /* Invalid argument */
|
||||
#define ENFILE 23 /* File table overflow */
|
||||
#define EMFILE 24 /* Too many open files */
|
||||
#define ENOTTY 25 /* Not a typewriter */
|
||||
#define ETXTBSY 26 /* Text file busy */
|
||||
#define EFBIG 27 /* File too large */
|
||||
#define ENOSPC 28 /* No space left on device */
|
||||
#define ESPIPE 29 /* Illegal seek */
|
||||
#define EROFS 30 /* Read-only file system */
|
||||
#define EMLINK 31 /* Too many links */
|
||||
#define EPIPE 32 /* Broken pipe */
|
||||
#define EDOM 33 /* Math argument out of domain */
|
||||
#define ERANGE 34 /* Math result not representable */
|
||||
#define ENOSYS 38 /* Function not implemented */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _NULSL_ERRNO_H */
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* stdio.h — standard I/O for nulsl-libc.
|
||||
*
|
||||
* Minimal for now: unbuffered writes to a file descriptor, plus ENOSYS
|
||||
* stubs for the buffered/formatting machinery that is still to come.
|
||||
* A FILE is just a file descriptor until we grow a real buffering layer.
|
||||
*/
|
||||
|
||||
#ifndef _NULSL_STDIO_H
|
||||
#define _NULSL_STDIO_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define EOF (-1)
|
||||
|
||||
#define SEEK_SET 0
|
||||
#define SEEK_CUR 1
|
||||
#define SEEK_END 2
|
||||
|
||||
typedef struct nulsl_file FILE;
|
||||
|
||||
extern FILE *stdin;
|
||||
extern FILE *stdout;
|
||||
extern FILE *stderr;
|
||||
|
||||
/* Real, unbuffered. */
|
||||
int puts(const char *s);
|
||||
int putchar(int c);
|
||||
|
||||
/* Stubs (see docs/architecture.md for the roadmap). */
|
||||
int printf(const char *fmt, ...);
|
||||
FILE *fopen(const char *path, const char *mode);
|
||||
int fclose(FILE *f);
|
||||
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *f);
|
||||
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *f);
|
||||
int fflush(FILE *f);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _NULSL_STDIO_H */
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* stdlib.h — general utilities for nulsl-libc.
|
||||
*
|
||||
* The memory functions are ENOSYS stubs until the brk-based allocator
|
||||
* lands; atoi and the exit paths are already real.
|
||||
*/
|
||||
|
||||
#ifndef _NULSL_STDLIB_H
|
||||
#define _NULSL_STDLIB_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define EXIT_SUCCESS 0
|
||||
#define EXIT_FAILURE 1
|
||||
|
||||
/* Stubs: brk()/mmap() allocator is on the roadmap. */
|
||||
void *malloc(size_t size);
|
||||
void *calloc(size_t nmemb, size_t size);
|
||||
void *realloc(void *ptr, size_t size);
|
||||
void free(void *ptr);
|
||||
|
||||
/* Real. */
|
||||
void exit(int status);
|
||||
void abort(void);
|
||||
int atoi(const char *s);
|
||||
|
||||
/* Stub. */
|
||||
long strtol(const char *s, char **endptr, int base);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _NULSL_STDLIB_H */
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* string.h — memory and string operations.
|
||||
*
|
||||
* All functions here are real, tiny, and freestanding: no libc, no
|
||||
* compiler builtins. They are the foundation everything else builds on.
|
||||
*/
|
||||
|
||||
#ifndef _NULSL_STRING_H
|
||||
#define _NULSL_STRING_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
size_t strlen(const char *s);
|
||||
int strcmp(const char *a, const char *b);
|
||||
int strncmp(const char *a, const char *b, size_t n);
|
||||
char *strcpy(char *dst, const char *src);
|
||||
char *strncpy(char *dst, const char *src, size_t n);
|
||||
|
||||
void *memcpy(void *restrict dst, const void *restrict src, size_t n);
|
||||
void *memmove(void *dst, const void *src, size_t n);
|
||||
void *memset(void *dst, int c, size_t n);
|
||||
int memcmp(const void *a, const void *b, size_t n);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _NULSL_STRING_H */
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* sys/syscall.h — the raw syscall interface.
|
||||
*
|
||||
* syscall() is the ONLY place in nulsl-libc that talks the kernel ABI.
|
||||
* Everything above it (read, write, exit, ...) is a wrapper.
|
||||
*
|
||||
* SYS_* constants below are the x86_64 numbers for the syscalls the libc
|
||||
* itself wraps or its tests use. They are #ifndef-guarded so kernel UAPI
|
||||
* headers can coexist. Extend the table as needed (docs/syscalls.md).
|
||||
*/
|
||||
|
||||
#ifndef _NULSL_SYS_SYSCALL_H
|
||||
#define _NULSL_SYS_SYSCALL_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
long syscall(long number, ...);
|
||||
|
||||
/* x86_64 syscall numbers (Linux ABI). */
|
||||
#ifndef SYS_read
|
||||
#define SYS_read 0
|
||||
#endif
|
||||
#ifndef SYS_write
|
||||
#define SYS_write 1
|
||||
#endif
|
||||
#ifndef SYS_open
|
||||
#define SYS_open 2
|
||||
#endif
|
||||
#ifndef SYS_close
|
||||
#define SYS_close 3
|
||||
#endif
|
||||
#ifndef SYS_getpid
|
||||
#define SYS_getpid 39
|
||||
#endif
|
||||
#ifndef SYS_exit
|
||||
#define SYS_exit 60
|
||||
#endif
|
||||
#ifndef SYS_unlink
|
||||
#define SYS_unlink 87
|
||||
#endif
|
||||
#ifndef SYS_clock_gettime
|
||||
#define SYS_clock_gettime 228
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _NULSL_SYS_SYSCALL_H */
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* sys/types.h — primitive system types for nulsl-libc.
|
||||
*/
|
||||
|
||||
#ifndef _NULSL_SYS_TYPES_H
|
||||
#define _NULSL_SYS_TYPES_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef long ssize_t;
|
||||
typedef long off_t;
|
||||
typedef long pid_t;
|
||||
typedef unsigned int uid_t;
|
||||
typedef unsigned int gid_t;
|
||||
typedef unsigned long mode_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _NULSL_SYS_TYPES_H */
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* unistd.h — POSIX system call wrappers for nulsl-libc.
|
||||
*
|
||||
* These are thin, real wrappers around the raw syscall() interface — see
|
||||
* docs/syscalls.md. The kernel is the API; everything else is sugar.
|
||||
*/
|
||||
|
||||
#ifndef _NULSL_UNISTD_H
|
||||
#define _NULSL_UNISTD_H
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define STDIN_FILENO 0
|
||||
#define STDOUT_FILENO 1
|
||||
#define STDERR_FILENO 2
|
||||
|
||||
/* Real syscall wrappers. */
|
||||
ssize_t read(int fd, void *buf, size_t count);
|
||||
ssize_t write(int fd, const void *buf, size_t count);
|
||||
int close(int fd);
|
||||
pid_t getpid(void);
|
||||
void _exit(int status) __attribute__((noreturn));
|
||||
|
||||
/* Stub. */
|
||||
int unlink(const char *path);
|
||||
|
||||
/* Raw syscall entry point (also available here for convenience). */
|
||||
long syscall(long number, ...);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _NULSL_UNISTD_H */
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* crt0.S — process entry point for statically linked nulsl-libc binaries.
|
||||
*
|
||||
* The kernel hands control to _start with the stack already set up:
|
||||
*
|
||||
* rsp -> argc
|
||||
* rsp + 8 -> argv[0], argv[1], ..., argv[argc-1], NULL
|
||||
* after NULL -> envp[0], ... (we compute it, the libc does not use
|
||||
* it yet, but a correct entry point is cheap)
|
||||
*
|
||||
* We set up a valid frame, call main(argc, argv), and hand the exit code
|
||||
* to exit(). No dynamic linker is involved anywhere: this object + libc.a
|
||||
* is the entire runtime (see docs/architecture.md).
|
||||
*
|
||||
* This file is assembled with `gcc -c`, so it passes through the C
|
||||
* preprocessor and can carry the same arch guards as the C sources.
|
||||
*/
|
||||
|
||||
#if defined(__x86_64__)
|
||||
|
||||
.section .text
|
||||
.globl _start
|
||||
.type _start, @function
|
||||
_start:
|
||||
xor %ebp, %ebp /* outermost frame: ebp = 0 */
|
||||
mov (%rsp), %edi /* argc */
|
||||
lea 8(%rsp), %rsi /* argv */
|
||||
lea 16(%rsp,%rdi,8), %rdx /* envp = &argv[argc + 1] */
|
||||
and $-16, %rsp /* ABI: 16-byte stack alignment */
|
||||
call main
|
||||
mov %eax, %edi /* exit(status) */
|
||||
call exit
|
||||
1: jmp 1b /* exit() is noreturn; never reached */
|
||||
.size _start, .-_start
|
||||
|
||||
.section .note.GNU-stack,"",@progbits
|
||||
|
||||
#else
|
||||
#error "nulsl-libc: no crt0 for this architecture yet — see src/arch/"
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* errno.c — the errno global.
|
||||
*
|
||||
* Single-threaded for now; see include/errno.h for the threading note.
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
int errno = 0;
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* internal.h — private declarations shared between nulsl-libc translation
|
||||
* units. NOT installed; src/ only.
|
||||
*/
|
||||
|
||||
#ifndef _NULSL_INTERNAL_H
|
||||
#define _NULSL_INTERNAL_H
|
||||
|
||||
#include <sys/syscall.h>
|
||||
|
||||
/*
|
||||
* The FILE type: a file descriptor until a real buffering layer lands.
|
||||
* Defined here (not in the public stdio.h) so the layout stays private.
|
||||
*/
|
||||
struct nulsl_file {
|
||||
int fd;
|
||||
};
|
||||
#endif /* _NULSL_INTERNAL_H */
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* stdio.c — standard I/O.
|
||||
*
|
||||
* What is real today: unbuffered character/string output to a descriptor.
|
||||
* What is stubbed: formatting (printf), and anything that would need a
|
||||
* buffer or the open() path. All stubs follow the same convention —
|
||||
* return the documented error value and set errno = ENOSYS.
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "internal.h"
|
||||
|
||||
FILE *stdin = &(struct nulsl_file){0};
|
||||
FILE *stdout = &(struct nulsl_file){1};
|
||||
FILE *stderr = &(struct nulsl_file){2};
|
||||
|
||||
int puts(const char *s)
|
||||
{
|
||||
if (write(STDOUT_FILENO, s, strlen(s)) < 0)
|
||||
return EOF;
|
||||
if (write(STDOUT_FILENO, "\n", 1) < 0)
|
||||
return EOF;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int putchar(int c)
|
||||
{
|
||||
unsigned char b = (unsigned char)c;
|
||||
return write(STDOUT_FILENO, &b, 1) == 1 ? c : EOF;
|
||||
}
|
||||
|
||||
/* Stub: formatting engine is on the roadmap. */
|
||||
int printf(const char *fmt, ...)
|
||||
{
|
||||
(void)fmt;
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Stub: needs open() and a buffering layer. */
|
||||
FILE *fopen(const char *path, const char *mode)
|
||||
{
|
||||
(void)path;
|
||||
(void)mode;
|
||||
errno = ENOSYS;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int fclose(FILE *f)
|
||||
{
|
||||
(void)f;
|
||||
errno = ENOSYS;
|
||||
return EOF;
|
||||
}
|
||||
|
||||
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *f)
|
||||
{
|
||||
(void)ptr;
|
||||
(void)size;
|
||||
(void)nmemb;
|
||||
(void)f;
|
||||
errno = ENOSYS;
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *f)
|
||||
{
|
||||
(void)ptr;
|
||||
(void)size;
|
||||
(void)nmemb;
|
||||
(void)f;
|
||||
errno = ENOSYS;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Trivially correct: nothing buffers yet, so there is nothing to flush. */
|
||||
int fflush(FILE *f)
|
||||
{
|
||||
(void)f;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* stdlib.c — general utilities.
|
||||
*
|
||||
* Real: process teardown (exit/_exit/abort) and atoi.
|
||||
* Stubs: the allocator (malloc & friends wait for a brk/mmap allocator)
|
||||
* and strtol (waits for proper errno/base handling).
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
void exit(int status)
|
||||
{
|
||||
/* TODO: run atexit() handlers and flush stdio once buffering exists. */
|
||||
_exit(status);
|
||||
for (;;)
|
||||
; /* unreachable */
|
||||
}
|
||||
|
||||
void abort(void)
|
||||
{
|
||||
/* TODO: raise SIGABRT once signals exist; 128+6 mirrors the shell
|
||||
* convention for "killed by signal 6" without any signal machinery. */
|
||||
_exit(134);
|
||||
for (;;)
|
||||
; /* unreachable */
|
||||
}
|
||||
|
||||
int atoi(const char *s)
|
||||
{
|
||||
int sign = 1;
|
||||
int v = 0;
|
||||
|
||||
while (*s == ' ' || (*s >= '\t' && *s <= '\r'))
|
||||
s++;
|
||||
if (*s == '-' || *s == '+') {
|
||||
if (*s == '-')
|
||||
sign = -1;
|
||||
s++;
|
||||
}
|
||||
while (*s >= '0' && *s <= '9')
|
||||
v = v * 10 + (*s++ - '0');
|
||||
return sign * v;
|
||||
}
|
||||
|
||||
/* Stub: brk()/mmap() allocator is on the roadmap. */
|
||||
void *malloc(size_t size)
|
||||
{
|
||||
(void)size;
|
||||
errno = ENOSYS;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *calloc(size_t nmemb, size_t size)
|
||||
{
|
||||
(void)nmemb;
|
||||
(void)size;
|
||||
errno = ENOSYS;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *realloc(void *ptr, size_t size)
|
||||
{
|
||||
(void)ptr;
|
||||
(void)size;
|
||||
errno = ENOSYS;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void free(void *ptr)
|
||||
{
|
||||
(void)ptr;
|
||||
/* Nothing to do until the allocator exists. */
|
||||
}
|
||||
|
||||
/* Stub: needs errno/base handling (ERANGE, EINVAL, 0x/0o prefixes). */
|
||||
long strtol(const char *s, char **endptr, int base)
|
||||
{
|
||||
(void)s;
|
||||
(void)endptr;
|
||||
(void)base;
|
||||
errno = ENOSYS;
|
||||
return 0;
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* string.c — real, minimal implementations of the memory/string core.
|
||||
*
|
||||
* Byte-at-a-time on purpose: for the sizes Null Linux deals with, and for
|
||||
* the goal of keeping the code obviously correct, word-at-a-time tricks
|
||||
* are not worth the branch soup. If profiling ever says otherwise, the
|
||||
* benchmark suite will say so (see benchmarks/).
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
size_t strlen(const char *s)
|
||||
{
|
||||
const char *p = s;
|
||||
while (*p)
|
||||
p++;
|
||||
return (size_t)(p - s);
|
||||
}
|
||||
|
||||
int strcmp(const char *a, const char *b)
|
||||
{
|
||||
while (*a && *a == *b) {
|
||||
a++;
|
||||
b++;
|
||||
}
|
||||
return (unsigned char)*a - (unsigned char)*b;
|
||||
}
|
||||
|
||||
int strncmp(const char *a, const char *b, size_t n)
|
||||
{
|
||||
for (; n && *a && *a == *b; n--, a++, b++)
|
||||
;
|
||||
if (n == 0)
|
||||
return 0;
|
||||
return (unsigned char)*a - (unsigned char)*b;
|
||||
}
|
||||
|
||||
char *strcpy(char *dst, const char *src)
|
||||
{
|
||||
char *d = dst;
|
||||
while ((*d++ = *src++))
|
||||
;
|
||||
return dst;
|
||||
}
|
||||
|
||||
char *strncpy(char *dst, const char *src, size_t n)
|
||||
{
|
||||
char *d = dst;
|
||||
while (n && *src) {
|
||||
*d++ = *src++;
|
||||
n--;
|
||||
}
|
||||
while (n--)
|
||||
*d++ = '\0';
|
||||
return dst;
|
||||
}
|
||||
|
||||
void *memcpy(void *restrict dst, const void *restrict src, size_t n)
|
||||
{
|
||||
unsigned char *d = dst;
|
||||
const unsigned char *s = src;
|
||||
while (n--)
|
||||
*d++ = *s++;
|
||||
return dst;
|
||||
}
|
||||
|
||||
void *memmove(void *dst, const void *src, size_t n)
|
||||
{
|
||||
unsigned char *d = dst;
|
||||
const unsigned char *s = src;
|
||||
|
||||
if ((size_t)(d - s) >= n) {
|
||||
/* No overlap (or exact): copy forward. */
|
||||
while (n--)
|
||||
*d++ = *s++;
|
||||
} else {
|
||||
/* Overlap: copy backward. */
|
||||
d += n;
|
||||
s += n;
|
||||
while (n--)
|
||||
*--d = *--s;
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
int memcmp(const void *a, const void *b, size_t n)
|
||||
{
|
||||
const unsigned char *x = a;
|
||||
const unsigned char *y = b;
|
||||
while (n--) {
|
||||
if (*x != *y)
|
||||
return *x - *y;
|
||||
x++;
|
||||
y++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void *memset(void *dst, int c, size_t n)
|
||||
{
|
||||
unsigned char *d = dst;
|
||||
while (n--)
|
||||
*d++ = (unsigned char)c;
|
||||
return dst;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* syscall.c — the raw syscall entry point.
|
||||
*
|
||||
* This is the ONLY file in nulsl-libc that executes the `syscall`
|
||||
* instruction. Every other function that reaches the kernel goes
|
||||
* through here (see docs/syscalls.md).
|
||||
*
|
||||
* ABI: on error the kernel returns -errno in rax. We follow the glibc
|
||||
* convention for syscall(): return -1 and set errno. (The -4095 bound is
|
||||
* the documented Linux range for negative errno values.)
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdarg.h>
|
||||
#include <sys/syscall.h>
|
||||
|
||||
long syscall(long number, ...)
|
||||
{
|
||||
va_list ap;
|
||||
long a1, a2, a3, a4, a5, a6;
|
||||
long ret;
|
||||
|
||||
va_start(ap, number);
|
||||
a1 = va_arg(ap, long);
|
||||
a2 = va_arg(ap, long);
|
||||
a3 = va_arg(ap, long);
|
||||
a4 = va_arg(ap, long);
|
||||
a5 = va_arg(ap, long);
|
||||
a6 = va_arg(ap, long);
|
||||
va_end(ap);
|
||||
|
||||
#if defined(__x86_64__)
|
||||
/* System V AMD64 ABI: number in rax, args in rdi rsi rdx r10 r8 r9. */
|
||||
register long r10 __asm__("r10") = a4;
|
||||
register long r8 __asm__("r8") = a5;
|
||||
register long r9 __asm__("r9") = a6;
|
||||
__asm__ volatile("syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(number), "D"(a1), "S"(a2), "d"(a3), "r"(r10),
|
||||
"r"(r8), "r"(r9)
|
||||
: "rcx", "r11", "memory");
|
||||
#else
|
||||
#error "nulsl-libc: no syscall ABI for this architecture yet — see src/arch/"
|
||||
#endif
|
||||
|
||||
if (ret < 0 && ret >= -4095) {
|
||||
errno = (int)-ret;
|
||||
return -1;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* unistd.c — thin wrappers around raw syscalls.
|
||||
*
|
||||
* Each function here is a one-liner on purpose: the kernel is the API
|
||||
* (project guideline #6), and a wrapper that does more than translate
|
||||
* arguments is a wrapper that can lie. Error translation (kernel -errno
|
||||
* -> errno) happens inside syscall() itself.
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
ssize_t read(int fd, void *buf, size_t count)
|
||||
{
|
||||
return (ssize_t)syscall(SYS_read, fd, buf, count);
|
||||
}
|
||||
|
||||
ssize_t write(int fd, const void *buf, size_t count)
|
||||
{
|
||||
return (ssize_t)syscall(SYS_write, fd, buf, count);
|
||||
}
|
||||
|
||||
int close(int fd)
|
||||
{
|
||||
return (int)syscall(SYS_close, fd);
|
||||
}
|
||||
|
||||
pid_t getpid(void)
|
||||
{
|
||||
return (pid_t)syscall(SYS_getpid);
|
||||
}
|
||||
|
||||
void _exit(int status)
|
||||
{
|
||||
syscall(SYS_exit, status);
|
||||
for (;;)
|
||||
; /* unreachable */
|
||||
}
|
||||
|
||||
/* Stub: trivially a one-liner once wanted (SYS_unlink = 87 on x86_64). */
|
||||
int unlink(const char *path)
|
||||
{
|
||||
(void)path;
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* smoke.c — end-to-end smoke test.
|
||||
*
|
||||
* Linked with crt0.o + libc.a, -nostdlib -static: if this binary runs at
|
||||
* all, the whole chain works — entry point, main(), our string code, and
|
||||
* raw syscalls — with no dynamic linker and no glibc in the process.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static const char msg[] = "nulsl-libc smoke test: ok\n";
|
||||
|
||||
if (strcmp("abc", "abc") != 0)
|
||||
return 1;
|
||||
if (strcmp("abc", "abd") == 0)
|
||||
return 2;
|
||||
if (strncmp("abcdef", "abcxyz", 3) != 0)
|
||||
return 3;
|
||||
if (strlen("nulsl") != 5)
|
||||
return 4;
|
||||
if (memcmp("abcd", "abce", 3) != 0)
|
||||
return 5;
|
||||
if (write(STDOUT_FILENO, msg, sizeof msg - 1) < 0)
|
||||
return 6;
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user