Skip to content

Userspace

kazah-png edited this page Aug 25, 2026 · 10 revisions

Userspace

Ring-3 ELF64 programs with a private address space each, preemptive scheduling, a shared libc mapped once into every process, runtime dynamic loading, and a suite of coreutils, TUI applications and network tools — including a port of DOOM.

See also: Syscalls, Process-Management, Memory-Management, Shell, Security

ELF loader (elf.c)

Validates the ELF64 header and program headers, then maps every PT_LOAD segment at its p_vaddr with W^X derived from the segment flags. Programs link at 0x10000.

elf_load_image() separates loading an image from creating a process, which is what lets execve() and spawn_user_path() share one code path.

The user stack is placed at USER_STACK_TOP (0x00007FFFFFFFE000) with 4 pages committed and a demand-grow ceiling of 128 pages — see Memory-Management.

Process isolation

  • A private PML4 per process, with no identity mapping of physical memory
  • NX on stack, heap and data pages
  • SMEP and SMAP enabled
  • The kernel's top PML4 entry mirrored in, so interrupts and syscalls always find their code mapped

Lifecycle

Step Detail
fork() Copy-on-write clone; child gets 0, parent gets the child's pid
execve(path, argv, envp) Replaces the image in place — same pid, same fds, same cwd, new address space
exit(code) Zombie + wake the parent; reaped by waitpid() or by the background reaper for orphans
Signals kill/signal/sigreturn with SIG_DFL, SIG_IGN or a user handler

Full detail in Process-Management.

crt0 (crt0.asm)

The assembly entry point. It reads the SysV entry-stack layout:

[rsp]      = argc
[rsp+8]    = argv[0]
   …         argv[argc-1], NULL
             envp[0] … NULL

then sets environ from envp, calls main(argc, argv), and exit()s with its return value. It also carries the __sigreturn trampoline that a signal handler returns through.

Shared libc

libc is built once as a prelinked shared object (libc.so) and mapped into every process, rather than bundling roughly 14 KB of it statically into each of the 62 programs. Programs link against it with:

ld … crt0.o --just-symbols=libc.so prog.o

--just-symbols resolves the symbols without copying any code, so a typical coreutil binary is around 10 KB.

The master libc frames are pinned un-freeable. They are shared by every process, so a single process exiting must not release them — that was a real memory-corruption bug.

API (user/libc.h)

Group Functions
Memory malloc free realloc calloc memset memcpy memmove memcmp memchr
Strings strlen strcpy strncpy strcmp strncmp strcat strncat strchr strrchr strstr strdup
ctype isspace isdigit isalpha isalnum toupper tolower
Output putchar puts printf sprintf snprintf
stdio (FILE*) fopen fclose fread fwrite fgetc fputc fgets fputs fseek ftell fflush feof ferror, and stdin/stdout/stderr
fprintf family fprintf vfprintf … on one unified printf formatter
Conversion atoi abs qsort strtod
Environment environ, getenv
Non-local jump setjmp/longjmp, sigsetjmp/siglongjmp

printf parses - and 0 flags plus a field width (%-4d, %5u, %-16s). Everything in Syscalls is available as an inline wrapper from user/syscall.h.

Note

The realloc/calloc/memmove, ctype, FILE* stdio, fprintf family and strtod groups were added in the Phase 2 work (v6.1.0v6.4.6) to make the libc C-compiler-complete — enough for the in-OS tcc to compile real C, including its own source. The in-OS compiler even recompiles this libc from source. See Toolchain.

malloc sits on sbrk(), which is lazy — a large allocation costs only the pages actually written.

Dynamic loading

long h = dlopen("/libdemo.so");
int (*f)(int) = dlsym(h, "demo_add");

Libraries load at fixed addresses, so there is no relocation to perform. libdemo.so and dltest.elf are the worked example.

N and N++ — the native language (user/nyxrt.h, user/nyxrt.c)

What started as "Nyx C" — a Go/Zig-influenced typed subset of C transpiling to C over the freestanding nyxrt runtime — has matured into N, the native systems language of NyxOS, with a real bootstrap compiler (ncc), a versioned spec, and a .n example suite that compiles and runs inside the OS. N++ is its planned safety superset. The nyxrt runtime described here is what a compiled N program links against. See N-Language for the full story.

Runtime provides Detail
Types nyx_str, nyx_slice, nyx_result, fixed-width integers
Syscall ABI __nyx_syscall6 for every ring-3 syscall (r10/r8/r9 loaded explicitly, for tcc)
String interpolation __nyx_fmt_begin / _str / _i64 backing "{var}" syntax

On NyxOS, xbm install ncc builds the N compiler from source with the in-OS toolchain; ncc hello.n then transpiles to C, which cc compiles and links against nyxrt — the whole loop in-OS.

Programs

Coreutils

Program Purpose
cat Concatenate files, or stdin, to stdout
wc Line, word and byte counts
ls Directory listing via getdents()
cp Copy a file, streaming into a truncated destination
mv Move/rename
rm Unlink a file or empty directory
mkdir Create directories
touch Create empty files
grep Print lines containing a literal pattern
head First N lines (default 10)
tail Last N lines via a circular buffer (default 10)
sort Sort lines
find Recursive directory walk with an optional name filter
stat File size and type via stat(2)
echo Print argv
upper Uppercase filter, stdin → stdout
which Resolve a name through $PATH
clear Clear the terminal, including scrollback (ESC[3J)
env Print the environment
date Wall-clock time from the RTC
sleep Pause for N seconds
uname System information
whoami Current user from $USER
free Memory usage, read from /proc/meminfo
pmap Mapped regions, read from /proc/<pid>/maps

Interactive

Program Purpose
sh The userspace shell — see Shell
top Live process monitor; redraws every ~1.5 s using readkey with a timeout, q quits
edit Full-screen nano-style editor — insert/split/join, arrows, Home/End, PgUp/PgDn, Ctrl-O save, Ctrl-X exit, status bar
less / more Full-screen pager over a file or stdin

Process and system

Program Purpose
ps Process table snapshot via getprocs()
kill kill [-SIG] pid..., numeric or named signals

Network

Program Purpose
nc Netcat over TCP or UDP; full-duplex via poll()
wget HTTP/1.0 client with its own DNS-over-UDP resolver; follows redirects
sockdemo TCP client
srvdemo TCP server (bind/listen/accept)
udpdemo UDP over loopback
polldemo poll() across a pipe and a socket at once

Games

doom — the original 1993 shareware DOOM, built as a real ring-3 ELF (~448 KB, zero undefined symbols) on the doomgeneric engine. Graphics go through fbpresent(), input through getkeyevent(). Requires /mnt/doom1.wad. See Desktop-Applications.

Tests and demos

These exist to prove specific kernel behaviour, and are the best short read if you want to understand a subsystem.

Program Proves
init The syscall regression suite — fork, execve, signals, pipe, mmap, mprotect, /dev, /proc
hello Minimal "Hello World" in pure assembly
hello_nyx The first Nyx C program
args execve argv passing
spin A spin loop, for scheduling tests
threads clone(CLONE_VM) + futex, and the shared thread-group heap
fault / segv Ring-3 faults become catchable signals rather than a kernel panic
sigrec Multi-fault recovery with sigsetjmp/siglongjmp
alarmdemo alarm() and SIGALRM
stacktest Demand-grown stacks and the guard page
munmaptest / mprotecttest Partial munmap/mprotect split the VMA
vmtest The user/kernel address boundary actually holds
dltest dlopen/dlsym against libdemo.so
fdleak Fds are force-closed when a process exits without closing them
fsx dup, rename, and the per-process cwd
vfsfill The per-directory child cap
bigread / pstorm / netstorm Pipeline and concurrent-socket stress reproducers
fbtest / keyevtest The fullscreen framebuffer and raw key-event paths

Writing your own program

#include "libc.h"

int main(int argc, char** argv) {
    printf("hello from pid %ld\n", getpid());
    return 0;
}

Add it to USER_ELFS in kernel/Makefile, add a rule following the pattern of the others, and regenerate the initramfs. See Building.

See also

External resources

Clone this wiki locally