Skip to content

Userspace

kazah-png edited this page Jul 26, 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 memset memcpy memcmp
Strings strlen strcpy strncpy strcmp strncmp strcat strchr strstr
Output putchar puts printf sprintf snprintf
Conversion atoi abs
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.

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.

Nyx C (user/nyxrt.h, user/nyxrt.c)

Nyx C is a Go/Zig-influenced typed subset of C that transpiles to C and links against the nyxrt runtime. The runtime is freestanding and makes no libc calls.

Provides Detail
Types nyx_str, nyx_slice, nyx_result, fixed-width integers
Syscall ABI __nyx_syscall6 for every ring-3 syscall
String interpolation __nyx_fmt_begin / _str / _i64 backing "{var}" syntax

user/hello.nyx is the first program; hello_nyx.c is its transpiler output, checked in so the build needs no transpiler.

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