boot2

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

commit dca1d7920a5a3ee3c5fd68e4d277264c3603495c
parent bf8dcd15f094656d643c07382501d47d925b8831
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 29 Apr 2026 11:25:07 -0700

cc.scm: --lib mode for catm-style libc linking

Replace scripts/boot-libc-prepend.sh's awk transforms with a --lib=PFX
flag in cc.scm that suppresses the auto-emitted entry stub and trailing
:ELF_end and namespaces anonymous string labels (cc__str_N → PFX+...).
Library and executable TUs now compose by catm:

    P1/entry-libc.P1pp  build/$ARCH/libc.P1pp  client.P1pp  P1/elf-end.P1pp

boot-build-p1pp.sh accepts the chain as multiple sources; boot-build-cc.sh
gains a CC_LIB env var to drive the new flag. Three small fragments in
P1/ (entry-plain, entry-libc, elf-end) supply the parts that aren't
cc.scm's job.

cg-intern-string pads each string literal to 8B so the next label lands
at an aligned address (aarch64 BLR / 4-byte LDR fault otherwise). Padding
is gated on the trailing-NUL flag so non-string init pieces (4-byte int
slots inside struct initializers, etc.) keep their natural length and
struct field offsets don't shift.

Diffstat:
MMakefile | 29+++++++++++++++++++----------
AP1/elf-end.P1pp | 8++++++++
AP1/entry-libc.P1pp | 19+++++++++++++++++++
AP1/entry-plain.P1pp | 21+++++++++++++++++++++
Mcc/cc.scm | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mdocs/LIBC.md | 119+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mscripts/boot-build-cc.sh | 21++++++++++++++++-----
Mscripts/boot-build-p1pp.sh | 30+++++++++++++++++++-----------
Dscripts/boot-libc-prepend.sh | 98-------------------------------------------------------------------------------
Mscripts/boot-run-tests.sh | 29+++++++++++++++--------------
Mtests/cc-libc/00-exit.c | 2+-
11 files changed, 279 insertions(+), 240 deletions(-)

diff --git a/Makefile b/Makefile @@ -199,7 +199,7 @@ $(HELLO_BINS): build/%/hello: $(HELLO_SRC) $(P1_BUILD_DEPS) $(call PODMAN,$*) sh scripts/boot-build-p1.sh $(HELLO_SRC) $@ $(SCHEME1_BINS): build/%/scheme1: $(SCHEME1_SRC) $(P1PP_BUILD_DEPS) - $(call PODMAN,$*) sh scripts/boot-build-p1pp.sh $(SCHEME1_SRC) $@ + $(call PODMAN,$*) sh scripts/boot-build-p1pp.sh $@ $(SCHEME1_SRC) # cc.scm: catm prelude + cc.scm + main.scm entry into one source the # scheme1 interpreter can run. Catm runs inside the per-arch container. @@ -248,24 +248,31 @@ $(LIBC_FLATS): build/cc-bootstrap/%/libc.flat.c: \ scripts/libc-flatten.sh $(LIBC_VENDOR_SRCS) sh scripts/libc-flatten.sh --arch $* +# libc and tcc.flat are both compiled with --lib= so they omit the +# entry stub and trailing :ELF_end (provided by P1/entry-libc.P1pp +# and P1/elf-end.P1pp at link time). Distinct prefixes keep the +# anonymous string labels (cc__str_N) from colliding when both TUs +# end up in the same catm chain. $(LIBC_P1PPS): build/%/libc.P1pp: \ build/cc-bootstrap/%/libc.flat.c \ build/%/scheme1 build/%/cc/cc.scm \ scripts/boot-build-cc.sh build/%/.image - $(call PODMAN,$*) sh scripts/boot-build-cc.sh $< $@ + $(call PODMAN,$*) env CC_LIB=libc__ sh scripts/boot-build-cc.sh $< $@ $(TCC_BOOT2_P1PPS): build/%/tcc-boot2/tcc.flat.P1pp: \ $(TCC_FLAT) build/%/scheme1 build/%/cc/cc.scm \ scripts/boot-build-cc.sh build/%/.image - $(call PODMAN,$*) sh scripts/boot-build-cc.sh $(TCC_FLAT) $@ + $(call PODMAN,$*) env CC_LIB=tcc__ sh scripts/boot-build-cc.sh $(TCC_FLAT) $@ +# tcc-boot2 link: pure catm chain — entry stub, libc, client TU, +# elf terminator. boot-build-p1pp.sh concatenates them in order +# ahead of the M1pp expander/M0/hex2 pipeline. $(TCC_BOOT2_BINS): build/%/tcc-boot2/tcc-boot2: \ build/%/tcc-boot2/tcc.flat.P1pp build/%/libc.P1pp \ - scripts/boot-libc-prepend.sh \ + P1/entry-libc.P1pp P1/elf-end.P1pp \ $(P1PP_BUILD_DEPS) - $(call PODMAN,$*) sh scripts/boot-libc-prepend.sh \ - build/$*/libc.P1pp $< $(@D)/tcc-boot2.P1pp - $(call PODMAN,$*) env P1PP_TRACE=1 sh scripts/boot-build-p1pp.sh $(@D)/tcc-boot2.P1pp $@ + $(call PODMAN,$*) env P1PP_TRACE=1 sh scripts/boot-build-p1pp.sh $@ \ + P1/entry-libc.P1pp build/$*/libc.P1pp $< P1/elf-end.P1pp # --- Native tools (opt-in dev-loop helpers) ------------------------------- @@ -327,11 +334,12 @@ TEST_CC_UNIT_DEPS := $(foreach a,$(TEST_ARCHES), \ TEST_CC_DEPS := $(TEST_CC_UNIT_DEPS) \ $(foreach a,$(TEST_ARCHES),build/$(a)/cc/cc.scm) -# cc-libc: cc deps + the pre-built libc.P1pp the suite prepends to every -# fixture. Targeted red-green TDD on the cc.scm + libc combination. +# cc-libc: cc deps + the pre-built libc.P1pp the suite catm's into every +# fixture, plus the entry/elf-end fragments the catm chain depends on. +# Targeted red-green TDD on the cc.scm + libc combination. TEST_CC_LIBC_DEPS := $(TEST_CC_DEPS) \ $(foreach a,$(TEST_ARCHES),build/$(a)/libc.P1pp) \ - scripts/boot-libc-prepend.sh + P1/entry-libc.P1pp P1/elf-end.P1pp test: ifeq ($(SUITE),) @@ -343,6 +351,7 @@ ifeq ($(SUITE),) @$(MAKE) --no-print-directory test SUITE=cc-pp @$(MAKE) --no-print-directory test SUITE=cc-cg @$(MAKE) --no-print-directory test SUITE=cc + @$(MAKE) --no-print-directory test SUITE=cc-libc else ifeq ($(SUITE),m1pp) @$(MAKE) --no-print-directory $(TEST_M1PP_DEPS) sh scripts/run-tests.sh --suite=m1pp $(if $(ARCH_FILTER),--arch=$(ARCH_FILTER)) diff --git a/P1/elf-end.P1pp b/P1/elf-end.P1pp @@ -0,0 +1,8 @@ +# P1/elf-end.P1pp — terminator for catm-style links. +# +# vendor/seed/$ARCH/ELF.hex2 sizes p_filesz from the first :ELF_end +# label it sees. Exactly one TU in a link must emit it; in exec mode +# cc.scm appends it for you, but in --lib mode every TU suppresses +# it so the chain can supply this fragment once at the tail. + +:ELF_end diff --git a/P1/entry-libc.P1pp b/P1/entry-libc.P1pp @@ -0,0 +1,19 @@ +# P1/entry-libc.P1pp — executable entry stub for links that include +# our libc (vendor/mes-libc + lispcc-syscall.c). +# +# Same shape as entry-plain.P1pp but threads __libc_init in front of +# main. __libc_init (lispcc-syscall.c) reads argv's NULL terminator +# to populate `environ` so getenv()/etc don't dereference a NULL +# environment pointer on the first call. argc/argv arrive in a0/a1 +# from the bootstrap _start; %call doesn't clobber them, so the +# second %call delivers them to main unchanged. +# +# Cat this in exactly once at the head of a libc-using catm chain +# and pair with P1/elf-end.P1pp at the tail. Library TUs (libc, +# client) should be built with cc.scm --lib=PFX so they don't +# also try to define :p1_main. + +%fn(p1_main, 0, { + %call(&__libc_init) + %call(&main) +}) diff --git a/P1/entry-plain.P1pp b/P1/entry-plain.P1pp @@ -0,0 +1,21 @@ +# P1/entry-plain.P1pp — executable entry stub for catm-style links. +# +# P1's program-entry contract (docs/P1.md §Program Entry) delivers +# argc in a0 and argv in a1 at :p1_main. Under cc.scm's standard +# convention :main has the same shape, so a bare %call(&main) forwards +# both unchanged (%call doesn't clobber a0/a1). main builds its own +# frame on top. +# +# %fn size = 0: this stub has no locals and no overflow outgoing args +# (only 2 register-passed args). %enter on every arch independently +# allocates a 16-byte slot for saved-lr / saved-old-sp on top of the +# user-requested size, so a 0-size frame still has the saves it needs. +# +# Cat this fragment in exactly once at the head of a catm chain to +# supply :p1_main, and pair with P1/elf-end.P1pp at the tail. Library +# TUs (built with cc.scm --lib=PFX) suppress the auto-emitted entry +# stub, which is what makes adding this fragment safe (no collision). + +%fn(p1_main, 0, { + %call(&main) +}) diff --git a/cc/cc.scm b/cc/cc.scm @@ -453,8 +453,16 @@ ;; cg-fn-meta: transient per-function state (fn-name, ret-slot, ret-type, ;; vararg-first-slot, indirect-slots, switch-case lists, ...). Reset on ;; cg-fn-begin/v; reads via %cg-fn-get / writes via %cg-fn-set!. +;; lib? / str-prefix encode the --lib=PFX flag from cc-main: +;; #f / "" — exec mode (default): cg-finish emits the +;; p1_main entry stub and trailing :ELF_end, and +;; cg-intern-string labels strings cc__str_N. +;; #t / "<pfx>" — library mode: skip the stub and :ELF_end so the +;; output catm's into a larger TU, and label strings +;; <pfx>cc__str_N so two cc.scm outputs in the same +;; link don't collide on cc__str_0..N. (define-record-type cg - (%cg text data bss vstack frame-hi label-ctr world fn-meta fn-buf prologue-buf max-outgoing in-fn?) + (%cg text data bss vstack frame-hi label-ctr world fn-meta fn-buf prologue-buf max-outgoing in-fn? lib? str-prefix) cg? (text cg-text) (data cg-data) @@ -467,7 +475,9 @@ (fn-buf cg-fn-buf) (prologue-buf cg-prologue-buf) (max-outgoing cg-max-outgoing cg-max-outgoing-set!) - (in-fn? cg-in-fn? cg-in-fn?-set!)) + (in-fn? cg-in-fn? cg-in-fn?-set!) + (lib? cg-lib?) + (str-prefix cg-str-prefix)) (define (cg-str-pool cg) (world-str-pool (cg-world cg))) (define (cg-str-pool-set! cg v) (world-str-pool-set! (cg-world cg) v)) @@ -2816,7 +2826,12 @@ ;; Lifecycle ;; -------------------------------------------------------------------- -(define (cg-init) +;; cc-cg fixtures construct a cg directly via (cg-init) — they don't +;; emit ELF and don't link against another TU, so library knobs are +;; irrelevant. cc-main routes through cg-init/v with the parsed flag. +(define (cg-init) (cg-init/v #f "")) + +(define (cg-init/v lib? str-prefix) (%cg (make-buf/cap %BUF-CAP-TEXT) ; text (make-buf/cap %BUF-CAP-DATA) ; data (make-buf/cap %BUF-CAP-BSS) ; bss @@ -2828,7 +2843,9 @@ (make-buf/cap %BUF-CAP-FN) ; fn-buf (reused per fn) (make-buf/cap %BUF-CAP-PROLOGUE) ; prologue-buf (reused per fn) 0 ; max-outgoing - #f)) ; in-fn? + #f ; in-fn? + lib? ; lib? (skip entry stub + :ELF_end) + str-prefix)) ; str-prefix (cc__str_N namespacing) (define (cg-finish cg) ;; Tentative file-scope defs (`int x;` / `static int x;` with no @@ -2840,17 +2857,24 @@ ;; clobber a0/a1, so falling straight through to main forwards ;; them unchanged. The 16-byte frame is just enough for %enter's ;; saved-fp/lr to fit; main builds its own frame on top. - (let ((tb (cg-text cg))) - (buf-push! tb "# entry stub: forwards argc=a0, argv=a1 to main\n") - (buf-push! tb "%fn(p1_main, 16, {\n") - (buf-push! tb "%call(&main)\n") - (buf-push! tb "})\n")) - ;; Every P1pp translation unit must end with :ELF_end so the ELF - ;; header can compute file-size and ph_memsz boundaries. + ;; + ;; In lib mode the stub and :ELF_end are suppressed: the catm chain + ;; supplies them once, from P1/entry-*.P1pp and P1/elf-end.P1pp, so + ;; library TUs don't fight the executable TU for ownership of + ;; :p1_main and don't truncate ELF p_filesz at the first inner + ;; :ELF_end (hex2 sizes off the first one it sees). + (cond + ((not (cg-lib? cg)) + (let ((tb (cg-text cg))) + (buf-push! tb "# entry stub: forwards argc=a0, argv=a1 to main\n") + (buf-push! tb "%fn(p1_main, 16, {\n") + (buf-push! tb "%call(&main)\n") + (buf-push! tb "})\n")))) (bv-cat (list (buf-flush (cg-text cg)) (buf-flush (cg-data cg)) (buf-flush (cg-bss cg)) - ":ELF_end\n"))) + (cond ((cg-lib? cg) "") + (else ":ELF_end\n"))))) (define (cg-fn-begin cg name params return-type) (cg-fn-begin/v cg name params return-type #f)) @@ -4026,7 +4050,8 @@ (p p) (else (let* ((n (length (cg-str-pool cg))) - (lbl (bytevector-append "cc__str_" (%n n)))) + (lbl (bytevector-append + (cg-str-prefix cg) "cc__str_" (%n n)))) (cg-str-pool-set! cg (alist-set bv-content lbl (cg-str-pool cg))) (buf-push! (cg-data cg) @@ -4044,12 +4069,24 @@ ;; is 256 bytes on amd64 and overflows otherwise. ;; ;; If TRAILING-NUL? is #t, an extra 0x00 byte is appended to terminate -;; a C string. Returns a list of bytevectors ready for bv-cat. +;; a C string AND total emitted bytes are rounded up to %CG-STR-ALIGN +;; (8). The padding makes the next label after a string land at an +;; 8-aligned address — aarch64 BLR / 4-byte LDR fault otherwise. +;; Padding is gated on TRAILING-NUL? because the only other caller +;; (%cg-init-piece->bv) emits arbitrary initializer bytes whose +;; length is sized exactly to the C-visible field; padding a 4-byte +;; int slot to 8 would shift every following struct field. The +;; %cg-hex-line tail already emits 0x00 for indices past LEN, so +;; padding costs only output bytes. Returns a list of bytevectors +;; ready for bv-cat. (define %CG-HEX-CHUNK-BYTES 64) +(define %CG-STR-ALIGN 8) (define (%cg-bv->hex-lines bv trailing-nul?) - (let* ((len (bytevector-length bv)) - (total (cond (trailing-nul? (+ len 1)) (else len)))) + (let* ((len (bytevector-length bv)) + (logical (cond (trailing-nul? (+ len 1)) (else len))) + (total (cond (trailing-nul? (align-up logical %CG-STR-ALIGN)) + (else logical)))) (cond ((= total 0) '()) (else @@ -6653,31 +6690,57 @@ ((bv= (car args) flag) (cdr args)) (else (cons (car args) (%cc-strip-flag (cdr args) flag))))) +;; --lib=PFX selects library-mode codegen: cc.scm skips the p1_main +;; entry stub and trailing :ELF_end (the catm chain supplies them +;; from P1/entry-*.P1pp + P1/elf-end.P1pp once), and namespaces +;; anonymous string labels as PFX+"cc__str_N" so two cc.scm outputs +;; in the same link don't collide on cc__str_0..N. Returns +;; (values prefix-bv rest-args). PREFIX = "" means exec mode (flag +;; absent). PREFIX = "" with the flag present is rejected — silently +;; falling back to exec mode would mask a typo'd Makefile rule. +(define (%cc-take-lib args) + (let loop ((acc '()) (rest args) (pfx #f)) + (cond + ((null? rest) + (values (cond (pfx pfx) (else "")) (reverse acc))) + ((bv-prefix? "--lib=" (car rest)) + (cond (pfx (die #f "cc: --lib= specified twice"))) + (let* ((arg (car rest)) + (p (bv-slice arg 6 (bytevector-length arg)))) + (cond ((= 0 (bytevector-length p)) + (die #f "cc: --lib= requires a non-empty PREFIX"))) + (loop acc (cdr rest) p))) + (else + (loop (cons (car rest) acc) (cdr rest) pfx))))) + (define (cc-main av) (let* ((raw (cdr (cdr av))) (dbg (%cc-flag? raw "--cc-debug")) - (args (%cc-strip-flag raw "--cc-debug"))) + (a1 (%cc-strip-flag raw "--cc-debug"))) (cond (dbg (debug-log-on!))) - (cond - ((or (null? args) (null? (cdr args))) - (die #f "usage: cc [--cc-debug] <input.c> <output.P1pp>"))) - (let* ((in-path (car args)) - (out-path (car (cdr args)))) - (debug-log "phase=start" "heap" (heap-usage)) - ;; Streaming pipeline: lex → pp → parser → cg, all concurrent. - ;; Each stage pulls one tok at a time from upstream. Steady-state - ;; live data is bounded by parser/pp state, not source length. - (let* ((src (%cc-slurp in-path)) - (_1 (debug-log "phase=slurp" "heap" (heap-usage) - "src-bytes" (bytevector-length src))) - (lex-iter (make-lex-iter src in-path)) - (pp-iter (make-pp-iter lex-iter '())) - (cg (cg-init)) - (ps (make-pstate pp-iter cg))) - (parse-translation-unit ps) - (debug-log "phase=parse" "heap" (heap-usage)) - (let ((out (cg-finish cg))) - (debug-log "phase=cg-finish" "heap" (heap-usage) - "out-bytes" (bytevector-length out)) - (%cc-write out-path out)) - 0)))) + (let-values (((lib-prefix args) (%cc-take-lib a1))) + (cond + ((or (null? args) (null? (cdr args))) + (die #f "usage: cc [--cc-debug] [--lib=PFX] <input.c> <output.P1pp>"))) + (let* ((in-path (car args)) + (out-path (car (cdr args))) + (lib? (cond ((= 0 (bytevector-length lib-prefix)) #f) + (else #t)))) + (debug-log "phase=start" "heap" (heap-usage)) + ;; Streaming pipeline: lex → pp → parser → cg, all concurrent. + ;; Each stage pulls one tok at a time from upstream. Steady-state + ;; live data is bounded by parser/pp state, not source length. + (let* ((src (%cc-slurp in-path)) + (_1 (debug-log "phase=slurp" "heap" (heap-usage) + "src-bytes" (bytevector-length src))) + (lex-iter (make-lex-iter src in-path)) + (pp-iter (make-pp-iter lex-iter '())) + (cg (cg-init/v lib? lib-prefix)) + (ps (make-pstate pp-iter cg))) + (parse-translation-unit ps) + (debug-log "phase=parse" "heap" (heap-usage)) + (let ((out (cg-finish cg))) + (debug-log "phase=cg-finish" "heap" (heap-usage) + "out-bytes" (bytevector-length out)) + (%cc-write out-path out)) + 0))))) diff --git a/docs/LIBC.md b/docs/LIBC.md @@ -49,8 +49,12 @@ vendor/mes-libc/ scripts/ ├── libc-flatten.sh host (no boot- prefix); stage + patch + -E -├── boot-build-cc.sh container; cc.scm → libc.P1pp -└── boot-libc-prepend.sh container; link-time transforms (§Linking) +└── boot-build-cc.sh container; cc.scm → libc.P1pp (CC_LIB=PFX + selects cc.scm --lib= mode; see §Linking) + +P1/ +├── entry-libc.P1pp :p1_main wrapper (calls __libc_init, main) +└── elf-end.P1pp single :ELF_end terminator label tests/cc-libc/ targeted fixtures for cc.scm + libc TDD ``` @@ -193,49 +197,49 @@ stack_t;`. `libc.flat.c` to produce `build/$ARCH/libc.P1pp` (~520 KB, ~21 K lines). -### Linking — `scripts/boot-libc-prepend.sh` - -cc.scm's output `libc.P1pp` is *almost* a P1pp library — but cc.scm -emits the same standard executable tail for every TU. The link-time -script transforms libc.P1pp: - -1. Drop cc.scm's auto-emitted exec tail (`# entry stub` comment plus - the `%fn(p1_main, 16, { %call(&main) })` block). Library TUs must - not own `:p1_main`. -2. Drop the trailing `:ELF_end`. `ELF.hex2` sizes `p_filesz` from - the first `:ELF_end` it sees; only the executable TU may emit it. -3. Rename internal-linkage `cc__str_N` (anonymous string literals) to - `libc__cc__str_N`. cc.scm restarts that counter at 0 per TU; hex2 - silently first-def-wins on duplicates and refs in the loser TU - bind to the wrong bytes. -4. Pad each `'<hex>'` literal to an 8-byte boundary. cc.scm emits - strings at their natural length; if the byte count isn't a - multiple of 4, every label that follows lands at a non-4-aligned - address and aarch64 BLR SIGBUSes. - -Then it appends our own fixed `:p1_main` wrapper that calls -`__libc_init` (sets `environ`) before forwarding to `main`, and -applies the same auto-`:p1_main` strip to the executable TU so its -copy doesn't shadow ours. - -The Makefile uses this script for both the tcc-boot2 link and the -cc-libc test suite, so there's exactly one place to evolve the -link-time invariants. - -All four numbered transforms work around cc.scm bugs; the right -long-term fix is a `--library` mode in cc.scm that does (1, 2, 3) -internally and a string-padding pass that handles (4). See -[TCC-TODO.md §cc.scm-libc-issues](TCC-TODO.md). +### Linking — catm chain + +cc.scm has a `--lib=PFX` flag that turns its output into a P1pp +library: it suppresses the auto-emitted entry stub +(`%fn(p1_main, 16, { %call(&main) })`) and the trailing `:ELF_end`, +and namespaces anonymous string labels as `PFX+"cc__str_N"` so two +cc.scm outputs in the same link don't collide on `cc__str_0..N`. +String literals are 8-byte padded unconditionally (any TU, lib or +exec) so labels following a string land at an aligned address — +without it, aarch64 BLR / 4-byte LDR SIGBUS once a non-multiple-of-4 +string shows up in `.data`. + +Wired together, the link is just `catm`: + +``` +P1/entry-libc.P1pp # :p1_main → __libc_init → main +build/$ARCH/libc.P1pp # cc.scm --lib=libc__ → libc__cc__str_* +<client>.P1pp # cc.scm --lib=<pfx>__ → <pfx>__cc__str_* +P1/elf-end.P1pp # :ELF_end +``` + +`scripts/boot-build-p1pp.sh` already cats its inputs in front of +the M1pp expander, so the catm chain is just its source-list +arguments. Both the tcc-boot2 link rule (Makefile) and the cc-libc +test suite (boot-run-tests.sh) compose this way; the tcc-boot2 +client uses prefix `tcc__`, every cc-libc fixture uses `app__`. + +`__libc_init` (`vendor/mes-libc/lispcc-syscall.c`) walks argv's +NULL terminator to populate `environ`; it must run before any libc +function that reads the environment. That's why the entry fragment +calls it ahead of `main`. ### Wiring ``` -make tcc-boot2 ARCH=aarch64 # builds libc.P1pp + tcc.flat.P1pp, - # then libc-prepends and assembles to ELF +make tcc-boot2 ARCH=aarch64 # builds libc.P1pp + tcc.flat.P1pp + # (both --lib= mode), then catms with + # entry-libc + elf-end into the ELF ``` -The tcc-boot2 link rule depends on `build/$ARCH/libc.P1pp` and -`scripts/boot-libc-prepend.sh`; rebuilds when either changes. +The tcc-boot2 link rule depends on `build/$ARCH/libc.P1pp`, +`P1/entry-libc.P1pp`, and `P1/elf-end.P1pp`; rebuilds when any +changes. ## Phase A status @@ -282,16 +286,17 @@ make test SUITE=cc-libc ARCH=aarch64 -- 05-printf-int # one fixture Per-fixture artefacts at `build/$ARCH/cc-libc/<name>/`: -- `<name>.client.P1pp` — cc.scm output for the fixture -- `<name>.P1pp` — merged (libc + client) input to boot-build-p1pp.sh +- `<name>.client.P1pp` — cc.scm output for the fixture (lib mode, + prefix `app__`) - `<name>` — final ELF -- `cc.log` / `prepend.log` / `p1pp.log` — captured stdout+stderr from - each pipeline stage; the suite handler dumps the relevant log under - the FAIL row when a stage exits non-zero. +- `cc.log` / `p1pp.log` — captured stdout+stderr from each pipeline + stage; the suite handler dumps the relevant log under the FAIL row + when a stage exits non-zero. -When triaging a failure, the merged `.P1pp` is the artefact to grep -for the symbol or sequence in question; cc.scm's output marks -function regions clearly. +When triaging a failure, the catm'd source the M1pp expander sees +lives at `build/$ARCH/.work/<name>/combined.M1pp` (boot-build-p1pp.sh +copies it there alongside the rest of the per-stage scratch outputs); +grep that for the symbol or sequence in question. ## Phase B — build the on-disk archives tcc-boot2 needs @@ -423,20 +428,12 @@ That's tracked in [TCC.md](TCC.md), not here. ### cc.scm bugs surfaced by Phase A -All worked around at link time (boot-libc-prepend.sh) or via patches. -Migrate into cc.scm as that becomes the cleanest place to fix them. - -- **String literal padding.** cc.scm emits `'<hex>'` literals at their - natural byte length. If a string's count isn't a multiple of 4, every - label that follows lands at a non-4-aligned address; aarch64 BLR - SIGBUSes. Workaround: link-time pad to 8 bytes. -- **Per-TU label namespacing.** cc.scm restarts `cc__str_N` at 0 per - TU. Linking two cc.scm outputs gives duplicate `:cc__str_0..N`; - hex2 first-def-wins silently. Workaround: rename library TU's - `cc__str_` to `libc__cc__str_` at link time. -- **Library mode missing.** cc.scm always emits `# entry stub` + - `%fn(p1_main, 16, { %call(&main) })` + trailing `:ELF_end`. Library - TUs must not own those. Workaround: strip them at link time. +The four link-composition issues — string padding, per-TU label +namespacing, missing library mode, missing :ELF_end suppression — +are now fixed in cc.scm itself (string padding is unconditional in +`%cg-bv->hex-lines`; the other three are gated on the `--lib=PFX` +flag, see §Linking). Remaining issues: + - **Empty-arg-list redecl rejection.** mes headers' K&R-style `f();` followed by a typed definition fails with `redecl: type mismatch`. Workaround: vendored-header patches that prototype the offenders diff --git a/scripts/boot-build-cc.sh b/scripts/boot-build-cc.sh @@ -9,6 +9,15 @@ ## Env: ARCH=aarch64|amd64|riscv64 ## CC_DEBUG=1 (optional) — pass --cc-debug to cc.scm so it prints ## per-phase heap usage on stderr. +## CC_LIB=PFX (optional) — compile in library mode (cc.scm +## --lib=PFX). Skips cc.scm's auto-emitted entry +## stub and trailing :ELF_end so the output catm's +## into a larger link, and namespaces anonymous +## string labels as PFX+"cc__str_N" to avoid +## collisions with other cc.scm outputs in the +## same chain. The catm caller then prepends +## P1/entry-{plain,libc}.P1pp and appends +## P1/elf-end.P1pp exactly once each. ## Usage: boot-build-cc.sh <src.c> <out.P1pp> set -eu @@ -28,8 +37,10 @@ CC_SRC=build/$ARCH/cc/cc.scm mkdir -p "$(dirname "$OUT")" -if [ "${CC_DEBUG:-0}" = "1" ]; then - "$SCHEME1_BIN" "$CC_SRC" --cc-debug "$SRC" "$OUT" -else - "$SCHEME1_BIN" "$CC_SRC" "$SRC" "$OUT" -fi +# Build cc-flag list once. Order doesn't matter to cc-main but +# stays stable for log readability. +set -- +[ "${CC_DEBUG:-0}" = "1" ] && set -- "$@" --cc-debug +[ -n "${CC_LIB:-}" ] && set -- "$@" "--lib=$CC_LIB" + +"$SCHEME1_BIN" "$CC_SRC" "$@" "$SRC" "$OUT" diff --git a/scripts/boot-build-p1pp.sh b/scripts/boot-build-p1pp.sh @@ -6,11 +6,11 @@ ## ELF binary (build/$ARCH/m1pp, built by boot2.sh / boot-build-p1.sh). ## ## Pipeline: -## cat <P1-$ARCH.M1pp> <P1.M1pp> <P1pp.P1pp> <src> -> /tmp/combined.M1pp -## m1pp /tmp/combined.M1pp -> /tmp/expanded.M1 -## M0 /tmp/expanded.M1 -> /tmp/prog.hex2 -## catm /tmp/elf.hex2 /tmp/prog.hex2 -> /tmp/linked.hex2 -## hex2-0 /tmp/linked.hex2 -> $OUT +## cat <P1-$ARCH.M1pp> <P1.M1pp> <P1pp.P1pp> <srcs...> -> /tmp/combined.M1pp +## m1pp /tmp/combined.M1pp -> /tmp/expanded.M1 +## M0 /tmp/expanded.M1 -> /tmp/prog.hex2 +## catm /tmp/elf.hex2 /tmp/prog.hex2 -> /tmp/linked.hex2 +## hex2-0 /tmp/linked.hex2 -> $OUT ## ## libp1pp (P1/P1pp.P1pp) is concatenated unconditionally so portable ## sources can use %fn, the control-flow macros, and libp1pp routines @@ -19,11 +19,19 @@ ## programs that don't reference any libp1pp routine still pay a fixed ## code-size tax (~a few KB). ## +## Multiple <srcs> are concatenated in the order given. This is how +## libc-using executables compose: a typical chain is +## P1/entry-libc.P1pp build/$ARCH/libc.P1pp client.P1pp P1/elf-end.P1pp +## with libc.P1pp / client.P1pp produced by cc.scm --lib=PFX so they +## omit the entry stub and trailing :ELF_end (those come from the +## fixed fragments instead). For a single-TU exec, pass exactly one +## source built without --lib= and the fragments are unnecessary. +## ## Env: ARCH=aarch64|amd64|riscv64 ## P1PP_TRACE=1 — print a one-line marker (phase, in/out path, size) ## before each pipeline stage so the failing tool is ## obvious. Off by default to keep test runs quiet. -## Usage: boot-build-p1pp.sh <src> <out> +## Usage: boot-build-p1pp.sh <out> <srcs...> set -eu @@ -35,10 +43,10 @@ trace() { } : "${ARCH:?ARCH must be set}" -[ "$#" -eq 2 ] || { echo "usage: ARCH=<arch> $0 <src> <out>" >&2; exit 2; } +[ "$#" -ge 2 ] || { echo "usage: ARCH=<arch> $0 <out> <srcs...>" >&2; exit 2; } -SRC=$1 -OUT=$2 +OUT=$1 +shift BACKEND=P1/P1-$ARCH.M1pp FRONTEND=P1/P1.M1pp @@ -46,11 +54,11 @@ LIBP1PP=P1/P1pp.P1pp ELF_HDR=vendor/seed/$ARCH/ELF.hex2 TOOLS=build/$ARCH/tools M1PP_BIN=build/$ARCH/m1pp -NAME=$(basename "$SRC" .P1pp) +NAME=$(basename "$OUT") WORK=build/$ARCH/.work/$NAME mkdir -p "$WORK" "$(dirname "$OUT")" -cat "$BACKEND" "$FRONTEND" "$LIBP1PP" "$SRC" > /tmp/combined.M1pp +cat "$BACKEND" "$FRONTEND" "$LIBP1PP" "$@" > /tmp/combined.M1pp trace "cat: combined" /tmp/combined.M1pp "$M1PP_BIN" /tmp/combined.M1pp /tmp/expanded.M1 trace "m1pp: expanded" /tmp/expanded.M1 diff --git a/scripts/boot-libc-prepend.sh b/scripts/boot-libc-prepend.sh @@ -1,98 +0,0 @@ -#!/bin/sh -## boot-libc-prepend.sh — produce a single .P1pp suitable for -## scripts/boot-build-p1pp.sh by stripping cc.scm's standard -## executable tail from libc.P1pp and prepending the result to a -## client (executable) TU. -## -## Per the scripts/ convention (boot-*.sh always runs in the minimal -## container) callers are: the cc-libc suite handler in -## boot-run-tests.sh (already in container), and the tcc-boot2 link -## rule in the Makefile (which wraps with `$(call PODMAN,…)`). The -## transforms are pure awk + sh — they happen to work on the host too, -## but the convention is what's load-bearing. -## -## Used by both call sites so the link-time invariants (which symbols -## collide between two cc.scm outputs, what alignment cc.scm doesn't -## enforce, what startup wiring our :_start expects) live in exactly -## one place. -## -## Transforms applied to libc.P1pp: -## 1. Drop cc.scm's auto-emitted exec tail — -## `# entry stub: forwards argc=a0, argv=a1 to main` -## `%fn(p1_main, 16, { %call(&main) })`. Library TUs must not -## define :p1_main; the executable TU is the only owner. -## 2. Drop the trailing `:ELF_end`. ELF.hex2 sizes p_filesz from -## the first :ELF_end it sees; only the executable TU may emit it. -## 3. Rename internal-linkage `cc__str_N` (anonymous string literals) -## to `libc__cc__str_N`. cc.scm restarts that counter at 0 per TU, -## so libc and the client otherwise duplicate :cc__str_0..N. hex2 -## resolves duplicates first-def-wins and silently mis-binds refs. -## 4. Pad each `'<hex>'` literal up to an 8-byte boundary. cc.scm -## emits strings at their natural length; if a string's byte -## count is not a multiple of 4, every label that follows lands -## at a non-4-aligned address and aarch64 BLR SIGBUSes. -## -## Then we append a fixed :p1_main wrapper that calls __libc_init -## (lispcc-syscall.c — populates `environ` from argv's NULL terminator) -## before forwarding to the client's :main. -## -## Same `# entry stub` strip is applied to the client TU so its own -## auto-:p1_main doesn't collide with our wrapper. -## -## Usage: boot-libc-prepend.sh <libc.P1pp> <client.P1pp> <out.P1pp> - -set -eu -[ "$#" -eq 3 ] || { - echo "usage: $0 <libc.P1pp> <client.P1pp> <out.P1pp>" >&2; exit 2 -} - -LIBC=$1 -CLIENT=$2 -OUT=$3 - -[ -r "$LIBC" ] || { echo "missing $LIBC" >&2; exit 1; } -[ -r "$CLIENT" ] || { echo "missing $CLIENT" >&2; exit 1; } - -WORK=$(dirname "$OUT") -mkdir -p "$WORK" - -LIBC_LIB=$WORK/libc.lib.P1pp -CLIENT_NOM=$WORK/client.no-p1main.P1pp - -awk ' - /^# entry stub: forwards argc=a0, argv=a1 to main$/ { skip=1; next } - skip && /^%fn\(p1_main,/ { next } - skip && /^%call\(&main\)/ { next } - skip && /^}\)$/ { skip=0; next } - /^:ELF_end[ \t]*$/ { next } - { gsub(/cc__str_/, "libc__cc__str_") } - /^'\''[0-9A-Fa-f]+'\''$/ { - print - s = $0; gsub(/'\''/, "", s) - bytes = length(s) / 2 - pad = (8 - bytes % 8) % 8 - if (pad > 0) { - pstr = "" - for (i = 0; i < pad; i++) pstr = pstr "00" - printf "'\''%s'\''\n", pstr - } - next - } - { print } -' "$LIBC" > "$LIBC_LIB" - -printf '%s\n' \ - '%fn(p1_main, 16, {' \ - ' %call(&__libc_init)' \ - ' %call(&main)' \ - '})' >> "$LIBC_LIB" - -awk ' - /^# entry stub: forwards argc=a0, argv=a1 to main$/ { skip=1; next } - skip && /^%fn\(p1_main,/ { next } - skip && /^%call\(&main\)/ { next } - skip && /^}\)$/ { skip=0; next } - { print } -' "$CLIENT" > "$CLIENT_NOM" - -cat "$LIBC_LIB" "$CLIENT_NOM" > "$OUT" diff --git a/scripts/boot-run-tests.sh b/scripts/boot-run-tests.sh @@ -144,7 +144,7 @@ run_p1_suite() { bin=build/$ARCH/p1-tests/$name log=$bin.build.log mkdir -p "$(dirname "$bin")" - if ! sh scripts/boot-build-p1pp.sh "$fixture" "$bin" \ + if ! sh scripts/boot-build-p1pp.sh "$bin" "$fixture" \ >"$log" 2>&1; then fail "$label" "" "$log" continue @@ -363,7 +363,7 @@ _cc_runtime_suite() { fi p1pp_log=$outdir/p1pp.log - if ! sh scripts/boot-build-p1pp.sh "$p1pp" "$elf" \ + if ! sh scripts/boot-build-p1pp.sh "$elf" "$p1pp" \ >"$p1pp_log" 2>&1; then fail "$label" "P1pp assemble failed:" "$p1pp_log" continue @@ -414,7 +414,7 @@ run_cc_suite() { fi p1pp_log=$outdir/p1pp.log - if ! sh scripts/boot-build-p1pp.sh "$p1pp" "$elf" \ + if ! sh scripts/boot-build-p1pp.sh "$elf" "$p1pp" \ >"$p1pp_log" 2>&1; then fail "$label" "P1pp assemble failed:" "$p1pp_log" continue @@ -454,28 +454,29 @@ run_cc_libc_suite() { fi outdir=build/$ARCH/cc-libc/$name client_p1pp=$outdir/$name.client.P1pp - merged_p1pp=$outdir/$name.P1pp elf=$outdir/$name label="[$ARCH] cc-libc/$name" mkdir -p "$outdir" + # Compile the client TU in lib mode so it doesn't emit its + # own :p1_main / :ELF_end and namespaces its anonymous string + # labels under app__cc__str_N — distinct from libc.P1pp's + # libc__cc__str_N. cc_log=$outdir/cc.log - if ! build/$ARCH/scheme1 build/$ARCH/cc/cc.scm "$src" "$client_p1pp" \ + if ! build/$ARCH/scheme1 build/$ARCH/cc/cc.scm \ + --lib=app__ "$src" "$client_p1pp" \ >"$cc_log" 2>&1; then fail "$label" "cc compile failed:" "$cc_log" continue fi - prepend_log=$outdir/prepend.log - if ! sh scripts/boot-libc-prepend.sh \ - build/$ARCH/libc.P1pp "$client_p1pp" "$merged_p1pp" \ - >"$prepend_log" 2>&1; then - fail "$label" "libc-prepend failed:" "$prepend_log" - continue - fi - + # catm chain: entry-libc supplies :p1_main (calls __libc_init + # then main), libc.P1pp supplies the libc routines, the client + # supplies :main, elf-end supplies the :ELF_end terminator. p1pp_log=$outdir/p1pp.log - if ! sh scripts/boot-build-p1pp.sh "$merged_p1pp" "$elf" \ + if ! sh scripts/boot-build-p1pp.sh "$elf" \ + P1/entry-libc.P1pp build/$ARCH/libc.P1pp \ + "$client_p1pp" P1/elf-end.P1pp \ >"$p1pp_log" 2>&1; then fail "$label" "P1pp assemble failed:" "$p1pp_log" continue diff --git a/tests/cc-libc/00-exit.c b/tests/cc-libc/00-exit.c @@ -1,5 +1,5 @@ /* Sanity: bare main returning a value. Doesn't touch libc — confirms - * the cc-libc harness (cc.scm + boot-libc-prepend + boot-build-p1pp) + * the cc-libc harness (cc.scm --lib=app__ → catm chain → boot-build-p1pp) * produces a runnable ELF when nothing in libc is exercised. */ int main (void)