-
-
Notifications
You must be signed in to change notification settings - Fork 5
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
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 |
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.
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.
CODE — Regenerate the initramfs and rebuild
host $ python3 tools/mkinitramfs.py kernel/fs/initramfs_data.h c
host $ make -C kernelNote
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.
CODE — Boot with a serial console
host $ ./run.ps1 -Mode serialnyx> 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
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.
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.
#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.
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.
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.
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.
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.
| 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.
- Includes
libc.h, not host headers - Compiled with
-mno-red-zone -ffreestanding -nostdlib - Linked at
-Ttext 0x10000withcrt0.oand--just-symbols=libc.so - Added to
USER_ELFSwith 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/getprocsare.bssor pre-memset -
ttymoderestored if raw mode was used - Builds with zero warnings
| 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 |
- Userspace — the ELF loader, crt0, shared libc, and every existing program
- Syscalls — the full interface
- Shell — how programs are invoked
- Debugging — tools and techniques
- System V AMD64 ABI — argument passing and stack layout
- ECMA-48 control functions — the CSI sequences the terminal implements
NyxOS v6.4.363 · GPL v2 · GitHub · uselessalter on Discord · nyxos@inbox.lv
NyxOS Wiki
Getting started
Kernel
Storage & network
Graphics & apps
Userspace
HOWTO
- HOWTO-Add-a-system-call
- HOWTO-Write-a-userspace-program
- HOWTO-Add-a-shell-command
- HOWTO-Add-a-GUI-application
Reference
- Syscall-Reference
- Command-Reference
- Hardware-Reference
- Format-Reference
- Kernel-Data-Structures
- Source-Tree-Reference
Project