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:
| A | docs/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.