Skip to content

Toolchain

kazah-png edited this page Aug 6, 2026 · 2 revisions

In-OS toolchain — libc, cc, and the tcc self-host

NyxOS carries its own C toolchain inside the running operating system. From the kernel shell you can compile a .c file to an ELF object, link it into a NyxOS executable, and run it — no host, no cross-compiler, no network. The compiler is a port of TinyCC (tcc) 0.9.27; the runtime is the NyxOS userland libc; the driver is the cc shell builtin.

This is the Phase 2 work (v6.1.0v6.4.9). Its headline result: the in-OS tcc compiles tcc's own ~40 000-line source, in-OS, to a valid x86-64 object (v6.4.9) — the operating system's own compiler compiling the compiler.

See also: Userspace, Shell, Command-Reference, Filesystem, Format-Reference, Version-History

Note

This is a hobby milestone, not a production toolchain. tcc is an unoptimising single-pass compiler; there is no make, no linker script control beyond what cc sets, and large objects still hit a filesystem write limit (see Known limitations). What it does do — compile, link, and run real C, including its own source — it does honestly and with zero kernel faults.

The three pieces

Piece Where Role
libc user/libc.c, user/libc.h The C runtime every program links against — the standard-library surface a C program expects
tcc user/tcc/, shipped as /tcc.elf The compiler+assembler+linker: C source → x86-64 ELF
cc cmd_cc in kernel/core/kernel.c The shell builtin that drives /tcc.elf with the NyxOS runtime defaults

The libc

user/libc.c is the freestanding C library that links into every ring-3 program (see Userspace). Phase 1 gave it the syscall wrappers and the basics; Phase 2 grew it into a compiler-complete C library — the surface tcc's own front-end and a typical C program lean on:

Release Added
v6.1.0 realloc, calloc, memmove — reallocation and overlap-safe copy
v6.1.1 The ctype family — isspace, isdigit, isalpha, isalnum, toupper, … (a compiler front-end's lexer primitives)
v6.1.2 memchr, strrchr, strncat, strdup, qsort
v6.1.3 The FILE* stdio core — fopen/fclose/fread/fwrite/fgetc/fputc/fflush/feof/ferror over the existing fd syscalls
v6.1.4 The fprintf family on a single unified printf formatter
v6.1.5 The standard streams stdin/stdout/stderr, plus fgets/fputs/fseek/ftell

The prebuilt (GCC-compiled) library ships in the initramfs as /libc.o and is the default link input for cc. When tcc compiles libc.c itself, the result carries 88 global FUNC symbols (memset, malloc, printf, snprintf, fopen, …), matching the host tcc's output byte-for-byte in symbol set.

The cc builtin

cc is a kernel-shell builtin (cmd_cc, kernel/core/kernel.c) that runs /tcc.elf in the foreground — it blocks until tcc exits, like exec, and prints the exit code.

cc [-c] [--self-libc] <in.c/.o ...> [-o <out>]
Mode Flag Behaviour
Link (default) (none) Compiles and links one or more sources/objects into a runnable NyxOS executable: a freestanding -nostdlib -static link over /crt0.o + /libc.o + /va_list.o. No -Ttext — tcc bases the image at its default 0x400000, which the ELF loader accepts (see the static-PLT fix)
Compile-only -c Compiles each source to a .o and does not link (crt0.o/libc.o omitted). Enables separate compilation: cc -c a.c -o a.o then cc a.o b.o -o prog
Self-libc --self-libc Before linking, rebuilds the whole C runtime from source with tcc — the OS libc (libc.c), tcc's va-runtime (va_list.c), and tcc's libtcc1.c — then links the program against those .slo/.vlo/.tlo objects instead of the prebuilt ones. Only crt0.o stays GCC-built
Self-compiler --self Compiles with the self-built tcc (/mnt/tcc_self.elf) rather than the shipped /tcc.elf — building and caching it on demand. cc --self rebuilds real coreutils (grep, sort) byte-identically, which is what closed the self-host arc (v6.4.17v6.4.18)

Every include path is wired for NyxOS sources: cc always passes -I/usr/lib/tcc/include (tcc's freestanding headers) and -I/usr/src/nyx (the OS's own libc.h/syscall.h), so a real NyxOS source that does #include "libc.h" compiles as-is. Any argument that is not -o <out> is forwarded to tcc verbatim, so -D/-I and extra inputs pass straight through.

Tip

Use absolute paths for a mount-backed working directory: cc /mnt/a.c /mnt/b.c -o /mnt/prog. With no -o, the output is the first input minus .c (plus .o in -c mode).

The tcc port

user/tcc/ is a vendored TinyCC 0.9.27 (~47 000 lines) configured for a single target: x86-64, ELF, no JIT. It is cross-compiled with host GCC into /tcc.elf, which is then a perfectly ordinary NyxOS ring-3 program — it reads source through the VFS, allocates with the NyxOS libc, and writes ELF through write().

The port is host-header-free, which is what makes the self-host possible. The user/tcc/nyxshim/ directory replaces everything a normal tcc build takes from the host system:

Shim Replaces Notes
config.h ./configure output TCC_TARGET_X86_64, TCC_TARGET_ELF, CONFIG_TCC_STATIC, CONFIG_TCCDIR "/usr/lib/tcc"
TCC_NO_NATIVE the -run/JIT path Undefines TCC_IS_NATIVE so tcc never pulls host <sys/mman.h>/<signal.h>/<sys/ucontext.h> — the in-OS tcc never JITs
nyxshim.c libc glue Provides the OS calls with POSIX signatures; __assert_fail prints to fd 2 and aborts
assert.h, ctype.h, errno.h, fcntl.h, stdio.h, stdlib.h, string.h, unistd.h, time.h, setjmp.h, inttypes.h, stdint.h, math.h, sys/… host system headers Freestanding shims, so tcc's own source compiles with no host include tree

The self-host arc

The path from "tcc runs" to "tcc compiles tcc" was built one verifiable increment at a time. Each row was checked live in QEMU with 0 kernel faults.

Release Milestone
v6.2.0 The in-OS C compiler runs
v6.2.1 tcc -E (preprocess) runs in-OS — unblocked by fixing a real strchr bug in the libc
v6.2.2 tcc -c compiles C to a valid x86-64 ELF object in-OS
v6.2.3 tcc links a static executable ELF in-OS
v6.3.0 compile → link → run closed — a program compiled in-OS by tcc now runs in-OS (the static-PLT fix)
v6.3.1 A real libc-using C program compiles and runs; the cc builtin lands
v6.3.2 Non-trivial and multi-file programs compile, link, and run
v6.3.3 cc -c compile-only mode — object-by-object separate compilation
v6.3.4 tcc's freestanding system headers provisioned into the OS (/usr/lib/tcc/include)
v6.3.5 tcc's varargs runtime (va_list.c) provisioned
v6.4.0 tcc compiles the NyxOS libc — the toolchain is self-hosting-capable
v6.4.1 The tcc-built libc actually runs (two real blocking bugs fixed)
v6.4.2 cc --self-libc — the in-OS tcc compiles a piece of tcc itself (its va-runtime)
v6.4.3 tcc compiles a real, unmodified NyxOS coreutil byte-identically to the GCC build
v6.4.4 tcc compiles the OS's own 1042-line userspace shell
v6.4.5 tcc compiles tcc's main runtime library libtcc1.c
v6.4.6 Floating-point works — strtod was a stub, so every float/double literal had compiled to zero; a real strtod fixes it
v6.4.7 tcc rebuilds grep + sort byte-identically
v6.4.8 The tcc port is host-header-freetcc.c itself now compiles in-OS
v6.4.9 TCC COMPILES TCC IN-OS — the in-OS tcc compiles tcc's whole ~40 000-line source to a valid x86-64 REL object, exit 0, 0 faults
v6.4.10 The self-host object now persists in full — the vfs_pwrite write cap is fixed, so the whole ~496 KB object reaches the disk
v6.4.11 The self-host loop is closed — in one boot the in-OS tcc compiles tcc.c, links the result, and runs it
v6.4.12 The self-built tcc_self.elf is itself a working compiler
v6.4.15 The self-built tcc compiles a real coreutil (grep) byte-identically to the GCC build
v6.4.17 cc --self — compile using the self-built tcc, cached at /mnt/tcc_self.elf
v6.4.18 cc --self rebuilds sort byte-identically — the tcc self-host arc is complete

The static-PLT fix

The first edit to tcc's own sources, and the fix that closed compile→link→run (v6.3.0). tcc's x86-64 backend routes a call to a global default-visibility function through a PLT, but for a -nostdlib -static link it builds the PLT and never fills the PLT's GOT slot — leaving the GOT all zeros, so every such call jumped through the GOT to a null address (fault at RIP 0x0), even a local add().

The fix in build_got_entries (user/tcc/tccelf.c) extends the existing rule that downgrades an R_X86_64_PLT32 call to a direct PC32 — already firing for local/hidden symbols — to also fire for a symbol already defined in a static executable, exactly what a real static linker does. The output then has no .plt/.got at all; every call is a direct PC32, and it runs. It was root-caused against a host build of the same vendored tcc (the empty GOT reproduces there too), proving it a tcc-logic bug rather than an in-OS one.

Making the libc tcc-compilable

v6.4.0 needed three source tweaks, each guarded so the GCC build stays byte-for-byte identical:

  • user/libc.hva_list was typedef __builtin_va_list (a GCC builtin tcc lacks); now #ifdef __TINYC__ includes tcc's own <stdarg.h>.
  • user/libc.clongjmp's naked asm used jmpq *56(%rdi); tcc's assembler has no jmpq opcode, so it is plain jmp *56(%rdi) (same bytes under GCC).
  • user/syscall.hsyscall6's register long r10 asm("r10") binding is unsatisfiable in tcc; a #ifdef __TINYC__ path passes a4/a5/a6 as memory operands and moves them into r10/r8/r9 inside the asm.

What is provisioned into the OS

tools/mkinitramfs.py ships the sources and headers the in-OS compiler needs. The initramfs grew from ~77 files (v6.4.0) to 117 (v6.4.9, +~950 KB for the tcc sources).

Path Contents
/tcc.elf The compiler
/crt0.o, /libc.o, /va_list.o Prebuilt (GCC) link inputs — the default runtime
/usr/lib/tcc/include/ tcc's freestanding system headers (<stddef.h>, <stdarg.h>, …); cc adds this to the include path
/usr/src/nyx/libc.c, libc.h, syscall.h The OS's own libc source — a single dir so the quote-includes resolve
/usr/src/nyx/va_list.c tcc's va-runtime source (for --self-libc)
/usr/src/nyx/libtcc1.c tcc's main runtime library — the float ↔ 64-bit-int helpers the codegen emits
/usr/src/nyx/sh.c The userspace shell source — cc /usr/src/nyx/sh.c -o sh rebuilds it
/usr/src/nyx/*.c Coreutil sources (so the OS can rebuild its own utilities)
/usr/src/nyx/tcc/ A full mirror of user/tcc/ — the compiler's own source, for the v6.4.9 self-compile

Worked example

# Compile and link a C program to a runnable NyxOS executable
cc /mnt/hello.c -o /mnt/hello
# cc: compiling -> /mnt/hello
spawn /mnt/hello

# Separate compilation, then link
cc -c /mnt/a.c -o /mnt/a.o
cc -c /mnt/b.c -o /mnt/b.o
cc /mnt/a.o /mnt/b.o -o /mnt/prog

# Prove the whole runtime is tcc-built (only crt0.o stays GCC-built)
cc --self-libc /mnt/hello.c -o /mnt/hello

# The self-host itself
cc -c /usr/src/nyx/tcc/tcc.c -I/usr/src/nyx/tcc -I/usr/src/nyx/tcc/nyxshim -o /mnt/tcc_self.o

Known limitations

  • No optimiser, no make, no shared-library linking from cc. tcc is single-pass; cc links static freestanding executables only.
  • No floating-point before v6.4.6. Objects built by an earlier in-OS tcc silently zeroed every float/double literal.

Note

The v6.4.9 274 KB large-mount-write truncation — the reason the self-host object could not be persisted — was fixed at v6.4.10v6.4.13 (see Filesystem). The whole ~496 KB self-host object now reaches the disk, a program persisted on /mnt runs across reboots, and the self-host loop runs end-to-end in a single boot.

Packages — xbm

xbm is a minimal in-OS package manager (v6.4.19, first named pkg) built directly on cc: it installs a package by compiling its source in-OS — the "fetch source, then compile" model of pacman/apt, made literal.

Verb Effect
xbm install <name> Compile the package's recipe with cc and install it; it then runs by bare name
xbm remove <name> Uninstall an installed package
xbm search <str> Search available packages
xbm list [--installed] Browse available or installed packages

A recipe may carry a url: line pointing at a remote http:// source; xbm install then downloads that source into the package before building it (v6.4.73). pkg remains as a hidden alias.

Warning

Untrusted inputs, hardened deliberately. xbm validates package and recipe names to close a path-traversal + cc-arg-injection hole (v6.4.44), and the HTTP response parser on the fetch path was audited for untrusted server data (v6.4.76). See Security.

See also

External resources

Clone this wiki locally