boot2

Playing with the boostrap
git clone https://git.ryansepassi.com/git/boot2.git
Log | Files | Refs | README

commit f40962dac334abe1b9e7d39892fef2eee92018d1
parent eba5446e0f34d7556cf0c4a7e47010615ed20058
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sun, 26 Apr 2026 07:55:11 -0700

docs: TCC.md — pipeline from tarball to tcc-boot0-mes + open bugs

Documents the host-side bootstrap that produces a tcc-0.9.26 binary
linked against mes libc, with no M2-Planet/Mes-Scheme/MesCC dependency
at any stage. Three sub-stages:

  1. flatten         tcc.c + mes headers -> tcc.flat.c     (host cc -E)
  2. bootstrap       tcc.flat.c -> tcc-host                (host gcc + musl)
  3. relink          tcc-host + mes libc -> tcc-boot0-mes  (mes-style)

Points readers at scripts/build-tcc-source.sh and build-tcc-real.sh
for the actual mechanics, and at live-bootstrap's pass1.kaem for the
upstream reference path our chain mirrors.

Issues section captures the four real bugs found bringing this up:
two tcc-0.9.26 SEGV-on-error-paths (worked around in the scripts),
plus the missing _DYNAMIC stub and the unexplained tcc-boot0-mes
startup crash under QEMU. Both open issues likely resolve with a
backport of tcc-0.9.28rc fixes; native x86_64 testing of the current
artifact would also help distinguish QEMU vs. real codegen bug.

Diffstat:
Adocs/TCC.md | 337+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 337 insertions(+), 0 deletions(-)

diff --git a/docs/TCC.md b/docs/TCC.md @@ -0,0 +1,337 @@ +# Building tcc-0.9.26 from this repo + +Working doc. Describes the host-side pipeline that takes the upstream +tcc-0.9.26 source tarball through to a working tcc-0.9.26 binary linked +against mes libc, **without** depending on M2-Planet, Mes Scheme, or +MesCC at any stage. Two scripts drive the pipeline: + +- [scripts/build-tcc-source.sh](../scripts/build-tcc-source.sh) — host + C preprocessor produces a flattened single-source `tcc.flat.c`, then + bootstraps `tcc-host` from it via host gcc. +- [scripts/build-tcc-real.sh](../scripts/build-tcc-real.sh) — uses + `tcc-host` to compile mes libc and link a real tcc-0.9.26 binary + (`tcc-boot0-mes`). + +This is the upstream half of the [CC.md](CC.md) story. Once our +scheme1-hosted compiler can ingest `tcc.flat.c`, it slots in where +`tcc-host` is today, replacing host gcc as the bootstrap-stage compiler. + +## Inputs + +| Path | Contents | +|------|----------| +| `../lb-work/distfiles/tcc-0.9.26.tar.gz` | tcc-0.9.26-1147-gee75a10c source (janneke's bootstrap-friendly fork; the same artifact live-bootstrap consumes) | +| `../lb-work/distfiles/mes-0.27.1.tar.gz` | GNU Mes 0.27.1 — used **only** for its bundled minimal libc sources and headers, not for any Scheme runtime | +| `../live-bootstrap/steps/tcc-0.9.26/simple-patches/` | Two file-open reorder patches applied before the flatten step | +| `../mes/include/` | Same Mes headers as those in the tarball — used at flatten time to provide `<stdio.h>` etc. without pulling in host glibc/musl | + +The two host scripts sit on top of these inputs; they require nothing +else from the host besides `tar`, `awk`, a host `cc`, and `podman`. + +## Pipeline overview + +``` +tcc-0.9.26-1147-gee75a10c.tar.gz live-bootstrap source + │ + │ build-tcc-source.sh + │ • unpack + │ • apply 2 simple-patches + │ • host cc -E -nostdinc with mes headers + tcc-mes defines + ▼ +build/cc-bootstrap/X86_64/tcc.flat.c 608 KB single-file C + │ + │ build-tcc-source.sh --self-host + │ • inside alpine linux/amd64 + gcc + musl-dev + │ • gcc -static tcc.flat.c errno-shim.c -> tcc-host + │ • tcc-host -c tcc.flat.c -> tcc-self.o (self-host check) + ▼ +build/cc-bootstrap/X86_64/{tcc-host, tcc-self.o} bootstrap compiler + self-compile + │ + │ build-tcc-real.sh + │ • unpack mes-0.27.1 + │ • set up include tree with arch -> linux/x86_64 symlink + │ • tcc-host compiles each mes libc .c file individually + │ • tcc-host -ar -> libc.a + libtcc1.a + │ • tcc-host links tcc-self.o + mes libc -> tcc-boot0-mes + ▼ +build/cc-bootstrap/X86_64/tcc-boot0-mes 313 KB tcc-0.9.26 ELF +``` + +## Stage 1 — flatten tcc.c into tcc.flat.c + +`scripts/build-tcc-source.sh --arch X86_64` + +Mirrors the live-bootstrap `tcc-mes` invocation +([steps/tcc-0.9.26/pass1.kaem:60–87](../../live-bootstrap/steps/tcc-0.9.26/pass1.kaem)) +minus the actual compile. The host preprocessor expands every `#include` +in `tcc.c` (which uses `ONE_SOURCE=1` to fold `libtcc.c`, `tcctools.c`, +and the per-arch backends in via `#include`) and inlines all the Mes- +bundled standard headers. + +### Sub-steps + +1. **Unpack** `tcc-0.9.26.tar.gz` into `build/cc-bootstrap/X86_64/`. +2. **Apply simple-patches**: `remove-fileopen.before/.after` then + `addback-fileopen.before/.after` against `tcctools.c`. Implemented as + an `awk` literal-block replacer (live-bootstrap's `simple-patch` is a + trivial before/after substitution; we don't depend on the binary). +3. **Empty config.h shims**: live-bootstrap creates two empty + `config.h` files via `catm` (an empty concat). We do the same — one + in `$TCC_PKG/config.h`, one in `mes-overlay/mes/config.h` for the + `<mes/config.h>` reach the Mes stdio.h does. +4. **Host preprocess**: `cc -E -nostdinc` with the Mes headers as the + sole `-I` set, plus the same `-D` set live-bootstrap passes: + + ``` + -D BOOTSTRAP=1 + -D HAVE_LONG_LONG=1 + -D inline= + -D ONE_SOURCE=1 + -D TCC_TARGET_X86_64=1 + -D __linux__=1 + -D __x86_64__=1 # mescc would inject these; we mirror them + -D CONFIG_TCC_*="..." # exactly the live-bootstrap paths + ``` + + Output: a single ~600 KB C bytestream, no remaining `#include`s, + no preprocessor directives at all. + +5. **(Optional, --verify)** host `cc -c tcc.flat.c -> tcc.flat.o`. On + macOS this produces a Mach-O .o; the verify is purely a "does the + source compile" check. Failure here means the flatten step is wrong. + +## Stage 2 — bootstrap tcc-host + +`scripts/build-tcc-source.sh --arch X86_64 --self-host` + +Implies `--verify`, then drives a Linux container to build a real ELF +tcc-0.9.26 binary from `tcc.flat.c` using host gcc. + +### Why a container + +Stage 1 only needs a preprocessor — works on any host. Stage 2 needs: +- Linux x86_64 toolchain (the resulting binary runs on Linux x86_64) +- musl-dev for headers/libs that gcc can link against + +The macOS dev host has clang but produces Mach-O, not ELF. We use a +clean `alpine:latest` linux/amd64 container (~5 MB image, +~150 MB for +gcc + musl-dev on first run, cached afterwards). On macOS arm64 hosts, +podman runs this under QEMU x86_64 emulation — slow but functional. + +### errno shim + +`tcc.flat.c` came out of the Mes-headers preprocess, so it expects +`extern int errno;` (Mes's plain-global form). musl provides errno +through `__errno_location()` instead, so the link fails. A one-line +`errno-shim.c` (`int errno;`) fills the gap. + +``` +gcc -w -static -no-pie -o tcc-host tcc.flat.c errno-shim.c +``` + +### Smoke tests + +After build: + +- `tcc-host -version` → `tcc version 0.9.26 (x86_64 Linux)` ✓ +- `tcc-host -c -o tcc-self.o tcc.flat.c` → 485 KB object file ✓ + This is **the self-host check**: tcc compiling its own preprocessed + source. `tcc-self.o` is the object that Stage 3 links against mes + libc. + +## Stage 3 — link a real tcc against mes libc + +`scripts/build-tcc-real.sh` + +Drives the same alpine container the rest of the way. Produces +`tcc-boot0-mes`, the equivalent of live-bootstrap's tcc-boot0 stage — +but built without ever invoking mescc. + +### Sub-steps + +1. **Unpack `mes-0.27.1`** alongside `tcc-0.9.26`. Used purely for + `lib/**.c` (libc sources) and `include/**.h` (libc headers). +2. **Sanitize the include tree**: + + ``` + /tmp/mes-inc/ <- copy of mes/include/ + /tmp/mes-inc/arch -> linux/x86_64 <- symlink we create + ``` + + The Mes headers do `#include <arch/syscall.h>`. Live-bootstrap must + set up an equivalent symlink internally; the tarball doesn't ship + one. Without this symlink, tcc-host segfaults rather than cleanly + reporting the missing include (see Issues §1). + +3. **Compile each mes libc .c file individually** with tcc-host. The + live-bootstrap kaem script `catm`s ~250 files into a single + `unified-libc.c` and compiles that as one TU. tcc-host crashes on + that approach (see Issues §2). Compiling each as a separate TU and + `ar`-ing the results sidesteps the bug and is what tcc was designed + to do anyway. + +4. **`tcc-host -ar` the .o set into `libc.a`** (~299 KB). + +5. **Build crt1.o** from `lib/linux/x86_64-mes-gcc/crt1.c` (mes's hand- + rolled `_start` that invokes `main` directly via inline asm). On + x86_64, `crtn.o` and `crti.o` are empty files (live-bootstrap does + the same). + +6. **Build libtcc1.a** from `lib/libtcc1.c` (tcc's helper runtime). + +7. **Install at the baked-in paths**: `tcc-host` was built with + `CONFIG_TCC_CRTPREFIX="/lib"`, `CONFIG_TCCDIR="/lib/tcc"`, + `CONFIG_TCC_SYSINCLUDEPATHS="/include/mes"`. The script populates + those locations inside the container so tcc-host can find its + pieces without `-B`/`-L` overrides. + +8. **Link**: `tcc-host -static -o tcc-boot0-mes tcc-self.o`. + + tcc-host walks `tcc-self.o`'s symbol references against `crt1.o`, + `libc.a`, `libtcc1.a`, emits a static ELF. + +Result: `build/cc-bootstrap/X86_64/tcc-boot0-mes` — 313 KB. Roughly +half the size of the earlier musl-linked variant (`tcc-boot0`, 764 KB), +which tracks: mes libc is stripped down, musl is full. + +## Relation to live-bootstrap + +``` +live-bootstrap path: mescc → tcc-mes → tcc-boot0 → tcc-boot1 → tcc-boot2 → tcc → tcc-0.9.27 → make 3.82 → ... +our path: gcc → tcc-host → tcc-boot0-mes + (= live-bootstrap's tcc-boot0 equivalent) +``` + +`tcc-boot0-mes` is the slot-equivalent of live-bootstrap's `tcc-boot0`. +The downstream chain (rebuild libc → tcc-boot1 → rebuild libc → +tcc-boot2 → tcc → tcc-0.9.27 → make 3.82 → real builds) is a series of +mechanical recompiles the existing pass1.kaem already orchestrates. +Picking up from `tcc-boot0-mes` requires only that it run correctly +(see Issues §3). + +## What this unlocks for the scheme1 cc + +The ultimate goal: replace `tcc-host` in Stage 2 with our scheme1-hosted +C compiler. The interface is fixed: + +- **Input**: `tcc.flat.c` produced by Stage 1. +- **Output**: an ELF binary equivalent to `tcc-host` (or an object file + equivalent to `tcc-self.o`, which gcc would then link). + +`tcc.flat.c` is a known-good, host-cc-validated artifact ready for +the scheme1 cc to chew on incrementally. Track progress against it. + +## Reproducibility + +``` +scripts/build-tcc-source.sh --arch X86_64 --self-host +scripts/build-tcc-real.sh +``` + +Three artifacts land in `build/cc-bootstrap/X86_64/`: + +| File | Size | Built by | What it is | +|-------------------|--------|------------------|-----------------------------------------| +| `tcc.flat.c` | 608 KB | host cc | flattened single-source tcc-0.9.26 | +| `tcc-host` | 704 KB | host gcc + musl | first-stage tcc, runs natively on Linux | +| `tcc-self.o` | 485 KB | tcc-host | tcc-host compiling its own source | +| `tcc-boot0-mes` | 313 KB | tcc-host + mes lib | real tcc-0.9.26, mes-libc-linked | + +`build/` is in `.gitignore`; nothing is tracked outside of the scripts +themselves. + +## Issues / bugs + +Found while bringing this pipeline up. All four are real, none are +ours; (1) and (2) are bugs in tcc 0.9.26 that we work around in the +scripts; (3) and (4) are open and need either native x86_64 testing or +a tcc backport. + +### 1. tcc 0.9.26 SEGV on missing include + +When tcc-host can't resolve an `#include "..."` or `#include <...>`, +it segfaults instead of reporting the error. Discovered when +`mes/include/dirent.h` does `#include <arch/syscall.h>`; mes/include +has no `arch/` directory by default. + +**Workaround**: in `build-tcc-real.sh`, create +`/tmp/mes-inc/arch -> linux/x86_64` before invoking tcc-host. The +live-bootstrap build presumably sets up the same symlink somewhere we +haven't traced — possibly via a configure step, or via the +`include_next` chain in `SYSTEM_LIBC` mode. + +A proper fix would patch tcc 0.9.26's preprocessor to error cleanly, +but the symlink workaround is enough for our purposes. + +### 2. tcc 0.9.26 SEGV on large concatenated TU + +When ~22+ mes libc files are catm'd into one TU and the chain hits a +file with non-trivial inline asm (the trigger we found was +`linux/x86_64-mes-gcc/_exit.c`), tcc-host crashes mid-compile. Below +that threshold all combinations work. Each individual file compiles +fine. + +The interaction is some accumulator state inside tcc — symbol table, +hash chain, or similar — that overflows or hits a corrupted state +when the TU grows large enough. + +**Workaround**: compile each `.c` separately, then `ar` together. +`build-tcc-real.sh` does this for all 258 mes libc .c files. +Bonus: avoids ~250 redundant header re-parses, faster overall. + +### 3. tcc 0.9.26 emits no `_DYNAMIC` stub for static binaries + +When linking a tcc-host-output ELF against musl's `crt1.o` + `libc.a` +on alpine, the resulting binary segfaults because tcc 0.9.26 doesn't +emit a `_DYNAMIC` symbol; the `lea _DYNAMIC(%rip), %rsi` in musl's +`_start` resolves to address 0, and musl's `__libc_start_main` +dereferences rsi. + +Side-by-side disassembly of the same hello-world built with alpine's +prebuilt tcc 0.9.28rc and our tcc-host: + +``` +alpine 0.9.28rc: lea 0x9bb(%rip),%rsi # 0x4014a0 ← real stub +ours 0.9.26: lea -0x4000bd(%rip),%rsi # 0x0 ← NULL +``` + +This is why the **musl-linked** tcc-boot0 doesn't run. The +**mes-libc-linked** `tcc-boot0-mes` should sidestep it because mes's +`crt1.c` calls `main` directly, not via `__libc_start_main`, so rsi +is never dereferenced. + +A patch to emit a `_DYNAMIC` stub would be small. tcc 0.9.28rc has +the fix; backporting it to the flat tcc.c is a candidate next step. + +### 4. tcc-boot0-mes segfaults at startup under QEMU x86_64 + +`./tcc-boot0-mes -version` segfaults under `podman run --platform +linux/amd64` on macOS arm64. **Whether this works on native x86_64 +hardware is untested.** + +Possibilities: + +a) tcc 0.9.26 codegen bug in the function prologue tcc emits for + `_start` (mes's `crt1.c` declares `_start` as a regular C + function with inline asm; tcc adds a standard prologue, and a + bug in that prologue under specific conditions could mismatch + mes's expectations of `rbp`). + +b) A QEMU-specific interaction with mes's hand-rolled `_start` + inline asm (which assumes the kernel-provided process-entry + stack layout). QEMU's process-emulation user-mode does set + that up, but corner cases exist. + +(a) is more likely because the same QEMU runs alpine's prebuilt +tcc 0.9.28rc — and tcc-built binaries from that — without any +trouble. So whatever's wrong is specific to **our** binary's +contents. + +**Path to verify**: run tcc-boot0-mes on native x86_64. Failure +there confirms (a) and points at a tcc-source patch. +**Path to fix**: backport tcc 0.9.28rc's `_DYNAMIC` and prologue +fixes (probably the same patch series that fixes Issue 3). + +Once tcc-boot0-mes runs, the rest of the chain +(rebuild-libc → tcc-boot1 → tcc-boot2 → tcc → tcc-0.9.27 → make 3.82 +→ ...) is what live-bootstrap's pass1.kaem already does.