Compare commits

...
6 Commits
Author SHA1 Message Date
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
huntedbytheirsandSisyphus 370e64c3a6 Add smoke test and benchmarks
smoke links crt0.o + libc.a with -nostdlib -static and proves the whole chain runs without the dynamic linker; bench_strlen and bench_syscall measure the string core and raw syscall round-trip cost via make bench.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
2026-08-30 04:09:45 -04:00
huntedbytheirsandSisyphus b9f8d9bf19 Add libc implementation stubs
Real string core and process teardown; thin read/write/close/getpid/_exit syscall wrappers; the rest follows the stub convention (documented error return, errno = ENOSYS, TODO). syscall.c is the only kernel ABI surface; crt0.S is the static entry point, kept out of libc.a.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
2026-08-30 04:09:45 -04:00
huntedbytheirsandSisyphus aca40a69b0 Add public headers
Declarations for the string core, stdio, stdlib, unistd, errno, sys/types, and the raw syscall interface; the FILE layout stays private in src/internal.h.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
2026-08-30 04:09:45 -04:00
huntedbytheirsandSisyphus 228ed097f0 Add autotools build system (static-only, C17)
configure.ac enforces C17 and Linux/x86_64, refuses in-tree configure; Makefile.am builds libc.a plus crt0.o; the driver Makefile dispatches out-of-tree builds into bin/release (-O2) and bin/debug (-O0 -g).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
2026-08-30 04:09:45 -04:00
huntedbytheirsandSisyphus 6768ec36e7 Add editor tooling configuration
clangd fallback flags, clang-format style, clang-tidy checks, and gitignore for the autotools-generated artifacts.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <[email protected]>
2026-08-30 04:09:45 -04:00
31 changed files with 1496 additions and 24 deletions
+15
View File
@@ -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
View File
@@ -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
+19
View 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
View File
@@ -1,49 +1,50 @@
# ---> C # Generated by autotools
# Prerequisites aclocal.m4
*.d 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 *.o
*.a
*.lo
*.la
.deps/
.libs/
# Generic C artifacts (for experiments outside the autotools flow)
*.ko *.ko
*.obj *.obj
*.elf *.elf
# Linker output
*.ilk *.ilk
*.map *.map
*.exp *.exp
# Precompiled Headers
*.gch *.gch
*.pch *.pch
# Libraries
*.lib *.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll *.dll
*.so *.so
*.so.* *.so.*
*.dylib *.dylib
# Executables
*.exe *.exe
*.out *.out
*.app *.app
*.i*86
*.x86_64
*.hex *.hex
# Debug files
*.dSYM/ *.dSYM/
*.su *.su
*.idb *.idb
*.pdb *.pdb
# Kernel Module Compile Results
*.mod* *.mod*
*.cmd *.cmd
.tmp_versions/ .tmp_versions/
@@ -52,3 +53,16 @@ Module.symvers
Mkfile.old Mkfile.old
dkms.conf 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/
*~
+68
View File
@@ -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>.
+61
View File
@@ -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
View File
@@ -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
+89 -1
View File
@@ -1,3 +1,91 @@
# nulsl-libc # 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
View File
@@ -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)"
+44
View File
@@ -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 */
+28
View File
@@ -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;
}
+29
View File
@@ -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;
}
+50
View File
@@ -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
+119
View File
@@ -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()`)
+74
View File
@@ -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.
+59
View File
@@ -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 */
+46
View File
@@ -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 */
+38
View File
@@ -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 */
+32
View File
@@ -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 */
+51
View File
@@ -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 */
+25
View File
@@ -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 */
+38
View File
@@ -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 */
+40
View File
@@ -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
+9
View File
@@ -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;
+18
View File
@@ -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
View File
@@ -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;
}
+85
View File
@@ -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
View File
@@ -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;
}
+51
View File
@@ -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;
}
+47
View File
@@ -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;
}
+30
View File
@@ -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;
}