hostas_cross.sh (15726B)
1 #!/usr/bin/env bash 2 # test/asm/hostas_cross.sh — cross-compile + cross-exec extension of the 3 # host-assembler lane (test/asm/hostas_toy.sh) to ELF Linux targets. 4 # (Type C — shared corpus harness; one kit_corpus_run per ELF target.) 5 # 6 # Where hostas_toy.sh proves `cc -S` on the *native* target, this proves it 7 # CROSS: for each ELF target (aarch64/x86_64/riscv64-linux) it emits ONE 8 # `cc -S`, feeds it to BOTH kit-as and a host assembler (clang), links each 9 # into a static ELF with `kit ld`, and runs it under podman/qemu — exit must 10 # match the toy oracle. The assembler is the only variable between the two 11 # lanes, judged by EXECUTION (kit and clang emit different code, so a 12 # byte/text match would be meaningless), exactly like hostas_toy.sh. 13 # 14 # Per toy case (each target; both O0 and O1): 15 # kit cc -S -target <triple> -> s.s (shared by both) 16 # A /kit-as: kit as -target | kit ld -static -> run exit == oracle 17 # B /clang-as: clang --target -c | kit ld -static -> run exit == oracle 18 # 19 # Executable shape: a STATIC, non-PIE ELF (`kit ld -static`) linked with the 20 # freestanding crt test/link/harness/start.c compiled `-Dtest_main=main`, so 21 # `_start` runs ctors then calls the toy's `main` and exits with its return 22 # (the oracle) via a raw syscall — no libc/loader needed, so any same-arch 23 # Linux image runs it. Execution uses the shared test/lib/exec_target.sh helper. 24 # Lane A (and, under ENFORCE_CLANG=1, lane B) defers exec to one batched 25 # `podman run` per target via the corpus engine's kit_queue_e; lane B under 26 # ENFORCE_CLANG=0 runs inline (bounded) so its exec verdict can be XFAIL/XPASS. 27 # 28 # Self-probing: each target is SKIPPED (not failed) unless the host has (1) a 29 # clang cross-compiler for it, (2) a runner (podman/qemu) per exec_target, (3) a 30 # working `kit cc -S | kit as` round-trip for that arch, and (4) a bounded 31 # exec smoke that returns the oracle. So the harness runs green on whatever the 32 # host supports and self-extends as gaps close. All three ELF targets pass BOTH 33 # lanes end-to-end (936/936 = 312 cases x {O0,O1} x 3 arches, ENFORCE_CLANG). 34 # Code locations that an encoding-divergent assembler must recompute — switch 35 # jump-table entries and `&&label` address-takes — are referenced via per-block 36 # local symbols emitted by codegen (mc_label_symbol): the jump table is 37 # `.quad .Lcfblk.*` (R_ABS64) and the address-take a standard PC-relative reloc 38 # against the same symbol (x86-64 leaq/R_PC32, aarch64 adrp+add, riscv64 39 # auipc+addi/%pcrel). So the references are genuinely relocatable on every arch 40 # and clang's encoding choices (movabs vs mov-imm32, jmp rel32 vs rel8, RVC 41 # compression) can't shift a baked offset onto the wrong instruction. 42 # Execution under qemu-user (x86_64/riscv64 in their podman containers) is the 43 # sole judge — kit and clang emit different code, so a byte/text match would be 44 # meaningless. The batched runner caps each case (EXEC_CASE_TIMEOUT) so one 45 # hanging binary can't wedge the whole container. 46 # 47 # Override the matrix with KIT_HOSTAS_CROSS_TARGETS="tag:triple ..." and the 48 # clang-as gate with KIT_HOSTAS_ENFORCE_CLANG=0 (demote lane B to XFAIL). 49 # Every lane hook writes only under $KIT_WORK and records via kit_*, so the runner 50 # is parallel-safe; KIT_HOSTAS_PARALLEL flips dispatch. 51 52 set -u 53 54 ROOT="$(cd "$(dirname "$0")/../.." && pwd)" 55 export KIT_LIB_DIR="$ROOT/test/lib" 56 # shellcheck source=../lib/kit_corpus.sh 57 . "$ROOT/test/lib/kit_corpus.sh" 58 59 KIT="${KIT:-$ROOT/build/kit}" 60 CASES="$ROOT/test/toy/cases" 61 BUILD_DIR="$ROOT/build/test/asm/hostas_cross" 62 START_SRC="$ROOT/test/link/harness/start.c" 63 ENFORCE_CLANG="${KIT_HOSTAS_ENFORCE_CLANG:-1}" 64 EXEC_SMOKE_TIMEOUT="${KIT_HOSTAS_EXEC_TIMEOUT:-45}" 65 PAR="${KIT_HOSTAS_PARALLEL:-1}" 66 67 # Opt axis (see hostas_toy.sh): map a KIT_TEST_OPTS override of O-spellings 68 # onto the engine's bare opt levels; default 0 1. 69 OPT_LEVELS="0 1" 70 if [ -n "${KIT_TEST_OPTS:-}" ]; then 71 OPT_LEVELS="" 72 for _o in $KIT_TEST_OPTS; do OPT_LEVELS="$OPT_LEVELS ${_o#O}"; done 73 fi 74 75 # "tag:triple" — tag is exec_target.sh's <arch>-<os> spelling. All three ELF 76 # targets are in the gating default (each SKIPs cleanly if its clang cross 77 # target or container runner is unavailable). Narrow the matrix with 78 # KIT_HOSTAS_CROSS_TARGETS, e.g. KIT_HOSTAS_CROSS_TARGETS="x64-linux:x86_64-linux-gnu". 79 TARGETS="${KIT_HOSTAS_CROSS_TARGETS:-aarch64-linux:aarch64-linux-gnu x64-linux:x86_64-linux-gnu rv64-linux:riscv64-linux-gnu}" 80 81 # Filter ($1) preserved -> the engine filters discovery by KIT_TEST_FILTER. 82 FILTER="${1:-${KIT_TEST_FILTER:-}}" 83 export KIT_TEST_FILTER="$FILTER" 84 85 # The original exits on a_fail (+ b_efail when enforcing) only — skips (the known 86 # 141 case) never gated. Keep that: SKIP must not fail the run. 87 KIT_SKIP_IS_FAILURE=0 88 89 CLANG="${CLANG:-$(command -v clang 2>/dev/null || true)}" 90 91 color_red() { printf '\033[31m%s\033[0m' "$1"; } 92 color_grn() { printf '\033[32m%s\033[0m' "$1"; } 93 color_yel() { printf '\033[33m%s\033[0m' "$1"; } 94 95 if [ ! -x "$KIT" ]; then 96 printf 'hostas-cross: %s kit missing — run "make bin"\n' "$(color_red FATAL)" >&2 97 exit 1 98 fi 99 if [ -z "$CLANG" ] || [ ! -x "$CLANG" ]; then 100 printf 'hostas-cross: %s no clang (host assembler); skipping\n' "$(color_yel SKIP)" 101 exit 0 102 fi 103 mkdir -p "$BUILD_DIR" 104 105 # ---- exec_target.sh wiring (mirrors test/link/run.sh detection) ------------ 106 have_qemu=0 107 QEMU_BIN="$(command -v qemu-aarch64-static 2>/dev/null || command -v qemu-aarch64 2>/dev/null || true)" 108 [ -n "$QEMU_BIN" ] && have_qemu=1 109 QEMU_RV64_BIN="$(command -v qemu-riscv64-static 2>/dev/null || command -v qemu-riscv64 2>/dev/null || true)" 110 have_podman=0 111 command -v podman >/dev/null 2>&1 && have_podman=1 112 arch_raw="$(uname -m 2>/dev/null || true)" 113 is_aarch64=0 114 { [ "$arch_raw" = "aarch64" ] || [ "$arch_raw" = "arm64" ]; } && is_aarch64=1 115 export have_qemu QEMU_BIN QEMU_RV64_BIN have_podman is_aarch64 116 # Pin arch-explicit images so podman doesn't resolve an ambiguous multi-arch 117 # tag to the wrong variant (the bare `alpine:latest` in local storage can map 118 # to a non-host arch). Overridable; the binaries are static/freestanding so any 119 # same-arch Linux image with /bin/sh works. Absent images fail --pull=never, 120 # which the per-target exec smoke turns into a clean SKIP. 121 : "${RUN_AARCH64_IMAGE:=docker.io/arm64v8/alpine:latest}" 122 : "${RUN_X64_IMAGE:=docker.io/amd64/alpine:latest}" 123 : "${RUN_RV64_IMAGE:=docker.io/riscv64/alpine:edge}" 124 export RUN_AARCH64_IMAGE RUN_X64_IMAGE RUN_RV64_IMAGE 125 EXEC_TARGET_MOUNT_ROOT="$BUILD_DIR" 126 export EXEC_TARGET_MOUNT_ROOT 127 # shellcheck source=../lib/exec_target.sh 128 . "$ROOT/test/lib/exec_target.sh" 129 130 # Same TLS-symbolization skip as the sibling lanes — driven by the shared 131 # sidecar 141_threadlocal_mutate.link.skip via KIT_READ_CASE (skips the WHOLE 132 # case here too, matching hostas_toy.sh). 133 hostas_read_case() { 134 local reason 135 if reason=$(kit_skip_sidecar "$KIT_SIDECAR_DIR" "$KIT_BASE" "" "link"); then 136 KIT_SKIP_CASE="$reason (cc -S symbolizer gap)" 137 fi 138 } 139 140 oracle() { 141 local name="$1" exp=0 142 [ -f "$CASES/$name.expected" ] && exp=$(head -n1 "$CASES/$name.expected") 143 echo $((exp & 255)) 144 } 145 146 # First real `error:` line from an assembler's stderr (clang prints a harmless 147 # -Wmissing-sysroot warning first — we never link with clang, so the SDK is 148 # irrelevant). 149 err_reason() { 150 local f="$1" line="" 151 line=$(grep -m1 -E 'error:|fatal:' "$f" 2>/dev/null | sed 's|.*\(error\|fatal\): *||') 152 [ -z "$line" ] && line=$(head -1 "$f" 2>/dev/null | sed 's|.*: ||') 153 printf '%s' "$line" 154 } 155 156 # Bounded exec: run exe via exec_target under a wall-clock cap so a wedged 157 # emulator (e.g. a riscv64 qemu-user that never returns) downgrades a target to 158 # SKIP instead of hanging the whole harness. Sets SMOKE_RC (124 == timed out). 159 bounded_exec() { 160 local to="$1" tag="$2" exe="$3" out="$4" err="$5" 161 local rc0="$exe.rc0" 162 rm -f "$rc0" 163 ( exec_target_run "$tag" "$exe" "$out" "$err"; echo "$RUN_RC" >"$rc0" ) & 164 local pid=$! waited=0 165 while kill -0 "$pid" 2>/dev/null; do 166 sleep 1; waited=$((waited+1)) 167 if [ "$waited" -ge "$to" ]; then 168 kill -9 "$pid" 2>/dev/null; wait "$pid" 2>/dev/null 169 SMOKE_RC=124; return 170 fi 171 done 172 wait "$pid" 2>/dev/null 173 SMOKE_RC=124 174 [ -f "$rc0" ] && SMOKE_RC="$(cat "$rc0")" 175 } 176 177 # Link a relocatable object + the target crt into a static ELF. Echoes nothing; 178 # returns nonzero (and leaves stderr in $3) on failure or a non-empty linker 179 # diagnostic (warnings are treated as failures, as in hostas_toy.sh). 180 kit_ld_static() { 181 local obj="$1" out="$2" lderr="$3" crt="$4" 182 "$KIT" ld -static "$obj" "$crt" -o "$out" 2>"$lderr" || return 1 183 [ -s "$lderr" ] && return 1 184 return 0 185 } 186 187 # ---- shared `cc -S -target` build for both lanes --------------------------- 188 # One cross .s per item, cached in $KIT_WORK so lanes A and B reuse it. A cc -S 189 # failure is a lane-A failure only (lane B records nothing for the item). 190 # Reads the per-target $TGT_TRIPLE (set before each target's kit_corpus_run). 191 _ccs_build() { 192 CCS_S="$KIT_WORK/s.s" 193 [ -f "$KIT_WORK/.ccs.done" ] && { CCS_RC=$(cat "$KIT_WORK/.ccs.rc"); return "$CCS_RC"; } 194 if "$KIT" cc -S "-O$KIT_OPT" -target "$TGT_TRIPLE" "$KIT_SRC" -o "$CCS_S" 2>"$KIT_WORK/ccs.err"; then 195 CCS_RC=0 196 else 197 CCS_RC=1 198 fi 199 echo "$CCS_RC" >"$KIT_WORK/.ccs.rc"; : >"$KIT_WORK/.ccs.done" 200 return "$CCS_RC" 201 } 202 203 # ---- lane A: kit-as -> kit ld -static -> run (deferred batched exec) ---- 204 # Reads per-target $TGT_TRIPLE, $TGT_TAG, $TGT_CRT (set before kit_corpus_run). 205 kit_lane_A() { 206 if ! _ccs_build; then 207 kit_fail "$TGT_TAG/$KIT_NAME/kit-as" "cc -S: $(err_reason "$KIT_WORK/ccs.err")" 208 return 209 fi 210 if ! "$KIT" as -target "$TGT_TRIPLE" "$CCS_S" -o "$KIT_WORK/a.o" 2>"$KIT_WORK/a.as.err"; then 211 kit_fail "$TGT_TAG/$KIT_NAME/kit-as" "$(err_reason "$KIT_WORK/a.as.err")"; return 212 fi 213 if ! kit_ld_static "$KIT_WORK/a.o" "$KIT_WORK/a.out" "$KIT_WORK/a.ld.err" "$TGT_CRT"; then 214 kit_fail "$TGT_TAG/$KIT_NAME/kit-as ld" "$(err_reason "$KIT_WORK/a.ld.err")"; return 215 fi 216 kit_queue_e "$TGT_TAG/$KIT_NAME/kit-as" "$KIT_WORK/a.out" \ 217 "$KIT_WORK/a.run.out" "$KIT_WORK/a.run.err" "$KIT_WORK/a.rc" \ 218 "$KIT_EXPECTED" "$TGT_TAG" 219 } 220 221 # ---- lane B: clang-as -> kit ld -static -> run --------------------------- 222 # Gated by ENFORCE_CLANG. Build-stage failures are recorded inline (FAIL when 223 # enforcing, XFAIL otherwise). The exec stage: when enforcing, defer to the 224 # batched flush via kit_queue_e (pass/fail). When NOT enforcing, run inline 225 # (bounded) so the exec verdict can be XPASS (pass) / XFAIL (fail). 226 kit_lane_B() { 227 if ! _ccs_build; then return; fi 228 local name="$TGT_TAG/$KIT_NAME/clang-as" 229 if ! "$CLANG" --target="$TGT_TRIPLE" -c "$CCS_S" -o "$KIT_WORK/b.o" 2>"$KIT_WORK/b.as.err"; then 230 if [ "$ENFORCE_CLANG" = "1" ]; then kit_fail "$name" "$(err_reason "$KIT_WORK/b.as.err")" 231 else kit_xfail "$name" "$(err_reason "$KIT_WORK/b.as.err")"; fi 232 return 233 fi 234 if ! kit_ld_static "$KIT_WORK/b.o" "$KIT_WORK/b.out" "$KIT_WORK/b.ld.err" "$TGT_CRT"; then 235 if [ "$ENFORCE_CLANG" = "1" ]; then kit_fail "$name" "ld: $(err_reason "$KIT_WORK/b.ld.err")" 236 else kit_xfail "$name"; fi 237 return 238 fi 239 if [ "$ENFORCE_CLANG" = "1" ]; then 240 kit_queue_e "$name" "$KIT_WORK/b.out" \ 241 "$KIT_WORK/b.run.out" "$KIT_WORK/b.run.err" "$KIT_WORK/b.rc" \ 242 "$KIT_EXPECTED" "$TGT_TAG" 243 return 244 fi 245 # ENFORCE_CLANG=0: inline bounded exec -> XPASS/XFAIL. 246 bounded_exec "${EXEC_CASE_TIMEOUT:-20}" "$TGT_TAG" "$KIT_WORK/b.out" \ 247 "$KIT_WORK/b.run.out" "$KIT_WORK/b.run.err" 248 if [ "$SMOKE_RC" = "$((KIT_EXPECTED & 255))" ]; then kit_xpass "$name" 249 else kit_xfail "$name"; fi 250 } 251 252 # ---- per-target capability gates (SKIP-TGT, never FAIL) -------------------- 253 # Returns 0 if the target is runnable (and sets TGT_CRT); prints a single 254 # SKIP-TGT line and returns 1 otherwise. Mirrors the original gate chain. 255 target_gate() { 256 local tag="$1" triple="$2" tdir="$3" 257 if ! "$CLANG" --target="$triple" -c -x c - -o /dev/null </dev/null 2>/dev/null; then 258 printf ' %s %s — no clang cross target\n' "$(color_yel SKIP-TGT)" "$tag"; return 1 259 fi 260 if ! exec_target_supported "$tag"; then 261 printf ' %s %s — no runner (podman/qemu)\n' "$(color_yel SKIP-TGT)" "$tag"; return 1 262 fi 263 TGT_CRT="$tdir/start.o" 264 if ! "$CLANG" --target="$triple" -O1 -ffreestanding -fno-stack-protector \ 265 -fno-PIC -fno-pie -Dtest_main=main -c "$START_SRC" -o "$TGT_CRT" 2>"$tdir/crt.err"; then 266 printf ' %s %s — crt build failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$tdir/crt.err")"; return 1 267 fi 268 269 # Pick a representative non-skip case for the smokes (first case without a 270 # 141-style .link.skip sidecar). 271 local smoke="" s n 272 for s in "$CASES"/*.toy; do 273 n="$(basename "$s" .toy)" 274 kit_skip_sidecar "$CASES" "$n" "" "link" >/dev/null && continue 275 smoke="$n"; break 276 done 277 local sd="$tdir/_smoke"; mkdir -p "$sd" 278 if ! "$KIT" cc -S -O0 -target "$triple" "$CASES/$smoke.toy" -o "$sd/s.s" 2>"$sd/ccs.err"; then 279 printf ' %s %s — cc -S failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ccs.err")"; return 1 280 fi 281 if ! "$KIT" as -target "$triple" "$sd/s.s" -o "$sd/a.o" 2>"$sd/as.err"; then 282 printf ' %s %s — cc -S|as gap: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/as.err")"; return 1 283 fi 284 if ! kit_ld_static "$sd/a.o" "$sd/a.out" "$sd/ld.err" "$TGT_CRT"; then 285 printf ' %s %s — kit ld -static failed: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$(err_reason "$sd/ld.err")"; return 1 286 fi 287 bounded_exec "$EXEC_SMOKE_TIMEOUT" "$tag" "$sd/a.out" "$sd/run.out" "$sd/run.err" 288 local sexp; sexp=$(oracle "$smoke") 289 if [ "$SMOKE_RC" != "$sexp" ]; then 290 local reason="exit $SMOKE_RC != $sexp" 291 [ "$SMOKE_RC" = "124" ] && reason="exec timed out (>${EXEC_SMOKE_TIMEOUT}s)" 292 printf ' %s %s — exec smoke: %s\n' "$(color_yel SKIP-TGT)" "$tag" "$reason"; return 1 293 fi 294 return 0 295 } 296 297 # ---- drive the corpus, one target at a time -------------------------------- 298 printf 'hostas-cross: kit=%s\n' "$KIT" 299 printf 'hostas-cross: clang=%s opts="%s" enforce_clang=%s podman=%s\n' \ 300 "$CLANG" "$OPT_LEVELS" "$ENFORCE_CLANG" "$have_podman" 301 printf 'hostas-cross: targets="%s"\n' "$TARGETS" 302 303 tgt_run=0 304 shopt -s nullglob 305 for entry in $TARGETS; do 306 TGT_TAG="${entry%%:*}"; TGT_TRIPLE="${entry##*:}" 307 tdir="$BUILD_DIR/$TGT_TAG"; rm -rf "$tdir"; mkdir -p "$tdir" 308 if ! target_gate "$TGT_TAG" "$TGT_TRIPLE" "$tdir"; then continue; fi 309 310 tgt_run=$((tgt_run + 1)) 311 printf ' %s %s (%s) — running corpus\n' "$(color_grn TGT)" "$TGT_TAG" "$TGT_TRIPLE" 312 # TGT_TAG / TGT_TRIPLE / TGT_CRT are set; lane hooks (incl. parallel 313 # workers, forked after this point) read them. One kit_corpus_run per target, 314 # like elf's A/B/C layers — counters accumulate into the shared summary. 315 export TGT_TAG TGT_TRIPLE TGT_CRT 316 KIT_LABEL=test-hostas-cross KIT_BUILD_DIR="$tdir" \ 317 KIT_CORPUS_GLOBS="$CASES/*.toy" KIT_CORPUS_EXT=toy KIT_SIDECAR_DIR="$CASES" \ 318 KIT_LANES="A B" KIT_OPT_LEVELS="$OPT_LEVELS" KIT_TUPLES="$TGT_TAG" \ 319 KIT_TARGETS_EXT="" KIT_READ_CASE=hostas_read_case KIT_PARALLELIZABLE="$PAR" \ 320 kit_corpus_run 321 done 322 shopt -u nullglob 323 324 printf '\n' 325 if [ "$tgt_run" -eq 0 ]; then 326 printf 'hostas-cross: no target ran (all SKIP-TGT) — needs a clang cross target + podman/qemu + a working cc -S|as for some ELF arch.\n' 327 fi 328 329 kit_summary test-hostas-cross 330 kit_exit