kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 6405d2b7504c93c0c3b41a7ac8180c45691fc0f8
parent fc7bdd38228d702866ea143dd58bf85d06fa48c0
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 23 May 2026 17:37:07 -0700

test: add format-bounce stress harness; fix assembler local UNDEFs

Add `make test-bounce`: take one compiled program and bounce its
relocatable object through chains of format conversions and toolchain
transforms (objcopy -O elf/mach-o/coff round-trips, ld -r partial-link,
strip, ar archive), relink an ELF executable, run it, and assert the
exit code matches a host-cc reference. The cross-TU call to bounce_main
must survive each re-encoding or the exit code diverges, so the chains
exercise obj read/write, reloc translation, partial-link, and archive
symbol resolution rather than the arches.

Chains are gated to formats each arch actually targets: ELF everywhere,
COFF on x64+aarch64, Mach-O on aarch64 only. Defaults to the host-native
arch; CFREE_BOUNCE_ARCHES sweeps the rest via podman. Green on aarch64
(70), x64 (50), and rv64 (40).

Two fixes the harness depends on / surfaced:

- lang asm: a symbol referenced but never defined or declared .local was
  emitted as a local *UND*, which the linker (correctly) will not pull
  from an archive. Promote undefined locals to global SK_UNDEF at
  asm_parse finalize, matching GNU as. Without this an asm/crt stub
  cannot link against an archived definition.

- arch/x64: add the `syscall` mnemonic (0F 05) to the ISA table and the
  assembler's mnemonic normalizer (it ends in 'l', so it needs an early
  return before the generic size-suffix strip). It was entirely missing.

Diffstat:
Msrc/arch/x64/asm.c | 5+++++
Msrc/arch/x64/isa.c | 3+++
Msrc/asm/asm.c | 22++++++++++++++++++++++
Atest/bounce/CORPUS.md | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/bounce/bounce.sh | 263+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/bounce/cases/01_arith.c | 11+++++++++++
Atest/bounce/cases/02_control.c | 22++++++++++++++++++++++
Atest/bounce/cases/03_struct.c | 30++++++++++++++++++++++++++++++
Atest/bounce/cases/04_recursion.c | 19+++++++++++++++++++
Atest/bounce/cases/05_wide.c | 12++++++++++++
Atest/bounce/crt/crt.c | 48++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/test.mk | 15++++++++++++++-
12 files changed, 507 insertions(+), 1 deletion(-)

diff --git a/src/arch/x64/asm.c b/src/arch/x64/asm.c @@ -445,6 +445,11 @@ static int parse_mnemonic(const char* s, size_t n, X64MnInfo* out) { if (n == 3 && memcmp(s, "ret", 3) == 0) { out->base_len = 3; return 1; } + /* "syscall" ends in 'l' — return early so the generic size-suffix + * stripper below does not mistake it for a movl-style width letter. */ + if (n == 7 && memcmp(s, "syscall", 7) == 0) { + out->base_len = 7; return 1; + } /* Indirect-branch spellings carry an explicit 'q' suffix that must be * preserved — the BR_RM rows in the table are keyed on "jmpq"/"callq". */ diff --git a/src/arch/x64/isa.c b/src/arch/x64/isa.c @@ -47,6 +47,9 @@ const X64InsnDesc x64_insn_table[] = { /* ---- two-byte UD2 ---- */ ROW("ud2", X64_PFX_NONE, 2, 0x0F, 0x0B, 0, 0xFF, NO_MODRM, X64_W_REQ_ANY, X64_FMT_NULLARY, 0), + /* ---- SYSCALL (0F 05): fast system call ---- */ + ROW("syscall", X64_PFX_NONE, 2, 0x0F, 0x05, 0, 0xFF, NO_MODRM, + X64_W_REQ_ANY, X64_FMT_NULLARY, 0), ROW("mfence", X64_PFX_NONE, 3, 0x0F, 0xAE, 0xF0, 0xFF, NO_MODRM, X64_W_REQ_ANY, X64_FMT_NULLARY, 0), diff --git a/src/asm/asm.c b/src/asm/asm.c @@ -176,6 +176,26 @@ static ObjSym* sym_mut(AsmDriver* d, ObjSymId id) { return (ObjSym*)obj_symbol_get(d->ob, id); } +/* GNU `as` makes any symbol that is referenced but neither defined nor + * declared `.local` an undefined *global* (a local UNDEF is meaningless + * in ELF and won't pull a member out of an archive at link time). + * intern_sym mints every new symbol SB_LOCAL/SK_NOTYPE, so after the + * parse we promote the ones that stayed undefined to global SK_UNDEF. + * Defined locals (labels), `.local` decls, and absolute/common symbols + * are left untouched. */ +static void promote_undef_externs(AsmDriver* d) { + ObjSymIter* it = obj_symiter_new(d->ob); + ObjSymEntry e; + while (obj_symiter_next(it, &e)) { + if (e.sym->section_id != OBJ_SEC_NONE) continue; /* defined here */ + if (e.sym->bind != SB_LOCAL) continue; + if (e.sym->kind == SK_ABS || e.sym->kind == SK_COMMON) continue; + obj_symbol_set_bind(d->ob, e.id, SB_GLOBAL); + sym_mut(d, e.id)->kind = (u16)SK_UNDEF; + } + obj_symiter_free(it); +} + /* ---- expression evaluator (constants + sym ± const) ---- */ typedef struct AsmExpr { @@ -1036,6 +1056,8 @@ void asm_parse(Compiler* c, AsmLexer* l, MCEmitter* mc) { d_skip_to_eol(&d); } + promote_undef_externs(&d); + if (d.arch_asm && d.arch_asm->destroy) d.arch_asm->destroy(d.arch_asm); SymSecMap_fini(&d.sec_map); SymSymMap_fini(&d.sym_map); diff --git a/test/bounce/CORPUS.md b/test/bounce/CORPUS.md @@ -0,0 +1,58 @@ +# test/bounce — format-bounce stress corpus + +Stresses cfree's object **read/write/reloc**, **archive**, and +**partial-link** machinery by taking one compiled program and *bouncing* +its relocatable object through chains of format conversions and toolchain +transforms, relinking, and checking the result still computes the right +answer. Cross-arch *run* coverage lives elsewhere (test/link, test/smoke); +this corpus exercises the format paths, not the arches. + +Run with `make test-bounce` (opt-in; needs podman/qemu + a host `cc`). + +## Cases + +`cases/NN_name.c` each define `int bounce_main(void)` returning a value in +`[0,127]`. They are portable LP64 integer C, so the **host-cc result is the +oracle** every target and every bounce chain must reproduce. `crt/crt.c` is +the freestanding `_start`: it calls `bounce_main` with a normal C call (so +the compiler emits a real cross-TU relocation — the edge under test) and +exits via `exit_group` with the return value. + +## Chains + +For each (arch, case, -O level) the program is compiled to one `.o`, then +each chain rebuilds an executable from it and runs it: + +| Chain | Transform | Stresses | +|-------|-----------|----------| +| `direct` | link straight through | baseline | +| `macho_rt` | ELF → Mach-O → ELF (objcopy) | `macho_emit`/`macho_read` + reloc xlate | +| `coff_rt` | ELF → COFF → ELF (objcopy) | `coff_emit`/`coff_read` + reloc xlate | +| `tri_rt` | ELF → Mach-O → COFF → ELF | chained round-trip | +| `ldr` | `ld -r` partial-link merge → link | relocatable output | +| `strip_dbg` | `objcopy --strip-debug` → link | strip + reduced symtab | +| `ar` | `ar rcs` → on-demand link | GNU archive index r/w + symbol resolution | + +A chain only runs where every format it routes through is a real cfree +target for that arch: + +| Format | Arches | +|--------|--------| +| ELF | all (Linux / freestanding) | +| COFF | x64, aarch64 (Windows) | +| Mach-O | aarch64 only (Apple Silicon; cfree does not target x64 Mach-O) | + +So `macho_rt`/`tri_rt` run on aarch64 only, `coff_rt` on aarch64+x64, and +the ELF-only chains everywhere. + +## Arches + +Defaults to the host-native Linux arch (no emulation). Sweep others with +`CFREE_BOUNCE_ARCHES="aarch64 x64 rv64"`. Optimization levels default to +`0 1` (override with `CFREE_BOUNCE_OPTS`). + +## Conventions + +Skip-vs-fail mirrors the smoke harness: a skip counts as failure unless +`CFREE_TEST_ALLOW_SKIP=1`. Add a case by dropping a `cases/NN_name.c` that +defines `bounce_main`; no other wiring is needed. diff --git a/test/bounce/bounce.sh b/test/bounce/bounce.sh @@ -0,0 +1,263 @@ +#!/usr/bin/env bash +# test/bounce/bounce.sh — format-bounce stress test. +# +# Goal: stress cfree's object read/write/reloc, archive, and partial-link +# paths by taking one compiled program and *bouncing* its object through +# chains of format conversions and toolchain transforms, then verifying +# the relinked executable still produces the right answer. We already have +# cross-arch run coverage elsewhere; this test exists to exercise the +# format machinery, not the arches. +# +# For each (arch, case, -O level) the program is compiled to a relocatable +# object once, then each bounce chain rebuilds an executable from it: +# +# direct baseline: link prog.o + crt.o straight through +# macho_rt ELF -> Mach-O -> ELF (objcopy) then link +# coff_rt ELF -> COFF -> ELF (objcopy) then link +# tri_rt ELF -> Mach-O -> COFF -> ELF (chained) then link +# ldr `ld -r` partial-link merge of prog.o+crt.o, then final link +# strip_dbg objcopy --strip-debug on prog.o, then link +# ar `ar rcs` prog.o into an archive, on-demand link with crt.o +# +# Every chain's executable is run (batched through one podman exec) and its +# exit code compared to a host-cc reference for that case. Skip-vs-fail +# follows the smoke harness: a skip is a failure unless CFREE_TEST_ALLOW_SKIP=1. +# +# Arch selection: defaults to the host-native Linux arch (fast, no +# emulation). Override with CFREE_BOUNCE_ARCHES="aarch64 x64 rv64" to sweep +# others through podman/qemu. +set -u + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +BIN="${CFREE_BIN:-$ROOT/build/cfree}" +HOSTCC="${CC:-cc}" +BUILD="$ROOT/build/test/bounce" +CRT="$ROOT/test/bounce/crt/crt.c" +ALLOW_SKIP="${CFREE_TEST_ALLOW_SKIP:-0}" +OPT_LEVELS="${CFREE_BOUNCE_OPTS:-0 1}" + +rm -rf "$BUILD" +mkdir -p "$BUILD" + +color_red() { printf '\033[31m%s\033[0m' "$1"; } +color_grn() { printf '\033[32m%s\033[0m' "$1"; } +color_yel() { printf '\033[33m%s\033[0m' "$1"; } + +PASS=0; FAIL=0; SKIP=0 +note_pass() { PASS=$((PASS+1)); printf ' %s %s\n' "$(color_grn PASS)" "$1"; } +note_fail() { FAIL=$((FAIL+1)); printf ' %s %s\n' "$(color_red FAIL)" "$1"; } +note_skip() { SKIP=$((SKIP+1)); printf ' %s %s — %s\n' "$(color_yel SKIP)" "$1" "$2"; } + +# ---- arch selection -------------------------------------------------------- + +case "$(uname -m 2>/dev/null)" in + arm64|aarch64) NATIVE=aarch64 ;; + x86_64) NATIVE=x64 ;; + *) NATIVE="" ;; +esac +ARCHES="${CFREE_BOUNCE_ARCHES:-$NATIVE}" + +arch_triple() { + case "$1" in + aarch64) echo aarch64-linux ;; + x64) echo x86_64-linux ;; + rv64) echo riscv64-linux ;; + *) echo "" ;; + esac +} +arch_define() { + case "$1" in + aarch64) echo BOUNCE_AARCH64 ;; + x64) echo BOUNCE_X64 ;; + rv64) echo BOUNCE_RV64 ;; + *) echo "" ;; + esac +} + +# ---- exec_target wiring ---------------------------------------------------- + +have_podman=0; command -v podman >/dev/null 2>&1 && have_podman=1 +have_qemu=0; QEMU_BIN="" +case "$(uname -m 2>/dev/null)" in arm64|aarch64) is_aarch64=1 ;; *) is_aarch64=0 ;; esac +export have_podman have_qemu QEMU_BIN is_aarch64 +EXEC_TARGET_MOUNT_ROOT="$BUILD" +export EXEC_TARGET_MOUNT_ROOT +# shellcheck source=../lib/exec_target.sh +source "$ROOT/test/lib/exec_target.sh" + +# ---- prerequisites --------------------------------------------------------- + +if [ ! -x "$BIN" ]; then + echo "missing $BIN — run \`make bin\` first" >&2 + exit 2 +fi +if [ -z "$ARCHES" ]; then + note_skip "arch" "unsupported host arch $(uname -m) — set CFREE_BOUNCE_ARCHES" + printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" + [ "$ALLOW_SKIP" = "1" ] && exit 0 || exit 1 +fi +if ! "$HOSTCC" -x c -c /dev/null -o /dev/null 2>/dev/null; then + note_skip "hostcc" "host '$HOSTCC' cannot compile — set CC" + printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" + [ "$ALLOW_SKIP" = "1" ] && exit 0 || exit 1 +fi + +CASES="$(ls "$ROOT"/test/bounce/cases/*.c 2>/dev/null | sort)" +if [ -z "$CASES" ]; then + echo "no cases under test/bounce/cases" >&2 + exit 2 +fi + +# ---- per-case host reference ---------------------------------------------- +# Reference exit code for a case = running it on the host. The cases are +# portable LP64 integer C, so the host result is the oracle every target +# and every bounce chain must reproduce. +compute_ref() { + local case="$1" name="$2" ref_exe="$BUILD/$name.ref" drv="$BUILD/$name.refmain.c" + printf 'int bounce_main(void);\nint main(void){return bounce_main();}\n' >"$drv" + if ! "$HOSTCC" -O1 "$case" "$drv" -o "$ref_exe" 2>"$BUILD/$name.ref.err"; then + return 1 + fi + "$ref_exe"; echo $? +} + +# ---- bounce chains --------------------------------------------------------- +# Each chain takes prog.o + crt.o and produces an executable at $out, or +# returns nonzero (with a diagnostic on stdout) if any transform/link fails. +# $1=prog.o $2=crt.o $3=out $4=workdir +chain_direct() { "$BIN" ld -o "$3" -e _start -static "$1" "$2" 2>&1; } +chain_macho_rt() { + "$BIN" objcopy -O mach-o "$1" "$4/m.o" 2>&1 || return 1 + "$BIN" objcopy -O elf "$4/m.o" "$4/m2.o" 2>&1 || return 1 + "$BIN" ld -o "$3" -e _start -static "$4/m2.o" "$2" 2>&1 +} +chain_coff_rt() { + "$BIN" objcopy -O coff "$1" "$4/c.o" 2>&1 || return 1 + "$BIN" objcopy -O elf "$4/c.o" "$4/c2.o" 2>&1 || return 1 + "$BIN" ld -o "$3" -e _start -static "$4/c2.o" "$2" 2>&1 +} +chain_tri_rt() { + "$BIN" objcopy -O mach-o "$1" "$4/t1.o" 2>&1 || return 1 + "$BIN" objcopy -O coff "$4/t1.o" "$4/t2.o" 2>&1 || return 1 + "$BIN" objcopy -O elf "$4/t2.o" "$4/t3.o" 2>&1 || return 1 + "$BIN" ld -o "$3" -e _start -static "$4/t3.o" "$2" 2>&1 +} +chain_ldr() { + "$BIN" ld -r -o "$4/merged.o" "$1" "$2" 2>&1 || return 1 + "$BIN" ld -o "$3" -e _start -static "$4/merged.o" 2>&1 +} +chain_strip_dbg() { + "$BIN" objcopy --strip-debug "$1" "$4/s.o" 2>&1 || return 1 + "$BIN" ld -o "$3" -e _start -static "$4/s.o" "$2" 2>&1 +} +chain_ar() { + rm -f "$4/lib.a" + "$BIN" ar rcs "$4/lib.a" "$1" 2>&1 || return 1 + "$BIN" ld -o "$3" -e _start -static "$2" "$4/lib.a" 2>&1 +} +CHAINS="direct macho_rt coff_rt tri_rt ldr strip_dbg ar" + +# A chain is only meaningful when every object format it routes through is +# a real cfree target for that arch. Support matrix: +# ELF all arches (Linux/freestanding) +# COFF x64 + aarch64 (Windows) +# Mach-O aarch64 only (Apple Silicon; cfree does not target x64 Mach-O) +# Returns 0 when $chain is applicable for $arch. +chain_applies() { + local arch="$1" chain="$2" + case "$chain" in + macho_rt | tri_rt) # route through Mach-O + [ "$arch" = aarch64 ] ;; + coff_rt) # route through COFF + case "$arch" in aarch64 | x64) return 0 ;; *) return 1 ;; esac ;; + *) return 0 ;; # ELF-only: direct, ldr, strip_dbg, ar + esac +} + +# ---- queue bookkeeping (parallel indexed arrays; bash 3.2 safe) ----------- +Q_LABEL=(); Q_RCFILE=(); Q_EXPECT=() + +# ---- main sweep ------------------------------------------------------------ + +for arch in $ARCHES; do + triple="$(arch_triple "$arch")" + define="$(arch_define "$arch")" + if [ -z "$triple" ]; then + note_skip "$arch" "unknown arch tag" + continue + fi + if ! exec_target_supported "$arch"; then + note_skip "$arch" "no runner (need podman or qemu for $arch-linux)" + continue + fi + + for opt in $OPT_LEVELS; do + # crt depends only on (arch, opt); build it once. + crt_o="$BUILD/crt.$arch.O$opt.o" + if ! "$BIN" cc -target "$triple" -D"$define" -O"$opt" -c "$CRT" \ + -o "$crt_o" 2>"$BUILD/crt.$arch.O$opt.err"; then + note_fail "$arch/O$opt crt.c (see $BUILD/crt.$arch.O$opt.err)" + continue + fi + + for case in $CASES; do + name="$(basename "$case" .c)" + ref="$(compute_ref "$case" "$name")" + if [ -z "$ref" ]; then + note_fail "$name reference build (host cc; see $BUILD/$name.ref.err)" + continue + fi + + prog_o="$BUILD/$name.$arch.O$opt.o" + if ! "$BIN" cc -target "$triple" -O"$opt" -c "$case" -o "$prog_o" \ + 2>"$BUILD/$name.$arch.O$opt.err"; then + note_fail "$name $arch/O$opt compile (see $BUILD/$name.$arch.O$opt.err)" + continue + fi + + for chain in $CHAINS; do + chain_applies "$arch" "$chain" || continue + tag="$name.$arch.O$opt.$chain" + wd="$BUILD/$tag.d"; mkdir -p "$wd" + exe="$BUILD/$tag.exe" + if ! "chain_$chain" "$prog_o" "$crt_o" "$exe" "$wd" \ + >"$BUILD/$tag.link.log" 2>&1; then + note_fail "$tag (transform/link; see $BUILD/$tag.link.log)" + continue + fi + # Re-read the produced executable's headers to catch malformed + # output the linker accepted but a reader would choke on. + rcfile="$BUILD/$tag.rc" + Q_LABEL+=("$tag"); Q_RCFILE+=("$rcfile"); Q_EXPECT+=("$ref") + exec_target_queue "$arch" "$tag" "$exe" \ + "$BUILD/$tag.out" "$BUILD/$tag.err" "$rcfile" + done + done + done +done + +# ---- run everything in batched container execs, then score ----------------- +exec_target_flush + +i=0; n="${#Q_LABEL[@]}" +while [ "$i" -lt "$n" ]; do + label="${Q_LABEL[$i]}"; rcfile="${Q_RCFILE[$i]}"; expect="${Q_EXPECT[$i]}" + i=$((i+1)) + if [ ! -f "$rcfile" ]; then + note_fail "$label (no rc recorded)" + continue + fi + got="$(cat "$rcfile")" + if [ "$got" = "$expect" ]; then + note_pass "$label (rc=$got)" + else + note_fail "$label (expected $expect got $got)" + fi +done + +printf '\nResults: %s pass, %s fail, %s skip\n' "$PASS" "$FAIL" "$SKIP" +[ "$FAIL" -eq 0 ] || exit 1 +if [ "$PASS" -eq 0 ]; then + [ "$ALLOW_SKIP" = "1" ] && exit 0 || exit 1 +fi +exit 0 diff --git a/test/bounce/cases/01_arith.c b/test/bounce/cases/01_arith.c @@ -0,0 +1,11 @@ +/* Integer arithmetic: add/sub/mul/div/mod/shift across a few widths. + * bounce_main returns a value in [0,127]. */ +int bounce_main(void) { + unsigned u = 0xDEADBEEFu; + int a = 1234567, b = 89; + long c = (long)a * b - (a / b) + (a % b); + c ^= (long)(u >> 7); + c += (c << 3) - (c >> 2); + int r = (int)(c & 0x7f); + return r; +} diff --git a/test/bounce/cases/02_control.c b/test/bounce/cases/02_control.c @@ -0,0 +1,22 @@ +/* Control flow: nested loops, break/continue, conditional accumulation. + * Computes a Collatz-step total. Returns a value in [0,127]. */ +static int collatz_steps(unsigned n) { + int s = 0; + while (n != 1u) { + if (n & 1u) + n = 3u * n + 1u; + else + n = n / 2u; + if (++s > 1000) break; + } + return s; +} + +int bounce_main(void) { + long total = 0; + for (unsigned i = 1; i <= 50u; ++i) { + if (i % 7u == 0u) continue; + total += collatz_steps(i); + } + return (int)(total & 0x7f); +} diff --git a/test/bounce/cases/03_struct.c b/test/bounce/cases/03_struct.c @@ -0,0 +1,30 @@ +/* Aggregates: struct-by-value args/returns, arrays, pointer walks. + * Returns a value in [0,127]. */ +struct vec3 { + long x, y, z; +}; + +static struct vec3 add3(struct vec3 a, struct vec3 b) { + struct vec3 r = {a.x + b.x, a.y + b.y, a.z + b.z}; + return r; +} + +static long dot(struct vec3 a, struct vec3 b) { + return a.x * b.x + a.y * b.y + a.z * b.z; +} + +int bounce_main(void) { + struct vec3 acc = {0, 0, 0}; + struct vec3 step = {1, 2, 3}; + for (int i = 0; i < 16; ++i) { + acc = add3(acc, step); + step.x += i; + step.z -= 1; + } + long arr[8]; + for (int i = 0; i < 8; ++i) arr[i] = acc.x + i * acc.y - acc.z; + long s = 0; + for (long* p = arr; p < arr + 8; ++p) s += *p; + s += dot(acc, step); + return (int)(s & 0x7f); +} diff --git a/test/bounce/cases/04_recursion.c b/test/bounce/cases/04_recursion.c @@ -0,0 +1,19 @@ +/* Recursion and the call/spill machinery: fib + a small Ackermann. + * Returns a value in [0,127]. */ +static long fib(int n) { + if (n < 2) return n; + return fib(n - 1) + fib(n - 2); +} + +static int ack(int m, int n) { + if (m == 0) return n + 1; + if (n == 0) return ack(m - 1, 1); + return ack(m - 1, ack(m, n - 1)); +} + +int bounce_main(void) { + long s = fib(20); /* 6765 */ + s += ack(2, 3); /* 9 */ + s += ack(3, 3); /* 61 */ + return (int)(s & 0x7f); +} diff --git a/test/bounce/cases/05_wide.c b/test/bounce/cases/05_wide.c @@ -0,0 +1,12 @@ +/* Wide arithmetic: 64-bit multiply/divide/shift mixes that lower to + * compiler-rt helpers on some targets. Returns a value in [0,127]. */ +int bounce_main(void) { + unsigned long long a = 0x0123456789ABCDEFull; + unsigned long long b = 0xFEDCBA9876543210ull; + unsigned long long m = a * b; /* low 64 of the product */ + unsigned long long d = b / (a | 1ull); /* 64-bit divide */ + long long s = (long long)(a >> 17) - (long long)(b >> 29); + s += (long long)(m ^ d); + s ^= (long long)(a % 1000003ull); + return (int)(s & 0x7f); +} diff --git a/test/bounce/crt/crt.c b/test/bounce/crt/crt.c @@ -0,0 +1,48 @@ +/* Freestanding entry point for the bounce harness. + * + * The harness compiles this with a -DBOUNCE_<ARCH> define selecting the + * guest architecture (cfree does not predefine __x86_64__ etc.). _start + * calls bounce_main with a normal C call so the compiler emits the proper + * cross-translation-unit relocation — the very edge the format-bounce + * exercises — then exits via exit_group with its return value. + * + * The syscall is set up with explicit `mov` instructions taking the exit + * code through a generic "r" operand rather than GCC local register + * variables (which cfree does not honor) or per-arch call/syscall pseudos + * (which the standalone assembler does not model). */ + +int bounce_main(void); + +__attribute__((noreturn)) static void sys_exit(int code) { +#if defined(BOUNCE_X64) + __asm__ volatile( + "movl %0, %%edi\n\t" + "movl $231, %%eax\n\t" /* exit_group */ + "syscall\n\t" + : + : "r"(code) + : "eax", "edi", "memory"); +#elif defined(BOUNCE_AARCH64) + __asm__ volatile( + "mov x0, %0\n\t" + "mov x8, #94\n\t" + "svc #0\n\t" + : + : "r"((long)code) + : "x0", "x8", "memory"); +#elif defined(BOUNCE_RV64) + __asm__ volatile( + "mv a0, %0\n\t" + "li a7, 94\n\t" + "ecall\n\t" + : + : "r"((long)code) + : "a0", "a7", "memory"); +#else +#error "define BOUNCE_X64 / BOUNCE_AARCH64 / BOUNCE_RV64" +#endif + for (;;) { + } +} + +void _start(void) { sys_exit(bounce_main()); } diff --git a/test/test.mk b/test/test.mk @@ -26,7 +26,7 @@ # asm_parse / cfree_disasm_iter_* are still stubs; the harness builds # and runs end-to-end so the wiring stays exercised. See doc/ASM.md. -.PHONY: test test-driver test-pp test-pp-err test-elf test-coff test-coff-mingw-import test-coff-windows-ucrt test-ar test-ar-driver test-strip-driver test-objcopy-driver test-objdump-driver test-link test-cg-api test-toy test-opt test-dwarf test-debug test-parse test-parse-err test-asm test-wasm-front test-isa test-aa64-inline test-rv64-inline test-rv64-jit test-emu test-x64-inline test-x64-dbg test-rt-headers test-rt-runtime test-musl test-musl-rv64 test-glibc test-glibc-rv64 test-lib-deps test-smoke-x64 test-smoke-rv64 test-cbackend rv64-doctor +.PHONY: test test-driver test-pp test-pp-err test-elf test-coff test-coff-mingw-import test-coff-windows-ucrt test-ar test-ar-driver test-strip-driver test-objcopy-driver test-objdump-driver test-link test-cg-api test-toy test-opt test-dwarf test-debug test-parse test-parse-err test-asm test-wasm-front test-isa test-aa64-inline test-rv64-inline test-rv64-jit test-emu test-x64-inline test-x64-dbg test-rt-headers test-rt-runtime test-musl test-musl-rv64 test-glibc test-glibc-rv64 test-lib-deps test-smoke-x64 test-smoke-rv64 test-bounce test-cbackend rv64-doctor test: test-driver test-pp test-pp-err test-elf test-coff test-ar test-ar-driver test-strip-driver test-objcopy-driver test-objdump-driver test-link test-toy test-dwarf test-debug test-parse test-parse-err test-asm test-isa test-aa64-inline test-rv64-inline test-rv64-jit test-emu test-x64-inline test-x64-dbg test-rt-headers test-lib-deps # `test-cbackend` is intentionally not in the default `test` target: the @@ -404,6 +404,19 @@ test-smoke-x64: test-smoke-rv64: bash test/smoke/rv64.sh +# test-bounce: format-bounce stress test. Compiles small programs with +# cfree, then bounces each object through chains of format conversions +# (ELF<->Mach-O<->COFF), partial links (ld -r), strip, and archive +# round-trips, relinks, and runs the result, asserting the exit code +# matches a host-cc reference. Stresses obj read/write/reloc, ar, and +# partial-link paths rather than the arches. Defaults to the host-native +# Linux arch (no emulation); sweep others with +# CFREE_BOUNCE_ARCHES="aarch64 x64 rv64". Excluded from the default `test` +# target because it needs podman/qemu + a host cc; opt-in via +# `make test-bounce`. +test-bounce: $(BIN) + bash test/bounce/bounce.sh + # test-musl / test-glibc: end-to-end static + dynamic libc link/run on # aarch64. Each variant pulls its own pinned sysroot (podman, ~30s on # first run) and shares the same case files under test/libc/cases/: