commit feb396e0fd9c03228accaafe31af0b6f1c8d6a7b
parent d5c7cfe06d456bb39988dacd4b24f1d394385f2c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 10 Jun 2026 10:08:13 -0700
perf(cg): kill O(n^2) zero-init + const-tracker blowup that hung debug bootstrap
`make bootstrap-debug` hung for minutes compiling src/api/package.c. Root
cause: a ~28 KB all-char DistPackageManifest local under
-ftrivial-auto-var-init=zero. Two compounding O(n^2) issues, amplified ~20x
by the debug build's ASAN instrumentation:
1. zero_init_at lowered the implicit zero-init to one scalar store per leaf
byte (~28k stores). Each non-zero-offset store stashes a fresh frontend
temp (pcg_store -> pcg_local -> g->nlocals++), so g->nlocals grows
linearly with stores emitted.
2. api_local_const_clear_all (hit on every store/call/branch/address-of via
the memory/control/address-taken boundaries, ~40 call sites) scanned all
g->nlocals. With (1) feeding the growth, the whole thing was O(n^2) in
leaf count.
Fix A: -ftrivial-auto-var-init=zero now zeroes the whole object with a
single kit_cg_memset (zero_object_bytes_at) instead of recursing to per-leaf
stores. All-zero bytes is the zero value of every C type on supported
targets, and it is what gcc/clang emit. Collapses ~28k stores to one.
Fix C: thread const-tracked locals onto an intrusive active list
(KitCg.const_head / ApiSourceLocal.const_next+const_listed) so the
const-tracker's boundary ops clear in O(#tracked) instead of O(nlocals).
Mirrors the NativeDirectTarget cache_head/tail precedent. Removes the O(n^2)
for all functions, not just zero-init. Pure no-op behavior change (clearing
an already-invalid local was always a no-op).
package.c: minutes -> 1.1s (ASAN). 28 KB manifest synthetic: 15.8s -> 0.06s.
No regressions: parse 3880/128, cg-api/opt suites, toy 1392 all green;
const-forwarding still correct (forward, invalidate-at-boundary, re-store)
at -O0/-O1.
Diffstat:
12 files changed, 57 insertions(+), 754 deletions(-)
diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c
@@ -928,7 +928,14 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) {
* variable is fully written before any read the store is dead and the
* optimizer drops it. VLAs and incomplete arrays are left alone. */
pcg_set_loc(p, loc);
- zero_init_at(p, s, var_ty, 0, var_ty);
+ /* Zero the whole object in one memset rather than recursing to one store
+ * per scalar leaf. The per-leaf path is O(leaves^2) on large aggregates
+ * (each leaf store stashes a fresh frontend temp, growing g->nlocals,
+ * which api_local_const_address_taken then re-scans on every store) and
+ * explodes under ASAN — e.g. a 28 KB manifest local in src/api/package.c
+ * stalled the debug bootstrap for minutes. memset is also what gcc/clang
+ * emit for -ftrivial-auto-var-init=zero. */
+ zero_object_bytes_at(p, s, var_ty, 0, var_ty);
}
}
}
diff --git a/lang/c/parse/parse_init.c b/lang/c/parse/parse_init.c
@@ -175,8 +175,8 @@ void push_subobject_lv(Parser* p, FrameSlot slot, const Type* arr_ty,
pcg_lv_member(p, (i64)offset, elem_ty, /*bf_off=*/0, /*bf_w=*/0, /*ss=*/0);
}
-static void zero_object_bytes_at(Parser* p, FrameSlot slot, const Type* arr_ty,
- u32 offset, const Type* ty) {
+void zero_object_bytes_at(Parser* p, FrameSlot slot, const Type* arr_ty,
+ u32 offset, const Type* ty) {
KitCgMemAccess access = pcg_mem(p, ty);
push_subobject_lv(p, slot, arr_ty, offset, ty);
pcg_addr(p);
diff --git a/lang/c/parse/parse_priv.h b/lang/c/parse/parse_priv.h
@@ -563,6 +563,12 @@ void init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset,
const Type* ty);
void zero_init_at(Parser* p, FrameSlot slot, const Type* arr_ty, u32 offset,
const Type* ty);
+/* Zero a whole sub-object with a single kit_cg_memset, instead of recursing to
+ * one scalar store per leaf. Correct for any type (all-zero bytes is the zero
+ * value of every C type on supported targets) and avoids O(leaves^2) blowup on
+ * large aggregates. */
+void zero_object_bytes_at(Parser* p, FrameSlot slot, const Type* arr_ty,
+ u32 offset, const Type* ty);
void parse_static_init_at(Parser* p, u8* buf, u32 buflen, u32 offset,
const Type* ty);
void define_static_object(Parser* p, ObjSymId sym, ObjSecId section_id,
diff --git a/src/cg/fold.c b/src/cg/fold.c
@@ -554,8 +554,26 @@ void api_local_const_clear(ApiSourceLocal* rec) {
}
void api_local_const_clear_all(KitCg* g) {
+ KitCgLocal cur;
if (!g) return;
- for (u32 i = 0; i < g->nlocals; ++i) api_local_const_clear(&g->locals[i]);
+ /* Walk only the locals threaded onto the const-active list, not all nlocals:
+ * clearing an already-invalid local is a no-op, so this set is exactly the
+ * one worth visiting. Boundary ops (memory/control/address-taken) are hit on
+ * every call/branch/store, so this O(#tracked) vs O(nlocals) is what keeps a
+ * function with many locals (e.g. a large zeroed aggregate) out of O(n^2). */
+ cur = g->const_head;
+ while (cur != KIT_CG_LOCAL_NONE) {
+ ApiSourceLocal* rec = api_local_from_handle(g, cur);
+ KitCgLocal next = rec ? rec->const_next : KIT_CG_LOCAL_NONE;
+ if (rec) {
+ rec->const_valid = 0;
+ rec->const_value = 0;
+ rec->const_listed = 0;
+ rec->const_next = KIT_CG_LOCAL_NONE;
+ }
+ cur = next;
+ }
+ g->const_head = KIT_CG_LOCAL_NONE;
}
void api_local_const_memory_boundary(KitCg* g) { api_local_const_clear_all(g); }
@@ -604,6 +622,14 @@ void api_local_const_store(KitCg* g, KitCgLocal local, KitCgMemAccess access,
}
rec->const_value = api_fold_result(g->c, ty, (u64)value, width);
rec->const_valid = 1;
+ /* Thread onto the const-active list so the next boundary can find and clear
+ * it without scanning all nlocals. A local already listed (possibly cleared
+ * since) stays linked once; membership is independent of const_valid. */
+ if (!rec->const_listed) {
+ rec->const_next = g->const_head;
+ g->const_head = local;
+ rec->const_listed = 1;
+ }
}
int api_local_const_load(KitCg* g, KitCgLocal local, KitCgMemAccess access,
diff --git a/src/cg/internal.h b/src/cg/internal.h
@@ -123,10 +123,16 @@ typedef struct ApiSourceLocal {
CGLocalDesc desc;
CGLocal storage;
i64 const_value;
+ /* Intrusive link for the const-active list (KitCg.const_head; see fold.c).
+ * KIT_CG_LOCAL_NONE terminates. const_listed marks membership so a re-store
+ * does not double-link; it stays set (the entry lingers as a cleared no-op)
+ * until the next boundary drains the list. */
+ KitCgLocal const_next;
u32 param_index;
u8 kind;
u8 const_valid;
- u8 pad[2];
+ u8 const_listed;
+ u8 pad[1];
} ApiSourceLocal;
struct KitCg {
@@ -151,6 +157,11 @@ struct KitCg {
ApiSourceLocal* locals;
u32 nlocals;
u32 locals_cap;
+ /* Head of the const-active list threaded through ApiSourceLocal.const_next:
+ * the locals that currently hold (or recently held, pending a boundary) a
+ * forwardable constant. Lets the const-tracker clear in O(#tracked) rather
+ * than scanning all nlocals on every store/call/branch/address-of. */
+ KitCgLocal const_head;
KitCgTypeId fn_ret_type; /* KIT_CG_TYPE_NONE/void == no result */
SrcLoc cur_loc;
diff --git a/src/cg/session.c b/src/cg/session.c
@@ -46,6 +46,7 @@ static void cg_free_obj_state(KitCg* g) {
g->sp = 0;
g->cap = 0;
g->nlocals = 0;
+ g->const_head = KIT_CG_LOCAL_NONE;
g->locals_cap = 0;
g->sym_cap = 0;
g->fn_ret_type = 0;
@@ -399,6 +400,7 @@ void kit_cg_func_begin_attrs(KitCg* g, KitCgSym cg_sym,
g->fn_ret_type = cg_type_func_ret_id(c, fty);
g->nlocals = 0;
+ g->const_head = KIT_CG_LOCAL_NONE;
g->sp = 0;
if (g->debug) {
diff --git a/test/hosted/cases/hello.c b/test/cross/cases/hello.c
diff --git a/test/hosted/cases/hello.expected b/test/cross/cases/hello.expected
diff --git a/test/hosted/cases/hello.stdout b/test/cross/cases/hello.stdout
diff --git a/test/hosted/run.sh b/test/hosted/run.sh
@@ -1,143 +0,0 @@
-#!/usr/bin/env bash
-# test/hosted/run.sh — the hosted test suite: build each C case for every
-# (target, link-mode) config in the support set with scripts/hosted.sh, run it
-# through the shared seam test/lib/exec_target.sh, and check exit code + stdout
-# against the oracle (<name>.expected / <name>.stdout). The first principled
-# cross-OS hosted-exec suite; seed case is cases/hello.c.
-#
-# Full matrix (15 configs):
-# linux {aa64,x64,rv64} x {musl-static, musl-dynamic, glibc} (podman: alpine
-# for musl, debian for glibc; each routed by its exec tag)
-# macos-aarch64 (native)
-# windows {x64,aarch64} (VM)
-# freebsd {amd64,aarch64,riscv64} (VM)
-#
-# Two verdicts per (case, config): ":build" (cc+link ok) and ":run" (right exit
-# code + stdout). A target whose sysroot is absent SKIPs build; a config with no
-# runner here SKIPs run. The tag carries the libc, so one flush routes every
-# config to its runner.
-#
-# Default config set: Linux + macOS (fast). FreeBSD + Windows (VMs) are added by
-# KIT_HOSTED_VM=1. env: KIT, HOSTED_CONFIGS (override the list), KIT_HOSTED_VM,
-# EXEC_VM_KEEP_UP.
-
-set -u
-
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-KIT="${KIT:-$ROOT/build/kit}"
-HOSTED="$ROOT/scripts/hosted.sh"
-CASES="$ROOT/test/hosted/cases"
-BUILD_DIR="$ROOT/build/test/hosted"
-
-# shellcheck source=../lib/kit_sh_report.sh
-. "$ROOT/test/lib/kit_sh_report.sh"
-# shellcheck source=../lib/exec_target.sh
-. "$ROOT/test/lib/exec_target.sh"
-kit_report_init
-trap exec_target_teardown_all EXIT
-
-[ -x "$KIT" ] || { echo "hosted: kit not found at $KIT (run 'make bin')" >&2; exit 2; }
-
-# exec_target's caller contract for the linux/macos runners (VM tags ignore these).
-have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1
-QEMU_BIN="${QEMU_BIN:-$(command -v qemu-aarch64 2>/dev/null || true)}"
-QEMU_RV64_BIN="${QEMU_RV64_BIN:-$(command -v qemu-riscv64 2>/dev/null || true)}"
-have_qemu=0; [ -n "$QEMU_BIN" ] && have_qemu=1
-case "$(uname -m 2>/dev/null)" in aarch64|arm64) is_aarch64=1 ;; *) is_aarch64=0 ;; esac
-export have_podman QEMU_BIN QEMU_RV64_BIN have_qemu is_aarch64
-mkdir -p "$BUILD_DIR"
-EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR"; export EXEC_TARGET_MOUNT_ROOT
-
-# ---- config list -----------------------------------------------------------
-# A config is "<target>[:<mode>]". mode (static|dynamic) only varies for musl;
-# glibc is dynamic-only, freebsd is static, windows/macos have one shape.
-LINUX_CONFIGS=""
-for a in aa64 x64 rv64; do
- LINUX_CONFIGS="$LINUX_CONFIGS linux-musl-$a:static linux-musl-$a:dynamic linux-glibc-$a"
-done
-DEFAULT_CONFIGS="$LINUX_CONFIGS macos-aarch64"
-VM_CONFIGS="freebsd-amd64 freebsd-aarch64 freebsd-riscv64 windows-x64 windows-aarch64"
-if [ -n "${HOSTED_CONFIGS:-}" ]; then
- CONFIGS="$HOSTED_CONFIGS"
-else
- CONFIGS="$DEFAULT_CONFIGS"
- [ "${KIT_HOSTED_VM:-0}" = 1 ] && CONFIGS="$CONFIGS $VM_CONFIGS"
-fi
-
-# Link flags for a (target, mode): musl honors the mode; freebsd is self-contained
-# static; glibc/windows/macos use their default shape (glibc is dynamic-only).
-link_flags_for() {
- local target="$1" mode="$2"
- case "$target" in
- linux-musl-*) [ "$mode" = static ] && echo -static ;;
- freebsd-*) echo -static ;;
- *) echo ;;
- esac
-}
-
-# ---- build + queue ---------------------------------------------------------
-H_NAME=(); H_OUT=(); H_RC=(); H_EXP=(); H_STDOUT=()
-
-printf 'hosted: configs=%s\n' "$CONFIGS"
-
-for case_src in "$CASES"/*.c; do
- cbase="$(basename "${case_src%.c}")"
- exp=0; [ -f "${case_src%.c}.expected" ] && exp="$(cat "${case_src%.c}.expected")"
- want_out=""; [ -f "${case_src%.c}.stdout" ] && want_out="$(cat "${case_src%.c}.stdout")"
- for config in $CONFIGS; do
- target="${config%%:*}"; mode="${config#*:}"; [ "$mode" = "$config" ] && mode=""
- os="${target%%-*}"
- label="$cbase/$config"
- sr="$("$HOSTED" path "$target" 2>/dev/null)"
- if [ "$os" != macos ] && { [ -z "$sr" ] || [ ! -d "$sr" ]; }; then
- kit_skip "$label:build" "missing sysroot (scripts/hosted.sh prepare $target)"
- continue
- fi
- cdir="$BUILD_DIR/$(printf '%s' "$config" | tr ':/' '__')"; mkdir -p "$cdir"
- ext=""; [ "$os" = windows ] && ext=".exe"
- exe="$cdir/$cbase$ext"
- # shellcheck disable=SC2046
- if ! "$HOSTED" cc "$target" $(link_flags_for "$target" "$mode") "$case_src" -o "$exe" \
- > "$cdir/$cbase.cc.out" 2> "$cdir/$cbase.cc.err"; then
- kit_fail "$label:build" "hosted.sh cc failed"
- sed 's/^/ | /' "$cdir/$cbase.cc.err" | head -20
- continue
- fi
- kit_pass "$label:build"
-
- tag="$("$HOSTED" tag "$target")"
- if ! exec_target_supported "$tag"; then
- kit_skip "$label:run" "no runner for $tag"
- continue
- fi
- exec_target_queue "$tag" "$label" "$exe" \
- "$cdir/$cbase.out" "$cdir/$cbase.err" "$cdir/$cbase.rc"
- H_NAME+=("$label:run"); H_OUT+=("$cdir/$cbase.out")
- H_RC+=("$cdir/$cbase.rc"); H_EXP+=("$exp"); H_STDOUT+=("$want_out")
- done
-done
-
-# ---- execute + check -------------------------------------------------------
-exec_target_flush
-
-i=0; n="${#H_NAME[@]}"
-while [ "$i" -lt "$n" ]; do
- name="${H_NAME[$i]}"; exp=$(( ${H_EXP[$i]} & 255 ))
- rc="$(cat "${H_RC[$i]}" 2>/dev/null || echo 127)"
- got_out="$(cat "${H_OUT[$i]}" 2>/dev/null || true)"
- if ! case "$rc" in ''|*[!0-9-]*) false ;; *) true ;; esac; then
- kit_fail "$name" "did not run (rc=$rc)"
- elif [ "$(( rc & 255 ))" -ne "$exp" ]; then
- kit_fail "$name" "expected rc $exp, got $rc"
- elif [ -n "${H_STDOUT[$i]}" ] && [ "$got_out" != "${H_STDOUT[$i]}" ]; then
- kit_fail "$name" "stdout mismatch"
- printf ' want: %s\n got: %s\n' "${H_STDOUT[$i]}" "$got_out"
- else
- kit_pass "$name"
- fi
- i=$((i + 1))
-done
-
-KIT_SKIP_IS_FAILURE=0
-kit_summary test-hosted
-kit_exit
diff --git a/test/lib/exec_rv32_bare.sh b/test/lib/exec_rv32_bare.sh
@@ -1,213 +0,0 @@
-#!/usr/bin/env bash
-# test/lib/exec_rv32_bare.sh — shared bare-metal execution helper for the rv32
-# cross-test lane (path "V") of the corpus harnesses (test/toy/run.sh and
-# test/parse/run.sh).
-#
-# rv32 is a freestanding `-none-elf` target with no qemu-user / podman path
-# (unlike the aa64/x64/rv64 Linux cross lanes that go through exec_target.sh).
-# A corpus object — whose `main` returns an exit code — is instead linked with a
-# bare-metal startup that sets the stack, enables the FPU (ilp32f), calls main,
-# and reports its return through a SiFive test finisher, then run under
-# qemu-system-riscv32 -machine virt. The qemu exit code equals main's return
-# (0 -> 0x5555 -> qemu exit 0; N -> 0x3333|(N<<16) -> qemu exit N), so the
-# corpus's existing `rc == expected` oracle applies unchanged.
-#
-# The link uses `kit ld` (a freestanding rv32 target defaults to no-PIE and
-# auto-links no runtime, so the corpus + the kit runtime archive are supplied
-# explicitly), exercising the full kit toolchain end to end. The startup stub is
-# kit-assembled (`kit as`) too — kit's assembler now emits the `csrs` CSR pseudo
-# used to enable the FPU — so the whole image is built by kit.
-#
-# Public API (after sourcing):
-# rv32_bare_setup <workdir> populate RV32_BARE_OK (0/1) and cache
-# the startup/wrapper/linkscript + rt.
-# rv32_bare_run <obj> <work> <rcfile> link <obj> into a bootable image and
-# run it; write the qemu exit code to
-# <rcfile>. Echoes a one-line reason and
-# returns: 0 ran (rc in <rcfile>),
-# 2 link/build failure (caller decides).
-
-# shellcheck disable=SC2034 # RV32_BARE_* are consumed by the sourcing harness.
-
-_rv32_bare_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
-# shellcheck source=check_rv32_env.sh
-. "$_rv32_bare_root/test/lib/check_rv32_env.sh"
-
-RV32_BARE_OK=0
-RV32_BARE_KIT="${KIT:-$_rv32_bare_root/build/kit}"
-# Entry symbol the wrapper calls and reports the exit code of. Toy cases define
-# `main`; the C parse corpus defines `test_main`. Set before rv32_bare_setup.
-RV32_BARE_ENTRY="${RV32_BARE_ENTRY:-main}"
-RV32_BARE_QEMU=""
-RV32_BARE_RT=""
-RV32_BARE_START=""
-RV32_BARE_WRAP=""
-RV32_BARE_LDS=""
-# ilp32f: hardware single float + soft double + i64 — the verified rv32 profile.
-RV32_BARE_MARCH="rv32imafc_zicsr_zifencei"
-RV32_BARE_MABI="ilp32f"
-
-rv32_bare_setup() {
- local work="$1"
- RV32_BARE_OK=0
- check_rv32_env >/dev/null 2>&1
- # The whole image (corpus, startup stub, wrapper, runtime archive, link) is
- # built by kit — `kit cc` / `kit as` / `kit ld` — and run under
- # qemu-system-riscv32, so the only external prerequisite is qemu. (clang is
- # not invoked anywhere on this path; it remains a gate only for the smoke
- # lane's ld.lld doctor in check_rv32_env.)
- if [ "${RV32_HAVE_QEMU_SYSTEM:-0}" -ne 1 ]; then
- return 0
- fi
- RV32_BARE_QEMU="${RV32_QEMU_SYSTEM_BIN:-qemu-system-riscv32}"
- [ -x "$RV32_BARE_KIT" ] || return 0
-
- # The freestanding runtime (i64 mul/div/shift + soft double helpers). Build it
- # on demand; without it, link of any corpus case touching those would fail.
- RV32_BARE_RT="$_rv32_bare_root/build/rt/riscv32-elf-hardfloat/libkit_rt.a"
- if [ ! -f "$RV32_BARE_RT" ]; then
- make -C "$_rv32_bare_root" rt-riscv32-elf-hardfloat >/dev/null 2>&1 || true
- fi
- [ -f "$RV32_BARE_RT" ] || return 0
-
- mkdir -p "$work"
- RV32_BARE_START="$work/_rv32_start.o"
- RV32_BARE_WRAP="$work/_rv32_wrap.o"
- RV32_BARE_LDS="$work/_rv32.lds"
-
- # Bare-metal reset entry: stack at the top of `virt` RAM, enable the FPU
- # (mstatus.FS=Initial) for ilp32f, then call the C wrapper.
- cat > "$work/_rv32_start.S" <<'EOF'
-.section .text.start,"ax",@progbits
-.globl _start
-_start:
- li sp, 0x80100000
- li t0, 0x2000
- csrs mstatus, t0 # mstatus.FS = Initial (enable the FPU for ilp32f)
-
- // ---- static thread-local storage setup -----------------------------------
- // Build the per-thread TLS image [TCB(16) | .tdata | .tbss] in RAM and point
- // tp at it. The corpus's TPREL relocs were resolved by kit-ld's scripted path
- // (which leaves img->tls_vaddr == 0) to tgt->vaddr + 16, so we must set
- // tp = __rv32_tls_block - __rv32_tdata_lma
- // making tp + (tgt->vaddr + 16) = __rv32_tls_block + 16 + off — the address
- // inside the copy where we place each variable. See the linker-script comment.
- la t0, __rv32_tls_block # t0 = block base (TCB at +0)
- addi t1, t0, 16 # t1 = dst = block + 16 (.tdata copy start)
- la t2, __rv32_tdata_lma # t2 = src = .tdata load image
- la t3, __rv32_tdata_size # t3 = .tdata byte count (abs symbol: la yields value)
-.Lcopy: # copy .tdata init image, byte at a time
- beqz t3, .Lcopy_done
- lbu t4, 0(t2)
- sb t4, 0(t1)
- addi t1, t1, 1
- addi t2, t2, 1
- addi t3, t3, -1
- j .Lcopy
-.Lcopy_done:
- la t3, __rv32_tbss_size # t3 = .tbss byte count
-.Lzero: # zero-fill .tbss (t1 already at end of .tdata copy)
- beqz t3, .Lzero_done
- sb zero, 0(t1)
- addi t1, t1, 1
- addi t3, t3, -1
- j .Lzero
-.Lzero_done:
- la t2, __rv32_tdata_lma
- sub tp, t0, t2 # tp = block - __rv32_tdata_lma
- // ---------------------------------------------------------------------------
-
- call _rv32_cmain
-.Lhang: j .Lhang
-
-// Per-thread TLS image scratch (single-threaded harness). Sized generously;
-// the corpus TLS cases use only a handful of bytes.
-.section .bss.rv32tls,"aw",@nobits
-.balign 16
-__rv32_tls_block:
- .zero 4096
-EOF
- # The wrapper calls the corpus's main() and maps its return onto the SiFive
- # test finisher at 0x100000. Compiled by kit (exercises rv32 codegen for the
- # finisher store + the call); main may return i32 or i64 — the low word is the
- # exit code.
- cat > "$work/_rv32_wrap.c" <<EOF
-#define FINISHER ((volatile unsigned int*)0x100000)
-extern int ${RV32_BARE_ENTRY}(void);
-__attribute__((noreturn)) void _rv32_cmain(void) {
- int code = ${RV32_BARE_ENTRY}();
- *FINISHER = code ? (0x3333u | ((unsigned)code << 16)) : 0x5555u;
- for (;;) {}
-}
-EOF
- # The TLS output sections below give the bare stub a static-TLS image to
- # seed from. kit-ld's *scripted* layout does NOT populate img->tls_vaddr (that
- # only happens in the bucketed, scriptless path), so the linker's own
- # __tdata_start/__tbss_size boundary symbols and PT_TLS are emitted as zero
- # here — and the corpus's TPREL relocs resolve to (tgt->vaddr - 0) + 16 =
- # tgt->vaddr + 16, i.e. the *placed vaddr* of the variable plus the 16-byte TCB
- # bias (src/obj/elf/link.c:475). We therefore can't lean on the linker's TLS
- # symbols; instead we define our own (__rv32_tdata_lma/_tdata_size/_tbss_size)
- # that the linker never clobbers, and the stub computes the thread pointer so
- # that tp + (tgt->vaddr + 16) lands inside a live RAM copy of the image.
- #
- # .tdata (PROGBITS, loaded at its vaddr) supplies the init image; .tbss
- # (NOBITS) only contributes a size. They are placed contiguously so a single
- # tp bias works for both: with the copy laid out as [TCB(16) | .tdata | .tbss]
- # and .tdata copied to block+16, set tp = block - __rv32_tdata_lma. Then for
- # any var v: tp + (v.vaddr + 16) = (block - tdata_lma) + (tdata_lma + off) + 16
- # = block + 16 + off — exactly where we copied/zeroed v. The
- # linker's +16 and our +16 TCB reservation cancel, mirroring start.c's
- # [TCB | tdata | tbss] convention (test/link/harness/start.c:146-149).
- #
- # __x = . assignments inside a section body are all applied *before* that
- # section's inputs (link_layout.c:775), so the post-.tdata dot is captured in a
- # following input-less marker section; sizes are then derived at top level.
- cat > "$RV32_BARE_LDS" <<'EOF'
-ENTRY(_start)
-SECTIONS {
- . = 0x80000000;
- .text : { *(.text.start) *(.text*) }
- .rodata : { *(.rodata*) }
- .data : { *(.data*) }
- .tdata : { . = ALIGN(16); __rv32_tdata_lma = .; *(.tdata .tdata.*) }
- .tdata_end : { __rv32_tdata_end = .; }
- .tbss : { __rv32_tbss_start = .; *(.tbss .tbss.*) }
- .tbss_end : { __rv32_tbss_end = .; }
- .bss : { *(.bss*) *(COMMON) }
- __rv32_tdata_size = __rv32_tdata_end - __rv32_tdata_lma;
- __rv32_tbss_size = __rv32_tbss_end - __rv32_tbss_start;
- /DISCARD/ : { *(.riscv.attributes) *(.comment) }
-}
-EOF
- if ! "$RV32_BARE_KIT" as -target riscv32-none-elf -march="$RV32_BARE_MARCH" \
- -mabi="$RV32_BARE_MABI" -o "$RV32_BARE_START" "$work/_rv32_start.S" \
- >/dev/null 2>&1; then
- return 0
- fi
- if ! "$RV32_BARE_KIT" cc -target riscv32-none-elf -march="$RV32_BARE_MARCH" \
- -mabi="$RV32_BARE_MABI" -O1 -ffreestanding -c "$work/_rv32_wrap.c" \
- -o "$RV32_BARE_WRAP" >/dev/null 2>&1; then
- return 0
- fi
- RV32_BARE_OK=1
-}
-
-rv32_bare_run() { # <obj> <work> <rcfile>
- local obj="$1" work="$2" rcfile="$3"
- local elf="$work/$(basename "$obj").elf"
- local lderr="$work/$(basename "$obj").rv32ld.err"
- if [ "$RV32_BARE_OK" -ne 1 ]; then
- echo "rv32 bare-metal toolchain unavailable"; return 2
- fi
- if ! "$RV32_BARE_KIT" ld -T "$RV32_BARE_LDS" -e _start \
- "$RV32_BARE_START" "$RV32_BARE_WRAP" "$obj" "$RV32_BARE_RT" \
- -o "$elf" 2>"$lderr"; then
- echo "kit ld (rv32) failed: $(head -n1 "$lderr" 2>/dev/null)"; return 2
- fi
- local rc=0
- timeout 20 "$RV32_BARE_QEMU" -machine virt -bios none -kernel "$elf" \
- -nographic -no-reboot >/dev/null 2>&1 || rc=$?
- printf '%s' "$rc" > "$rcfile"
- return 0
-}
diff --git a/test/smoke/freestanding_system.sh b/test/smoke/freestanding_system.sh
@@ -1,393 +0,0 @@
-#!/usr/bin/env bash
-# test/smoke/freestanding_system.sh - qemu-system bare-metal smoke for
-# kit-compiled aarch64/x86_64/riscv64/riscv32 freestanding ELF images.
-#
-# Each lane builds the same small C payload with `kit cc -target *-none-elf`,
-# assembles an arch-specific reset stub with `kit as`, links a static image with
-# `kit ld -T`, then boots it under the matching qemu-system binary. This is a
-# whole-toolchain smoke: no clang, ld.lld, hosted libc, or qemu-user path is in
-# the image build.
-
-set -u
-
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
-BUILD_DIR="$ROOT/build/test/freestanding-system"
-mkdir -p "$BUILD_DIR"
-
-KIT_KIT_DIR="$ROOT/test/lib"
-# shellcheck source=../lib/kit_sh_kit.sh
-. "$ROOT/test/lib/kit_sh_kit.sh"
-kit_report_init
-KIT_SKIP_IS_FAILURE=1
-
-KIT="${KIT:-$ROOT/build/kit}"
-TIMEOUT="${TIMEOUT:-timeout}"
-if ! command -v "$TIMEOUT" >/dev/null 2>&1; then
- if command -v gtimeout >/dev/null 2>&1; then
- TIMEOUT="gtimeout"
- fi
-fi
-
-if [ ! -x "$KIT" ]; then
- skip_test "kit" "kit binary not found at $KIT"
- kit_summary test-smoke-freestanding-system
- kit_exit
-fi
-
-if ! command -v "$TIMEOUT" >/dev/null 2>&1; then
- skip_test "timeout" "timeout command unavailable"
- kit_summary test-smoke-freestanding-system
- kit_exit
-fi
-
-cat > "$BUILD_DIR/app.c" <<'EOF'
-static int calc(void) {
- volatile unsigned long long a = 0x1122334455667788ull;
- volatile unsigned long long b = 0x0102030405060708ull;
- unsigned long long c = (a ^ b) + 0x10ull;
- if (c != ((0x1122334455667788ull ^ 0x0102030405060708ull) + 0x10ull))
- return 2;
-
- int sum = 0;
- for (int i = 1; i <= 10; ++i) sum += i;
- if (sum != 55) return 3;
-
- volatile long long sx = -7;
- if ((sx >> 1) != -4) return 4;
-
- unsigned x = 0x12345678u;
- x = (x << 3) | (x >> 29);
- if (x != 0x91a2b3c0u) return 5;
-
- return 0;
-}
-
-int cmain(void) { return calc(); }
-EOF
-
-have_qemu() {
- command -v "$1" >/dev/null 2>&1
-}
-
-compile_obj() { # <arch-label> <target> <extra...>
- local label="$1" target="$2"
- shift 2
- local dir="$BUILD_DIR/$label"
- mkdir -p "$dir"
- "$KIT" cc -target "$target" "$@" -O1 -ffreestanding -c "$BUILD_DIR/app.c" \
- -o "$dir/app.o" 2>"$dir/cc.err"
-}
-
-assemble_obj() { # <label> <target> <src> <extra...>
- local label="$1" target="$2" src="$3"
- shift 3
- "$KIT" as -target "$target" "$@" -o "$BUILD_DIR/$label/start.o" "$src" \
- 2>"$BUILD_DIR/$label/as.err"
-}
-
-link_elf() { # <label>
- local label="$1" dir="$BUILD_DIR/$1"
- "$KIT" ld -T "$dir/link.ld" -e _start "$dir/start.o" "$dir/app.o" \
- -o "$dir/kernel.elf" 2>"$dir/ld.err"
-}
-
-record_build_fail() { # <name> <log>
- local name="$1" log="$2"
- if [ -s "$log" ]; then
- not_ok "$name" "$log"
- else
- not_ok "$name" "command failed with no stderr"
- fi
-}
-
-run_aa64() {
- local label="aa64" dir="$BUILD_DIR/aa64"
- if ! have_qemu qemu-system-aarch64; then
- skip_test "$label" "qemu-system-aarch64 unavailable"
- return
- fi
- mkdir -p "$dir"
- cat > "$dir/start.S" <<'EOF'
-.section .text.start,"ax",@progbits
-.globl _start
-_start:
- adrp x0, stack_top
- add x0, x0, :lo12:stack_top
- mov sp, x0
- bl cmain
- adrp x1, semihost_args
- add x1, x1, :lo12:semihost_args
- str x0, [x1, #8]
- mov x0, #0x20
- hlt #0xf000
-.Lhang:
- b .Lhang
-
-.section .data.semihost,"aw",@progbits
-.balign 8
-semihost_args:
- .quad 0x20026
- .quad 0
-
-.section .bss.stack,"aw",@nobits
-.balign 16
-stack_bottom:
- .zero 65536
-stack_top:
-EOF
- cat > "$dir/link.ld" <<'EOF'
-ENTRY(_start)
-SECTIONS {
- . = 0x40080000;
- .text : { *(.text.start) *(.text*) }
- .rodata : { *(.rodata*) }
- .data : { *(.data*) }
- .bss : { *(.bss*) *(COMMON) }
- /DISCARD/ : { *(.comment) }
-}
-EOF
- if ! compile_obj "$label" aarch64-none-elf; then
- record_build_fail "$label cc" "$dir/cc.err"; return
- fi
- if ! assemble_obj "$label" aarch64-none-elf "$dir/start.S"; then
- record_build_fail "$label as" "$dir/as.err"; return
- fi
- if ! link_elf "$label"; then
- record_build_fail "$label ld" "$dir/ld.err"; return
- fi
-
- local rc=0
- "$TIMEOUT" 20 qemu-system-aarch64 -machine virt -cpu cortex-a53 \
- -kernel "$dir/kernel.elf" -display none -serial none -monitor none \
- -semihosting-config enable=on,target=native -no-reboot \
- >"$dir/qemu.out" 2>"$dir/qemu.err" || rc=$?
- if [ "$rc" -eq 0 ]; then
- ok "$label qemu-system (rc=0)"
- else
- printf 'expected qemu rc 0, got %s; see %s\n' "$rc" "$dir/qemu.err" \
- > "$dir/qemu.diag"
- not_ok "$label qemu-system" "$dir/qemu.diag"
- fi
-}
-
-run_riscv() { # <label> <qemu> <target> <march> <mabi> <stack>
- local label="$1" qemu="$2" target="$3" march="$4" mabi="$5" stack="$6"
- local dir="$BUILD_DIR/$label"
- if ! have_qemu "$qemu"; then
- skip_test "$label" "$qemu unavailable"
- return
- fi
- mkdir -p "$dir"
- cat > "$dir/start.S" <<EOF
-.section .text.start,"ax",@progbits
-.globl _start
-_start:
- li sp, $stack
- li t0, 0x2000
- csrs mstatus, t0
- call cmain
- li t0, 0x100000
- beqz a0, .Lpass
- slli a0, a0, 16
- li t1, 0x3333
- or a0, a0, t1
- sw a0, 0(t0)
-.Lhang:
- j .Lhang
-.Lpass:
- li t1, 0x5555
- sw t1, 0(t0)
- j .Lhang
-EOF
- cat > "$dir/link.ld" <<'EOF'
-ENTRY(_start)
-SECTIONS {
- . = 0x80000000;
- .text : { *(.text.start) *(.text*) }
- .rodata : { *(.rodata*) }
- .data : { *(.data*) }
- .bss : { *(.bss*) *(COMMON) }
- /DISCARD/ : { *(.riscv.attributes) *(.comment) }
-}
-EOF
- if ! compile_obj "$label" "$target" -march="$march" -mabi="$mabi"; then
- record_build_fail "$label cc" "$dir/cc.err"; return
- fi
- if ! assemble_obj "$label" "$target" "$dir/start.S" -march="$march" -mabi="$mabi"; then
- record_build_fail "$label as" "$dir/as.err"; return
- fi
- if ! link_elf "$label"; then
- record_build_fail "$label ld" "$dir/ld.err"; return
- fi
-
- local rc=0
- "$TIMEOUT" 20 "$qemu" -machine virt -bios none \
- -kernel "$dir/kernel.elf" -nographic -no-reboot \
- >"$dir/qemu.out" 2>"$dir/qemu.err" || rc=$?
- if [ "$rc" -eq 0 ]; then
- ok "$label qemu-system (rc=0)"
- else
- printf 'expected qemu rc 0, got %s; see %s\n' "$rc" "$dir/qemu.err" \
- > "$dir/qemu.diag"
- not_ok "$label qemu-system" "$dir/qemu.diag"
- fi
-}
-
-run_rv64() {
- run_riscv rv64 qemu-system-riscv64 riscv64-none-elf \
- rv64imafd_zicsr_zifencei lp64 0x81000000
-}
-
-run_rv32() {
- run_riscv rv32 qemu-system-riscv32 riscv32-none-elf \
- rv32imafc_zicsr_zifencei ilp32f 0x80100000
-}
-
-emit_x64_start() {
- local out="$1"
- cat > "$out" <<'EOF'
-.section .multiboot,"a",@progbits
-.balign 4
-.long 0x1badb002
-.long 0
-.long 0xe4524ffe
-
-.section .note.Xen,"a",@note
-.balign 4
-.long 4
-.long 4
-.long 18
-.ascii "Xen"
-.byte 0
-.long _start
-
-.section .text.start,"ax",@progbits
-.globl _start
-_start:
- .byte 0xfa /* cli */
- .byte 0xbc /* mov $stack_top, %esp */
- .long stack_top
- .byte 0xb8 /* mov $pml4, %eax */
- .long pml4
- .byte 0x0f, 0x22, 0xd8 /* mov %eax, %cr3 */
- .byte 0x0f, 0x20, 0xe0 /* mov %cr4, %eax */
- .byte 0x83, 0xc8, 0x20 /* or $CR4_PAE, %eax */
- .byte 0x0f, 0x22, 0xe0 /* mov %eax, %cr4 */
- .byte 0xb9, 0x80, 0x00, 0x00, 0xc0 /* mov $EFER, %ecx */
- .byte 0x0f, 0x32 /* rdmsr */
- .byte 0x0d, 0x00, 0x01, 0x00, 0x00 /* or $EFER_LME, %eax */
- .byte 0x0f, 0x30 /* wrmsr */
- .byte 0x0f, 0x20, 0xc0 /* mov %cr0, %eax */
- .byte 0x0d, 0x00, 0x00, 0x00, 0x80 /* or $CR0_PG, %eax */
- .byte 0x0f, 0x22, 0xc0 /* mov %eax, %cr0 */
- .byte 0x0f, 0x01, 0x15 /* lgdt gdt_desc */
- .long gdt_desc
- .byte 0xea /* ljmp $0x08,$long_entry */
- .long long_entry
- .hword 0x08
-
-long_entry:
- .byte 0x66, 0xb8, 0x10, 0x00 /* mov $0x10, %ax */
- .byte 0x8e, 0xd8 /* mov %ax, %ds */
- .byte 0x8e, 0xc0 /* mov %ax, %es */
- .byte 0x8e, 0xd0 /* mov %ax, %ss */
- .byte 0x48, 0xbc /* movabs $stack_top, %rsp */
- .quad stack_top
- call cmain
- .byte 0x66, 0xba, 0x01, 0x05 /* mov $0x501, %dx */
- .byte 0x66, 0xef /* outw %ax, %dx */
-.Lhang:
- .byte 0xf4 /* hlt */
- jmp .Lhang
-
-.section .data.boot,"aw",@progbits
-.balign 8
-gdt:
- .quad 0
- .quad 0x00af9a000000ffff
- .quad 0x00af92000000ffff
-gdt_desc:
- .hword 23
- .long gdt
-
-.balign 4096
-pml4:
- .quad pdpt + 0x3
-EOF
- for _ in $(seq 1 511); do printf ' .quad 0\n' >> "$out"; done
- cat >> "$out" <<'EOF'
-.balign 4096
-pdpt:
- .quad pd + 0x3
-EOF
- for _ in $(seq 1 511); do printf ' .quad 0\n' >> "$out"; done
- printf '.balign 4096\npd:\n' >> "$out"
- local i=0
- while [ "$i" -lt 512 ]; do
- printf ' .quad 0x%016x\n' $((i * 0x200000 + 0x83)) >> "$out"
- i=$((i + 1))
- done
- cat >> "$out" <<'EOF'
-
-.section .bss.stack,"aw",@nobits
-.balign 16
-stack_bottom:
- .zero 65536
-stack_top:
-EOF
-}
-
-run_x64() {
- local label="x64" dir="$BUILD_DIR/x64"
- if ! have_qemu qemu-system-x86_64; then
- skip_test "$label" "qemu-system-x86_64 unavailable"
- return
- fi
- mkdir -p "$dir"
- emit_x64_start "$dir/start.S"
- cat > "$dir/link.ld" <<'EOF'
-ENTRY(_start)
-SECTIONS {
- . = 0x100000;
- .multiboot : { *(.multiboot) }
- .note.Xen : { *(.note.Xen) }
- .text : { *(.text.start) *(.text*) }
- .rodata : { *(.rodata*) }
- .data.boot : ALIGN(4096) { *(.data.boot) }
- .data : { *(.data*) }
- .bss.stack : ALIGN(16) { *(.bss.stack) }
- .bss : { *(.bss*) *(COMMON) }
- /DISCARD/ : { *(.comment) }
-}
-EOF
- if ! compile_obj "$label" x86_64-none-elf; then
- record_build_fail "$label cc" "$dir/cc.err"; return
- fi
- if ! assemble_obj "$label" x86_64-none-elf "$dir/start.S"; then
- record_build_fail "$label as" "$dir/as.err"; return
- fi
- if ! link_elf "$label"; then
- record_build_fail "$label ld" "$dir/ld.err"; return
- fi
-
- local rc=0 want=1
- "$TIMEOUT" 20 qemu-system-x86_64 -kernel "$dir/kernel.elf" \
- -device isa-debug-exit,iobase=0x501,iosize=0x02 \
- -display none -serial none -monitor none -no-reboot \
- >"$dir/qemu.out" 2>"$dir/qemu.err" || rc=$?
- if [ "$rc" -eq "$want" ] && ! grep -q 'Error loading' "$dir/qemu.err"; then
- ok "$label qemu-system (guest rc=0)"
- else
- printf 'expected qemu rc %s (guest rc=0), got %s; see %s\n' \
- "$want" "$rc" "$dir/qemu.err" > "$dir/qemu.diag"
- not_ok "$label qemu-system" "$dir/qemu.diag"
- fi
-}
-
-run_aa64
-run_x64
-run_rv64
-run_rv32
-
-kit_summary test-smoke-freestanding-system
-kit_exit