Skip to content

HOWTO Write a userspace program

kazah-png edited this page Jul 27, 2026 · 2 revisions

HOWTO Write a userspace program

How to write, build, install and run a ring-3 program on NyxOS. The worked example is a small coreutil; the later sections cover pipelines, signals, sockets, threads and full-screen TUIs.

See also: Userspace, Syscalls, Shell, Building, HOWTO-Add-a-system-call

The environment

NyxOS userspace is freestanding. There is no standard C library, no stdio.h, no stdlib.h.

Header Provides
user/syscall.h Every syscall as a static inline wrapper, plus the shared structs and flag constants
user/libc.h malloc, printf, string functions, getenv, setjmp — see Userspace

Include libc.h; it includes syscall.h for you.

Property Value
Load address 0x10000
Entry symbol _start (in crt0.asm)
Stack top 0x00007FFFFFFFE000, 16 KB committed, grows to 512 KB
Heap Lazy sbrk; pages appear on first write
Address space Private PML4, no identity mapping

Step 1 — Write the program

FILE — user/greet.c

#include "libc.h"

/* greet — print a greeting for each argument, or for $USER if given none. */
int main(int argc, char** argv) {
    if (argc < 2) {
        char* u = getenv("USER");
        printf("hello, %s\n", u ? u : "world");
        return 0;
    }
    for (int i = 1; i < argc; i++)
        printf("hello, %s\n", argv[i]);
    return 0;
}

main returns an exit status; crt0 passes it to exit(). argc/argv come from the SysV entry stack that execve builds — see Userspace.

Step 2 — Add it to the build

Two rules, following the pattern every coreutil uses.

FILE — kernel/Makefile

USER_ELFS = … $(USER_DIR)/greet.elf

$(USER_DIR)/greet.elf: $(USER_DIR)/crt0.o $(USER_DIR)/libc.so $(USER_DIR)/greet.o
	$(LD) -nostdlib -m elf_x86_64 -e _start -Ttext 0x10000 -o $@ \
	    $(USER_DIR)/crt0.o --just-symbols=$(USER_DIR)/libc.so $(USER_DIR)/greet.o

$(USER_DIR)/greet.o: $(USER_DIR)/greet.c $(USER_DIR)/libc.h $(USER_DIR)/syscall.h
	$(CC) -std=gnu99 -Os -ffreestanding -nostdlib -m64 -mno-red-zone \
	    -I$(USER_DIR) -c $< -o $@
Flag Why
-ffreestanding -nostdlib No host libc
-mno-red-zone Mandatory — interrupts would clobber the 128-byte red zone below RSP
-Ttext 0x10000 The load address the ELF loader expects
-e _start Entry point in crt0.o
--just-symbols=libc.so Resolve libc symbols without copying its code in

Important

Omitting -mno-red-zone produces a binary that works until an interrupt lands at the wrong moment, then corrupts silently. It is not optional.

Step 3 — Install and build

CODE — Regenerate the initramfs and rebuild

host $ python3 tools/mkinitramfs.py kernel/fs/initramfs_data.h c
host $ make -C kernel

Note

The initramfs is embedded in the kernel as a C array. A program that is built but not packed into the initramfs will not exist at boot.

Step 4 — Run

CODE — Boot with a serial console

host $ ./run.ps1 -Mode serial
nyx> greet
hello, nyx
nyx> greet world NyxOS
hello, world
hello, NyxOS
[exec] PID 7 exited (code 0)

The kernel shell auto-execs /greet.elf for a bare name that is not a builtin, forwarding argv. From the userspace shell it resolves through $PATH:

nyx> exec /sh.elf
sh$ greet | wc
      1       3      12

Writing pipeline-friendly tools

A tool that reads stdin and writes stdout composes with everything else. Read until EOF, never assume a single read returns everything:

FILE — pattern used by cat, wc, grep, upper

char buf[512];
long n;
while ((n = read(0, buf, sizeof buf)) > 0)
    write(1, buf, n);
Return Meaning
> 0 Bytes read
0 EOF — all writers closed their end
< 0 Error, or interrupted by a signal (-EINTR)

Write diagnostics to fd 2, never fd 1, or they end up in the next stage of the pipeline.

Buffers the kernel writes into

getdents() and getprocs() copy into your buffer. The kernel cannot fault in a lazy-sbrk heap page on your behalf, so every page must already be resident.

static nyx_dirent_t ents[64];        /* .bss — always resident */

int n = getdents(".", ents, 64);

Or, for a heap buffer, memset it first to force the pages in.

Warning

Passing a freshly malloc'd buffer to getdents without touching it gives partial or zero results, with no error returned.

Signals

#include "libc.h"

static volatile int hits = 0;
static void on_int(int sig) { (void)sig; hits++; }

int main(void) {
    signal(SIGINT, on_int);
    while (hits < 3) sleep_ms(100);
    printf("caught %d\n", hits);
    return 0;
}

CPU exceptions are catchable too — SIGSEGV, SIGFPE, SIGILL. With sigsetjmp/siglongjmp a program can survive repeated faults; user/sigrec.c is the worked example. See Process-Management.

Sockets

int fd = socket(AF_INET, SOCK_STREAM, 0);
if (connect(fd, inet_ipv4(10,0,2,2), 80) < 0) { /* … */ }
write(fd, "GET / HTTP/1.0\r\n\r\n", 18);
long n = read(fd, buf, sizeof buf);
close(fd);

Socket fds work with read, write, close and poll like any other fd. inet_ipv4() builds a network-order address; ports are host order. See Networking-Stack.

Threads

static volatile int lock = 0;

static void worker(void* arg) {
    /* … */
    exit(0);                 /* a thread must exit(); it never returns */
}

int main(void) {
    void* stack = malloc(64 * 1024);
    clone(worker, (char*)stack + 64 * 1024, 0, CLONE_VM);
    futex_wait(&lock, 0);
    return 0;
}

stack_top is the high end of the block, 16-byte aligned. Threads share the heap, the mmap table and the fd table. See Process-Management.

Full-screen TUIs

Two ingredients: raw input, and cursor addressing.

ttymode(TTY_RAW);                  /* no echo, byte at a time */
printf("\x1b[2J\x1b[H");           /* clear, home */
printf("\x1b[%d;%dH", row, col);   /* position */

long k = readkey(1500);            /* ms; 0 = block forever */
if (k == 0) { /* timeout — redraw */ }

ttymode(TTY_CANON);                /* restore before exiting */
Sequence Effect
ESC[2J Clear screen
ESC[H Cursor home
ESC[r;cH Position cursor
ESC[K Clear to end of line
ESC[…m SGR colour

top.c (timed refresh) and edit.c (blocking input) are the two reference implementations. The GUI terminal renders these live — see GUI-Subsystem.

Tip

execve resets tty mode, so a crashed child cannot leave the shell in raw mode. Restore it yourself anyway on the normal exit path.

Fullscreen graphics

For a game that owns the whole screen:

unsigned int info[3];
fbinfo(info);                             /* {width, height, bpp} */
fbpresent(framebuffer, w, h);             /* call once per frame */

int pressed, code;
while (getkeyevent(&pressed, &code)) { /* press AND release */ }

While the program keeps calling fbpresent() it owns the display and the compositor yields. This is the path DOOM uses.

Debugging

Technique How
Print tracing printf goes to the terminal; the kernel's own log goes to the serial file
Exit status The shell prints [exec] PID n exited (code c)
Faults A ring-3 fault becomes a signal, not a panic — the shell survives and reports it
Memory pmap <pid> shows your mappings; free shows system memory
Fd leaks The kernel logs [reap] force-closed leaked fd(s) at exit

See Debugging.

Checklist

  • Includes libc.h, not host headers
  • Compiled with -mno-red-zone -ffreestanding -nostdlib
  • Linked at -Ttext 0x10000 with crt0.o and --just-symbols=libc.so
  • Added to USER_ELFS with both a link rule and a compile rule
  • Initramfs regenerated
  • Reads stdin to EOF if it is a filter; diagnostics to fd 2
  • Buffers passed to getdents/getprocs are .bss or pre-memset
  • ttymode restored if raw mode was used
  • Builds with zero warnings

Troubleshooting

Symptom Cause Fix
command not found Not packed into the initramfs Rerun mkinitramfs.py, rebuild
Program starts, then faults immediately Missing -mno-red-zone, or linked without crt0.o Check the link rule
undefined reference at link time Function not in the shared libc Implement it in your own file, or add it to user/libc.c
getdents returns 0 or partial Buffer pages not resident Use a .bss array or memset first
Output vanishes in a pipeline Diagnostics written to fd 1 Write them to fd 2
Shell left with no echo after a crash Raw tty mode never restored ttymode(TTY_CANON); execve also resets it
malloc returns non-NULL but writing faults Not a bug — lazy sbrk faults pages in on write If the kernel must write there, touch it first
Works alone, corrupts under threads Shared state without a futex Guard it

See also

  • Userspace — the ELF loader, crt0, shared libc, and every existing program
  • Syscalls — the full interface
  • Shell — how programs are invoked
  • Debugging — tools and techniques

External resources

Clone this wiki locally