|
| 1 | +#!/usr/bin/env bash |
| 2 | +# requires: llvm unix-shell qemu-arm |
| 3 | +# ARMv7-A: the first 32-bit row in the table with a memory management unit. |
| 4 | +# |
| 5 | +# ⚠️ THE ROW IS NOT A SECOND SPELLING OF THE M ROWS, AND THIS TEST IS WHERE THAT |
| 6 | +# BECOMES CHECKABLE. Every other 32-bit target here is M-profile — an MPU that |
| 7 | +# describes regions by base and limit, with no page-table entry at all — so it |
| 8 | +# is the one machine class on which an address-space abstraction has never been |
| 9 | +# asked what a 32-bit entry looks like. |
| 10 | +# |
| 11 | +# ⚠️ AND THE SEMIHOSTING EXIT CALL IS SPELLED DIFFERENTLY FROM M-PROFILE. |
| 12 | +# `SYS_EXIT` (0x18) on AArch32 takes the reason code in `r1` DIRECTLY; the |
| 13 | +# `{reason, code}` block a Cortex-M board passes is `SYS_EXIT_EXTENDED` (0x20). |
| 14 | +# Measured: passing the block to 0x18 prints correctly and then reports the |
| 15 | +# wrong exit status. The assertion below reads the STATUS, not only the output, |
| 16 | +# which is what makes that difference visible here rather than on a board. |
| 17 | +set -e |
| 18 | + |
| 19 | +MCPP="${MCPP:-mcpp}" |
| 20 | +work="$(mktemp -d)" |
| 21 | +trap 'rm -rf "$work"' EXIT |
| 22 | + |
| 23 | +qemu_arm() { |
| 24 | + local d c |
| 25 | + for d in "${MCPP_HOME:-$HOME/.mcpp}/registry" "$HOME/.xlings"; do |
| 26 | + c=$(ls "$d"/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm 2>/dev/null | sort -V | tail -1) |
| 27 | + [ -n "$c" ] && [ -x "$c" ] && { echo "$c"; return 0; } |
| 28 | + done |
| 29 | + command -v qemu-system-arm 2>/dev/null && return 0 |
| 30 | + return 1 |
| 31 | +} |
| 32 | +QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 0; } |
| 33 | + |
| 34 | +llvm_tool() { |
| 35 | + local c |
| 36 | + c=$(ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-llvm/*/bin/"$1" 2>/dev/null | sort -V | tail -1) |
| 37 | + [ -n "$c" ] && { echo "$c"; return 0; } |
| 38 | + command -v "$1" 2>/dev/null |
| 39 | +} |
| 40 | +NM="$(llvm_tool llvm-nm)" || { echo "SKIP: llvm-nm not found"; exit 0; } |
| 41 | +OBJDUMP="$(llvm_tool llvm-objdump)" || { echo "SKIP: llvm-objdump not found"; exit 0; } |
| 42 | + |
| 43 | +mkdir -p "$work/soc/src" |
| 44 | +cd "$work/soc" |
| 45 | +printf '[package]\nname = "soc"\nversion = "0.1.0"\n' > mcpp.toml |
| 46 | + |
| 47 | +cat > src/start.S <<'ASM' |
| 48 | +.section .text.start,"ax" |
| 49 | +.global _start |
| 50 | +_start: |
| 51 | + ldr sp, =__stack_top |
| 52 | + bl kmain |
| 53 | +1: b 1b |
| 54 | +ASM |
| 55 | + |
| 56 | +cat > src/main.cpp <<'CPP' |
| 57 | +namespace { |
| 58 | +inline void sh(int op, const void* a) { |
| 59 | + register int r0 __asm__("r0") = op; |
| 60 | + register const void* r1 __asm__("r1") = a; |
| 61 | + __asm__ volatile("svc 0x123456" :: "r"(r0), "r"(r1) : "memory"); |
| 62 | +} |
| 63 | +} |
| 64 | +// Referenced by nothing. --gc-sections must remove it. |
| 65 | +extern "C" void collected_because_nothing_calls_it() { sh(0x04, (void*)"UNREACHABLE\n"); } |
| 66 | +
|
| 67 | +extern "C" void kmain() { |
| 68 | + sh(0x04, (void*)"armv7a ok\n"); |
| 69 | + // ADP_Stopped_ApplicationExit, in r1 itself — see the header note. |
| 70 | + sh(0x18, (void*)0x20026); |
| 71 | + for (;;) {} |
| 72 | +} |
| 73 | +CPP |
| 74 | + |
| 75 | +cat > build.mcpp <<'BUILD' |
| 76 | +import mcpp; |
| 77 | +int main() { mcpp::link_script("link.ld"); return 0; } |
| 78 | +BUILD |
| 79 | + |
| 80 | +# qemu `-M virt` puts RAM at 0x40000000. A board fact, written here. |
| 81 | +cat > link.ld <<'LD' |
| 82 | +ENTRY(_start) |
| 83 | +MEMORY { RAM (rwx) : ORIGIN = 0x40000000, LENGTH = 16M } |
| 84 | +SECTIONS { |
| 85 | + .text : { KEEP(*(.text.start)) *(.text*) *(.rodata*) } > RAM |
| 86 | + .data : { *(.data*) } > RAM |
| 87 | + .bss : { *(.bss*) *(COMMON) } > RAM |
| 88 | + . = ALIGN(16); |
| 89 | + __stack_top = ORIGIN(RAM) + LENGTH(RAM); |
| 90 | +} |
| 91 | +LD |
| 92 | + |
| 93 | +ran=0 |
| 94 | +boot_row() { |
| 95 | + local triple=$1 |
| 96 | + rm -rf target |
| 97 | + "$MCPP" build --target "$triple" >/dev/null 2>&1 || { |
| 98 | + echo "FAIL: $triple did not build"; exit 1; } |
| 99 | + local elf |
| 100 | + elf=$(find target -type f -name soc | head -1) |
| 101 | + [ -n "$elf" ] || { echo "FAIL: $triple produced no artefact"; exit 1; } |
| 102 | + if [ "$("$NM" "$elf" | grep -c collected_because_nothing_calls_it)" != "0" ]; then |
| 103 | + echo "FAIL: $triple kept a function nothing calls (--gc-sections not applied)"; exit 1 |
| 104 | + fi |
| 105 | + local out rc |
| 106 | + # ⚠️ NOT `qemu | head`, AND THAT IS THE WHOLE POINT OF THE STATUS CHECK. |
| 107 | + # `$?` after a pipeline is the LAST command's status, so piping into `head` |
| 108 | + # would read head's 0 and the exit assertion below would be vacuous — this |
| 109 | + # repository has shipped that shape before. The output goes to a file and |
| 110 | + # the status is taken from qemu itself. |
| 111 | + set +e |
| 112 | + timeout 30 "$QEMU" -M virt -cpu cortex-a15 -nographic -semihosting \ |
| 113 | + -no-reboot -kernel "$elf" > qemu.log 2>&1 |
| 114 | + rc=$? |
| 115 | + set -e |
| 116 | + out=$(head -3 qemu.log) |
| 117 | + case "$out" in |
| 118 | + *"armv7a ok"*) ;; |
| 119 | + *) echo "FAIL: $triple did not boot; got: $out"; exit 1 ;; |
| 120 | + esac |
| 121 | + # ⭐ THE EXIT STATUS, NOT ONLY THE OUTPUT. A program that prints and then |
| 122 | + # gets its exit call wrong is exactly what this row's semihosting note is |
| 123 | + # about, and only the status tells the two apart. |
| 124 | + [ "$rc" = "0" ] || { echo "FAIL: $triple booted but exited $rc"; exit 1; } |
| 125 | + echo " ok $triple booted on virt/cortex-a15 and exited 0" |
| 126 | + ran=$((ran + 1)) |
| 127 | +} |
| 128 | + |
| 129 | +boot_row armv7a-none-eabi |
| 130 | +boot_row armv7a-none-eabihf |
| 131 | +[ "$ran" = "2" ] || { echo "FAIL: expected 2 rows to boot, got $ran"; exit 1; } |
| 132 | + |
| 133 | +# ── The float ABI, both sides, on this architecture ──────────────────────── |
| 134 | +# |
| 135 | +# ⚠️ MEASURED HERE RATHER THAN CARRIED OVER FROM M-PROFILE. `armv7-a` is a |
| 136 | +# different architecture from `thumbv7em`; that the soft ABI still reaches the |
| 137 | +# FPU there says nothing about here. It does — measured — and the row carries |
| 138 | +# `-mfpu=none` for it. |
| 139 | +mkdir -p "$work/fp/src" |
| 140 | +cd "$work/fp" |
| 141 | +cp "$work/soc/build.mcpp" "$work/soc/link.ld" . |
| 142 | +cp "$work/soc/src/start.S" src/ |
| 143 | +printf '[package]\nname = "fp"\nversion = "0.1.0"\n' > mcpp.toml |
| 144 | +cat > src/main.cpp <<'CPP' |
| 145 | +volatile float fa = 3.0f, fb = 4.0f, fout = 0.0f; |
| 146 | +extern "C" void kmain() { fout = fa * fb + 1.0f; for (;;) {} } |
| 147 | +CPP |
| 148 | + |
| 149 | +rm -rf target |
| 150 | +"$MCPP" build --target armv7a-none-eabihf >/dev/null 2>&1 || { |
| 151 | + echo "FAIL: the hard-float row did not build"; exit 1; } |
| 152 | +hard=$("$OBJDUMP" -d "$(find target -type f -name fp | head -1)" \ |
| 153 | + | grep -cE '\bv[a-z]+\.f32' || true) |
| 154 | +[ "$hard" -gt 0 ] || { |
| 155 | + echo "FAIL: armv7a-none-eabihf emitted no FPU instruction — the control is vacuous"; exit 1; } |
| 156 | +echo " ok armv7a-none-eabihf uses the FPU ($hard instructions)" |
| 157 | + |
| 158 | +# The soft row's proof is its link failure against `__aeabi_fmul`: with |
| 159 | +# `-mfpu=none` the multiply lowers onto a libcall, and this tier has no C |
| 160 | +# library and no builtins for it to resolve against. |
| 161 | +rm -rf target |
| 162 | +soft_out=$("$MCPP" build --target armv7a-none-eabi 2>&1 || true) |
| 163 | +case "$soft_out" in |
| 164 | + *__aeabi_fmul*) echo " ok armv7a-none-eabi lowered the multiply onto a libcall, not the FPU" ;; |
| 165 | + *) echo "FAIL: armv7a-none-eabi did not reference __aeabi_fmul; -mfpu=none may not be applied" |
| 166 | + echo "$soft_out" | tail -5 | sed 's/^/ /'; exit 1 ;; |
| 167 | +esac |
| 168 | + |
| 169 | +echo "PASS: armv7-a rows build, boot, exit cleanly and honour the float ABI" |
0 commit comments