diff --git a/scripts/m2/args.c b/scripts/m2/args.c new file mode 100644 index 0000000..0628482 --- /dev/null +++ b/scripts/m2/args.c @@ -0,0 +1,3 @@ +__attribute__((noinline)) int add2(int a, int b) { return a + b; } +__attribute__((noinline)) int first(int a, int b) { return a; } +__attribute__((noinline)) int second(int a, int b) { return b; } diff --git a/scripts/m2/caller.c b/scripts/m2/caller.c new file mode 100644 index 0000000..ca6e279 --- /dev/null +++ b/scripts/m2/caller.c @@ -0,0 +1,2 @@ +extern int add2(int, int); +int caller(void) { return add2(20, 22); } diff --git a/scripts/m2/check.c b/scripts/m2/check.c new file mode 100644 index 0000000..aacf620 --- /dev/null +++ b/scripts/m2/check.c @@ -0,0 +1,3 @@ +/* First ROM call: strlen("0::0") must be 4. */ +extern int strlen(const char *); +int check(void) { return strlen("0::0"); } diff --git a/scripts/m2/dis-roundtrip.sh b/scripts/m2/dis-roundtrip.sh new file mode 100755 index 0000000..5444bdd --- /dev/null +++ b/scripts/m2/dis-roundtrip.sh @@ -0,0 +1,326 @@ +#!/bin/sh +# M2 dis34 round-trip. +# +# scripts/m2/dis-roundtrip.sh +# +# Extracts the linked .text/.data from build/m2/m2.coff (using the COFF +# section table, not hard-coded file offsets), disassembles the text with the +# VT1200 tree's dis34 at the linked bit address, and checks that GSPA encoded +# the TI-ABI code llc printed: +# +# _strlen JAUC 3FB458D0 (the ROM stub, ABI-preserving) +# _first MOVE A13,A? ; ADDI -32,A? ; RETS 2 +# _second ADDI -64,A? ; RETS 2 +# _keep old-STK push, argument pushes, CALLA, SETF, LOADSTK, RETS 2 +# _locals nonzero frame ADDI, a frame access, RETS 0 +# +# It also checks the old-STK operand against the number of pushes before +# SETFP, and the .data bytes contain the "0::0" string literal. +# +# Overridable: M2_COFF DIS34 +set -eu + +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +COFF=${M2_COFF:-$ROOT/build/m2/m2.coff} +DIS34=${DIS34:-$ROOT/vendor/vt1200/tools/smart34010/dis34} + +fail() { + echo "M2 dis34 round-trip: FAIL: $*" >&2 + exit 1 +} + +[ -f "$COFF" ] || fail "linked COFF not found: $COFF (run scripts/m2/run.sh)" +[ -x "$DIS34" ] || fail "dis34 not found or not executable: $DIS34" + +TMP=$(mktemp -d "${TMPDIR:-/tmp}/m2-dis34.XXXXXX") +FAIL_LOG=${M2_DIS_FAIL_LOG:-${TMPDIR:-/tmp}/m2-dis34-failure.txt} +trap 'rm -rf "$TMP"' EXIT HUP INT TERM + +# Read little-endian fields out of the COFF. od keeps this dependency-free. +read_u8s() { + od -An -v -tu1 -j "$1" -N "$2" "$COFF" +} +u16() { + read_u8s "$1" 2 | awk '{ print $1 + 256 * $2 }' +} +u32() { + read_u8s "$1" 4 | awk '{ print $1 + 256 * $2 + 65536 * $3 + 16777216 * $4 }' +} + +echo "== parse the COFF section table ($COFF)" +nscns=$(u16 2) +opthdr=$(u16 16) +[ "$nscns" -gt 0 ] 2>/dev/null || fail "COFF has no sections (nscns='$nscns')" +[ "$opthdr" -gt 0 ] 2>/dev/null || fail "COFF has no optional header (opthdr='$opthdr')" + +TEXT_ADDR= +TEXT_BITS= +TEXT_PTR= +DATA_ADDR= +DATA_BITS= +DATA_PTR= +sh_base=$((20 + opthdr)) +i=0 +while [ "$i" -lt "$nscns" ]; do + o=$((sh_base + i * 40)) + name=$(dd if="$COFF" bs=1 skip="$o" count=8 2>/dev/null | tr -d '\000') + if [ "$name" = ".text" ]; then + TEXT_ADDR=$(u32 $((o + 8))) + TEXT_BITS=$(u32 $((o + 16))) + TEXT_PTR=$(u32 $((o + 20))) + elif [ "$name" = ".data" ]; then + DATA_ADDR=$(u32 $((o + 8))) + DATA_BITS=$(u32 $((o + 16))) + DATA_PTR=$(u32 $((o + 20))) + fi + i=$((i + 1)) +done +[ -n "$TEXT_ADDR" ] || fail "COFF has no .text section" +[ "$TEXT_BITS" -gt 0 ] 2>/dev/null || fail ".text is empty" +[ $((TEXT_BITS % 8)) -eq 0 ] || fail ".text size $TEXT_BITS bits is not whole bytes" +[ -n "$DATA_ADDR" ] || fail "COFF has no .data section" +[ "$DATA_BITS" -gt 0 ] 2>/dev/null || fail ".data is empty" +[ $((DATA_BITS % 8)) -eq 0 ] || fail ".data size $DATA_BITS bits is not whole bytes" + +TEXT_HEX=$(printf '%08X' "$TEXT_ADDR") +ENTRY=$(u32 $((20 + 16))) +ENTRY_HEX=$(printf '%08X' "$ENTRY") +TEXT_BYTES=$((TEXT_BITS / 8)) +DATA_BYTES=$((DATA_BITS / 8)) + +[ "$TEXT_HEX" = "018A0000" ] \ + || fail ".text is linked at $TEXT_HEX, want 018A0000" +[ "$ENTRY_HEX" = "018A0000" ] \ + || fail "COFF entry point is $ENTRY_HEX, want 018A0000" +[ "$TEXT_PTR" -gt 0 ] || fail ".text has no raw data (scnptr=0)" +[ "$DATA_PTR" -gt 0 ] || fail ".data has no raw data (scnptr=0)" + +echo " .text at $TEXT_HEX: $TEXT_BITS bits ($TEXT_BYTES bytes), scnptr=$TEXT_PTR" +echo " .data at $(printf '%08X' "$DATA_ADDR"): $DATA_BITS bits ($DATA_BYTES bytes), scnptr=$DATA_PTR" +echo " entry point $ENTRY_HEX" + +dd if="$COFF" of="$TMP/text.bin" bs=1 skip="$TEXT_PTR" count="$TEXT_BYTES" 2>/dev/null +[ "$(wc -c < "$TMP/text.bin")" -eq "$TEXT_BYTES" ] \ + || fail "extracted $(wc -c < "$TMP/text.bin") .text bytes, want $TEXT_BYTES" +dd if="$COFF" of="$TMP/data.bin" bs=1 skip="$DATA_PTR" count="$DATA_BYTES" 2>/dev/null +[ "$(wc -c < "$TMP/data.bin")" -eq "$DATA_BYTES" ] \ + || fail "extracted $(wc -c < "$TMP/data.bin") .data bytes, want $DATA_BYTES" + +echo "== resolve the linked symbols from the COFF symbol table" +symptr=$(u32 8) +nsyms=$(u32 12) +[ "$symptr" -gt 0 ] 2>/dev/null || fail "COFF has no symbol table" +[ "$nsyms" -gt 0 ] 2>/dev/null || fail "COFF symbol table is empty" + +sym_addr() { + _want=$1 + _i=0 + while [ "$_i" -lt "$nsyms" ]; do + _o=$((symptr + _i * 18)) + _n=$(dd if="$COFF" bs=1 skip="$_o" count=8 2>/dev/null | tr -d '\000') + if [ "$_n" = "$_want" ]; then + u32 $((_o + 8)) + return 0 + fi + _aux=$(od -An -v -tu1 -j $((_o + 17)) -N 1 "$COFF" | awk '{ print $1 }') + _i=$((_i + 1 + _aux)) + done + return 1 +} + +get_sym() { + _name=$1 + _addr=$(sym_addr "$_name") || fail "COFF symbol table has no $_name" + [ -n "$_addr" ] || fail "COFF symbol table has no $_name" + printf '%s' "$_addr" +} + +STRLEN=$(get_sym _strlen) +FIRST=$(get_sym _first) +SECOND=$(get_sym _second) +KEEP=$(get_sym _keep) +LOCALS=$(get_sym _locals) +ADD2=$(get_sym _add2) +STRLEN_HEX=$(printf '%08X' "$STRLEN") +FIRST_HEX=$(printf '%08X' "$FIRST") +SECOND_HEX=$(printf '%08X' "$SECOND") +KEEP_HEX=$(printf '%08X' "$KEEP") +LOCALS_HEX=$(printf '%08X' "$LOCALS") +ADD2_HEX=$(printf '%08X' "$ADD2") +echo " _strlen=$STRLEN_HEX _first=$FIRST_HEX _second=$SECOND_HEX" +echo " _add2=$ADD2_HEX _keep=$KEEP_HEX _locals=$LOCALS_HEX" + +echo "== disassemble the emitted .text with dis34" +"$DIS34" -b "$TEXT_HEX" -l -o "$TMP/text.bin" > "$TMP/dis34.txt" 2> "$TMP/dis34.err" \ + || { cat "$TMP/dis34.err" >&2; fail "dis34 failed"; } +[ -s "$TMP/dis34.txt" ] || fail "dis34 produced no output" + +# match_at FILE DECIMAL_ADDR ERE +# dis34 -l prints "018a05c0:\tmove a13, a0\t; 4da0". The operand must be +# followed by optional whitespace and then the comment/end-of-line marker, so +# "rets 1" cannot satisfy a "rets" pattern. +match_at() { + _file=$1 + _addr=$2 + _pat=$3 + _laddr=$(printf '%08x' "$_addr") + grep -Eiq "^${_laddr}:[[:space:]]+${_pat}([[:space:]]*(;|$))" "$_file" +} + +# expect_at LABEL DECIMAL_ADDR EXPECTED_TEXT ERE +expect_at() { + _label=$1 + _addr=$2 + _want=$3 + _pat=$4 + _laddr=$(printf '%08x' "$_addr") + if match_at "$TMP/dis34.txt" "$_addr" "$_pat"; then + echo " ok $_label @ 0x$_laddr: $_want" + return 0 + fi + { + echo "M2 dis34 round-trip: FAIL" + echo " $_label at bit 0x$_laddr: expected $_want" + echo " dis34 output near 0x$_laddr:" + awk -v a="$_laddr" ' + { low = tolower($0) } + low ~ ("^" a ":") { seen = 1 } + seen { print " " $0; n++ } + seen && n >= 4 { exit } + ' "$TMP/dis34.txt" + if cp "$TMP/dis34.txt" "$FAIL_LOG" 2>/dev/null; then + echo " full disassembly preserved at: $FAIL_LOG" + else + echo " full disassembly: $TMP/dis34.txt (temporary; removed on exit)" + fi + } >&2 + exit 1 +} + +# func_body DECIMAL_ADDR > FILE: the function's instructions, through RETS. +func_body() { + _laddr=$(printf '%08x' "$1") + awk -v a="$_laddr" ' + { low = tolower($0) } + low ~ ("^" a ":") { on = 1 } + on { print } + on && low ~ /^[0-9a-f]+:[[:space:]]+rets([[:space:]]|;|$)/ { exit } + ' "$TMP/dis34.txt" +} + +# expect_in BODY EXPECTED_TEXT ERE +expect_in() { + _file=$1 + _want=$2 + _pat=$3 + if grep -Eiq "$_pat" "$_file"; then + echo " ok $_want" + return 0 + fi + echo "M2 dis34 round-trip: FAIL: missing $_want" >&2 + sed 's/^/ /' "$_file" >&2 + exit 1 +} + +# expect_seq BODY REGEX...: each pattern must occur, in order, after the last. +expect_seq() { + _file=$1 + shift + _last=0 + for _pat in "$@"; do + _line=$(awk -v start="$_last" -v pat="$_pat" ' + NR > start && tolower($0) ~ pat { print NR; exit } + ' "$_file") + [ -n "$_line" ] || { + echo "M2 dis34 round-trip: FAIL: call sequence missing /$_pat/" >&2 + sed 's/^/ /' "$_file" >&2 + exit 1 + } + _last=$_line + done + echo " ok call sequence ($# instructions/operands, in order)" +} + +echo "== check the linked instructions" +# ROM import stub: absolute unconditional jump, ABI-preserving. +expect_at "_strlen" "$STRLEN" "JAUC 3FB458D0" \ + 'jauc[[:space:]]+0x3fb458d0' + +# _first must materialise its frame pointer and load argument 0 at FP(-32). +func_body "$FIRST" > "$TMP/first.txt" +expect_in "$TMP/first.txt" "_first MOVEFP (MOVE A13,A?)" \ + 'move[[:space:]]+a13,[[:space:]]*a[0-9]+' +expect_in "$TMP/first.txt" "_first ADDI -32" \ + 'addi[[:space:]]+(0xffe0|-0x20|-32),' +expect_in "$TMP/first.txt" "_first RETS 2" \ + 'rets[[:space:]]+2([[:space:]]*(;|$))' + +# _second loads argument 1 at FP(-64). +func_body "$SECOND" > "$TMP/second.txt" +expect_in "$TMP/second.txt" "_second ADDI -64" \ + 'addi[[:space:]]+(0xffc0|-0x40|-64),' +expect_in "$TMP/second.txt" "_second RETS 2" \ + 'rets[[:space:]]+2([[:space:]]*(;|$))' + +# _keep exercises the whole call/return path against _add2. +func_body "$KEEP" > "$TMP/keep.txt" +expect_seq "$TMP/keep.txt" \ + 'move[[:space:]]+a14,[[:space:]]*-\*sp,[[:space:]]*1' \ + 'move[[:space:]]+[^,]+,[[:space:]]*\*a14\+,[[:space:]]*1' \ + 'calla' \ + 'setf[[:space:]]+32,[[:space:]]*0,[[:space:]]*1' \ + 'move[[:space:]]+\*sp\(0x[0-9a-f]+\),[[:space:]]*a14,[[:space:]]*1' \ + 'rets[[:space:]]+2([[:space:]]*(;|$))' + +# _locals has a nonzero frame allocation, a frame access and RETS 0. +func_body "$LOCALS" > "$TMP/locals.txt" +expect_in "$TMP/locals.txt" "_locals frame ADDI" \ + 'addi[[:space:]]+0x0*[1-9a-f][0-9a-f]*,[[:space:]]*a14' +expect_in "$TMP/locals.txt" "_locals frame access" \ + 'move[[:space:]]+(a[0-9]+,[[:space:]]*\*a[0-9]+|\*a[0-9]+,[[:space:]]*a[0-9]+)' +expect_in "$TMP/locals.txt" "_locals RETS 0" \ + 'rets([[:space:]]+0)?([[:space:]]*(;|$))' + +# Old-STK offset: the LOADSTK operand must count every callee-saved push +# before SETFP, plus the caller's pushed old STK. This is the check the M2 +# lit tests deliberately leave to dis34. +echo "== check the old-STK restore offsets" +check_stk() { + _label=$1 + _addr=$2 + func_body "$_addr" > "$TMP/func.txt" + _pushes=$(awk ' + tolower($0) ~ /move[[:space:]]+[a-z0-9]+,[[:space:]]*-\*sp,[[:space:]]*1/ { n++ } + tolower($0) ~ /move[[:space:]]+a14,[[:space:]]*a13/ { print n; exit } + ' "$TMP/func.txt") + [ -n "$_pushes" ] || fail "$_label has no SETFP (MOVE A14,A13)" + _operand=$(grep -Eio '\*sp\(0x[0-9a-f]+\)' "$TMP/func.txt" | head -1 \ + | sed 's/.*0x//; s/).*//') + [ -n "$_operand" ] || fail "$_label has no LOADSTK (*SP(n),A14)" + _want=$(( (_pushes + 1) * 32 )) + _got=$((0x$_operand)) + [ "$_got" -eq "$_want" ] \ + || fail "$_label restores A14 from *SP(0x$_operand) ($_got bits) but $_pushes pushes need $_want bits" + echo " ok $_label: $_pushes pushes -> *SP($(printf '0x%x' "$_want")), A14, 1" +} +check_stk "_add2" "$ADD2" +check_stk "_first" "$FIRST" +check_stk "_second" "$SECOND" +check_stk "_keep" "$KEEP" + +# Negative control for the RETS operand boundary: "rets 1" must not match. +printf '%08x:\trets\t1\t; 0961\n' "$ADD2" > "$TMP/rets-neg.txt" +if match_at "$TMP/rets-neg.txt" "$ADD2" 'rets([[:space:]]+0)?'; then + fail "negative control: the RETS pattern accepted 'rets 1'" +fi +echo " ok negative control @ 0x$(printf '%08x' "$ADD2"): 'rets 1' rejected" + +echo "== check the copied .data bytes" +# The check.c string literal "0::0\0" must survive llc -> GSPA -> COFF. +DATA_HEX=$(od -An -v -tx1 "$TMP/data.bin" | tr -d ' \n') +case "$DATA_HEX" in +*303a3a3000*) echo " ok .data contains 30 3A 3A 30 00 (\"0::0\")" ;; +*) fail ".data does not contain 30 3A 3A 30 00 (got $DATA_HEX)" ;; +esac + +echo "M2 dis34 round-trip: PASS" diff --git a/scripts/m2/entry.asm b/scripts/m2/entry.asm new file mode 100644 index 0000000..e17c09e --- /dev/null +++ b/scripts/m2/entry.asm @@ -0,0 +1,59 @@ +; entry.asm -- M2 TI ABI smoke-test entry, linked at bit 018A0000. +; +; Markers (0x20 bits apart): +; 018A8000 check() strlen("0::0") == 4 +; 018A8020 first(20,22) == 20 +; 018A8040 second(20,22) == 22 +; 018A8060 caller() add2(20,22) == 42 +; 018A8080 locals() == 7 +; 018A80A0 keep(10) == 16 (g == 5 before the call, + 11) + .text + .globl _start +_start: + setf 32, 0, 1 + setf 16, 0, 0 + movi _cstack, a14 + move a14, a13 + + calla _check + movi 018A8000h, a2 + move a8, *a2, 1 + + ; first(20, 22): last argument first, on the program stack + move a14, -*sp, 1 + movi 22, a0 + move a0, *a14+, 1 + movi 20, a0 + move a0, *a14+, 1 + calla _first + movi 018A8020h, a2 + move a8, *a2, 1 + + ; second(20, 22) + move a14, -*sp, 1 + movi 22, a0 + move a0, *a14+, 1 + movi 20, a0 + move a0, *a14+, 1 + calla _second + movi 018A8040h, a2 + move a8, *a2, 1 + + calla _caller + movi 018A8060h, a2 + move a8, *a2, 1 + + calla _locals + movi 018A8080h, a2 + move a8, *a2, 1 + + move a14, -*sp, 1 + movi 10, a0 + move a0, *a14+, 1 + calla _keep + movi 018A80A0h, a2 + move a8, *a2, 1 + + rets + + .bss _cstack, 32768, 16 ; 32768 bits = 4096 bytes diff --git a/scripts/m2/gen-rom-imports.sh b/scripts/m2/gen-rom-imports.sh new file mode 100755 index 0000000..b0cfe7f --- /dev/null +++ b/scripts/m2/gen-rom-imports.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Generate absolute-jump stubs for the ROM entry points in a per-ROM symbol file. +# Usage: gen-rom-imports.sh symbols/vt1200-v22.syms OUT.asm +set -eu +SYMS=${1:?symbol file} +OUT=${2:?output file} +printf '\t.text\n' > "$OUT" +awk ' + /^[[:space:]]*#/ || NF < 2 { next } + { + name = $1 + addr = $2 + sub(/^0x/, "", addr) + printf "\t.globl _%s\n_%s:\n\tjauc %sh\n", name, name, addr + } +' "$SYMS" >> "$OUT" diff --git a/scripts/m2/keep.c b/scripts/m2/keep.c new file mode 100644 index 0000000..8073c23 --- /dev/null +++ b/scripts/m2/keep.c @@ -0,0 +1,3 @@ +extern int add2(int, int); +int g = 5; /* initialized .data: also tests the loader copy */ +int keep(int a) { int x = g; int y = add2(a, 1); return x + y; } diff --git a/scripts/m2/link.cmd b/scripts/m2/link.cmd new file mode 100644 index 0000000..7406ca5 --- /dev/null +++ b/scripts/m2/link.cmd @@ -0,0 +1,16 @@ +-a +-o M2.OUT +-m M2.MAP +-e _start +M2.obj +MEMORY +{ + prog: origin = 018A0000h, length = 8000h + ram: origin = 018A9000h, length = 40000h +} +SECTIONS +{ + .text: {} > prog + .data: {} > prog + .bss: {} > ram +} diff --git a/scripts/m2/locals.c b/scripts/m2/locals.c new file mode 100644 index 0000000..7a59455 --- /dev/null +++ b/scripts/m2/locals.c @@ -0,0 +1 @@ +int locals(void) { volatile int x = 7; return x; } diff --git a/scripts/m2/m2-check.lua b/scripts/m2/m2-check.lua new file mode 100644 index 0000000..d8ae546 --- /dev/null +++ b/scripts/m2/m2-check.lua @@ -0,0 +1,51 @@ +-- m2-check.lua -- load the M2 COFF with dwt/loader/loader.lua and read the +-- marker words the compiled TI-ABI code wrote. +-- +-- M2_LOADER_LUA path to vendor/vt1200/dwt/loader/loader.lua +-- M2_EXPECT_CHECK 4 (strlen("0::0")) +-- M2_EXPECT_FIRST 20 (first(20,22)) +-- M2_EXPECT_SECOND 22 (second(20,22)) +-- M2_EXPECT_CALLER 42 (add2(20,22)) +-- M2_EXPECT_LOCALS 7 +-- M2_EXPECT_KEEP 16 (g == 5, + keep(10)) +-- +-- The marker read runs one frame before loader.lua's inspection/exit frame. + +dofile(os.getenv("M2_LOADER_LUA")) + +local ENDF = tonumber(os.getenv("LOADER_END") or "2700") +local EXPECT_CHECK = tonumber(os.getenv("M2_EXPECT_CHECK") or "4") +local EXPECT_FIRST = tonumber(os.getenv("M2_EXPECT_FIRST") or "20") +local EXPECT_SECOND = tonumber(os.getenv("M2_EXPECT_SECOND") or "22") +local EXPECT_CALLER = tonumber(os.getenv("M2_EXPECT_CALLER") or "42") +local EXPECT_LOCALS = tonumber(os.getenv("M2_EXPECT_LOCALS") or "7") +local EXPECT_KEEP = tonumber(os.getenv("M2_EXPECT_KEEP") or "16") +local frames = 0 +local checked = false + +emu.register_frame_done(function() + frames = frames + 1 + if frames == ENDF - 1 and not checked then + checked = true + local prog = manager.machine.devices[':maincpu'].spaces['program'] + local check = prog:read_u32(0x018A8000) + local first = prog:read_u32(0x018A8020) + local second = prog:read_u32(0x018A8040) + local caller = prog:read_u32(0x018A8060) + local locals = prog:read_u32(0x018A8080) + local keep = prog:read_u32(0x018A80A0) + print(string.format( + "M2: check=%d first=%d second=%d caller=%d locals=%d keep=%d", + check, first, second, caller, locals, keep)) + if check == EXPECT_CHECK and first == EXPECT_FIRST + and second == EXPECT_SECOND and caller == EXPECT_CALLER + and locals == EXPECT_LOCALS and keep == EXPECT_KEEP then + print("M2 MAME: PASS") + else + print(string.format( + "M2 MAME: FAIL (want check=%d first=%d second=%d caller=%d locals=%d keep=%d)", + EXPECT_CHECK, EXPECT_FIRST, EXPECT_SECOND, EXPECT_CALLER, + EXPECT_LOCALS, EXPECT_KEEP)) + end + end +end, "m2-check") diff --git a/scripts/m2/run.sh b/scripts/m2/run.sh new file mode 100755 index 0000000..d327235 --- /dev/null +++ b/scripts/m2/run.sh @@ -0,0 +1,171 @@ +#!/bin/sh +# M2 end-to-end smoke test: TI ABI + a real ROM strlen call. +# +# scripts/m2/run.sh +# +# Compiles five C translation units with scripts/tms-clang + llc -O0, assembles +# and links them with the 1987 GSPA/GSPLNK 2.00 under DOSBox, then loads the +# COFF into MAME through dwt/loader/loader.lua and checks the marker words: +# +# check() strlen("0::0") == 4 +# first(20,22) == 20 +# second(20,22) == 22 +# caller() add2(20,22) == 42 +# locals() == 7 +# keep(10) == 16 +# +# Overridable: TMS_CLANG TMS_OPT TMS_BITADDR TMS_LLC MAME DOSBOX DOSBOX_CONF +# SDK_IMG M2_WORK M2_COFF M2_SDK LOADER_AT LOADER_END +set -eu + +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +DIR=$ROOT/scripts/m2 + +LLVM_BIN=${LLVM_BIN:-$ROOT/build/llvm-clean-build/bin} +TMS_CLANG=${TMS_CLANG:-$LLVM_BIN/clang} +TMS_OPT=${TMS_OPT:-$LLVM_BIN/opt} +TMS_BITADDR=${TMS_BITADDR:-$ROOT/tools/tms-bitaddr/tms-bitaddr} +TMS_LLC=${TMS_LLC:-$LLVM_BIN/llc} + +MAME=${MAME:-$ROOT/build/mame-src/vt1200} +DOSBOX=${DOSBOX:-dosbox} +DOSBOX_CONF=${DOSBOX_CONF:-$ROOT/vendor/vt1200/harness/sdk-scratch/dosbox-bench.conf} +SDK_IMG=${SDK_IMG:-$ROOT/vendor/vt1200/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img} + +WORK=${M2_WORK:-$ROOT/build/m2} +COFF=${M2_COFF:-$WORK/m2.coff} +SDK=${M2_SDK:-$ROOT/build/m2-sdk} +ASM=$WORK/asm +LOADER_LUA=$ROOT/vendor/vt1200/dwt/loader/loader.lua +LOADER_BIN=$ROOT/vendor/vt1200/dwt/loader/build/loader.bin +LOADER_AT=${LOADER_AT:-2500} +LOADER_END=${LOADER_END:-2700} + +SYMS=${M2_SYMS:-$ROOT/symbols/vt1200-v22.syms} + +fail() { + echo "m2: $*" >&2 + exit 1 +} + +for tool in "$TMS_CLANG" "$TMS_OPT" "$TMS_BITADDR" "$TMS_LLC"; do + [ -x "$tool" ] || fail "missing tool: $tool" +done + +mkdir -p "$ASM" "$WORK/evidence" "$WORK/run/nvram" "$WORK/run/cfg" +mkdir -p "$(dirname "$COFF")" + +echo "== compile and select" +export TMS_CLANG TMS_OPT TMS_BITADDR +for name in check args caller locals keep; do + "$ROOT/scripts/tms-clang" -O2 -o "$ASM/$name.ll" "$DIR/$name.c" + "$TMS_LLC" -march=tms34010 -O0 -filetype=asm \ + "$ASM/$name.ll" -o "$ASM/$name.s" \ + || fail "llc failed for $name.c" + echo " $name.s: $(grep -c . "$ASM/$name.s") lines" +done + +# strlen("0::0") must be a real ROM call, not a folded 4. +grep -q 'CALLA _strlen' "$ASM/check.s" \ + || fail "check.s has no 'CALLA _strlen' (strlen was folded or not called)" +echo " check.s: $(grep -E 'CALLA _strlen' "$ASM/check.s" | head -1 | sed 's/^[[:space:]]*//')" + +echo "== generate the per-ROM import stubs" +"$DIR/gen-rom-imports.sh" "$SYMS" "$WORK/rom-imports.asm" +grep -q '^_strlen:' "$WORK/rom-imports.asm" \ + || fail "rom-imports.asm has no _strlen stub" +grep -A1 '^_strlen:' "$WORK/rom-imports.asm" | grep -qi 'jauc 3FB458D0h' \ + || fail "rom-imports.asm _strlen stub does not jump to 3FB458D0h" +echo " _strlen: $(grep -A1 '^_strlen:' "$WORK/rom-imports.asm" | tail -1 | sed 's/^[[:space:]]*//')" + +echo "== extract the 1987 SDK" +if [ ! -f "$SDK/GSPA.EXE" ]; then + command -v mcopy >/dev/null 2>&1 || fail "mtools (mcopy) is required" + [ -f "$SDK_IMG" ] || fail "SDK image not found: $SDK_IMG" + mkdir -p "$SDK" + mcopy -s -n -i "$SDK_IMG" "::/A2/*" "$SDK/" + mcopy -s -n -i "$SDK_IMG" "::/C2/*" "$SDK/" +fi +[ -f "$SDK/GSPA.EXE" ] || fail "GSPA.EXE not extracted into $SDK" + +echo "== assemble and link with GSPA/GSPLNK under DOSBox" +cat "$DIR/entry.asm" "$ASM/check.s" "$ASM/args.s" "$ASM/caller.s" \ + "$ASM/locals.s" "$ASM/keep.s" "$WORK/rom-imports.asm" > "$SDK/M2.ASM" +# The linker command file must keep CRLF line endings. +sed 's/\r$//' "$DIR/link.cmd" | sed 's/$/\r/' > "$SDK/M2.CMD" +rm -f "$SDK/M2.OBJ" "$SDK/M2.OUT" "$SDK/M2.MAP" "$SDK/M2.LST" + +SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ +timeout "${DOSBOX_TIMEOUT:-300}" "$DOSBOX" -conf "$DOSBOX_CONF" \ + -noprimaryconf -nolocalconf \ + -c "mount c $SDK" -c "c:" \ + -c "gspa M2 M2.obj M2.lst -l" \ + -c "gsplnk M2.cmd" \ + -c "exit" -exit > "$WORK/dosbox.log" 2>&1 \ + || { tail -20 "$WORK/dosbox.log" >&2; fail "DOSBox failed"; } + +[ -f "$SDK/M2.LST" ] || fail "GSPA produced no listing (see $WORK/dosbox.log)" +sed 's/\r$//' "$SDK/M2.LST" > "$WORK/m2.lst" +grep -q 'No Errors' "$WORK/m2.lst" \ + || { grep -i -B2 -A2 'error' "$WORK/m2.lst" >&2 || true; fail "GSPA reported errors"; } +grep -q 'No Warnings' "$WORK/m2.lst" \ + || { grep -i -B2 -A2 'warning' "$WORK/m2.lst" >&2 || true; fail "GSPA reported warnings"; } +[ -f "$SDK/M2.OUT" ] || fail "GSPLNK produced no M2.OUT" +sed 's/\r$//' "$SDK/M2.MAP" > "$WORK/m2.map" +cp "$SDK/M2.OUT" "$COFF" +cp "$SDK/M2.ASM" "$WORK/m2.asm" + +ENTRY=$(awk '$2 == "_start" { print $1; exit }' "$WORK/m2.map") +[ "$ENTRY" = "018A0000" ] || fail "linker entry _start is '$ENTRY', want 018A0000" +grep -q 'ENTRY POINT SYMBOL: "_start"' "$WORK/m2.map" \ + || fail "linker map has no _start entry point" + +BSS_ORIGIN=$(awk '$1 == ".bss" && $2 == "0" { print $3; exit }' "$WORK/m2.map") +BSS_LENGTH=$(awk '$1 == ".bss" && $2 == "0" { print $4; exit }' "$WORK/m2.map") +[ -n "$BSS_ORIGIN" ] || fail "linker map has no .bss section" +[ "$BSS_ORIGIN" = "018A9000" ] \ + || fail "linker .bss origin is $BSS_ORIGIN, want 018A9000 (link.cmd ram origin)" +[ "$BSS_LENGTH" = "00008000" ] \ + || fail ".bss is $BSS_LENGTH bits, expected 00008000 (_cstack)" +DATA_ORIGIN=$(awk '$1 == ".data" && $2 == "0" { print $3; exit }' "$WORK/m2.map") +DATA_LENGTH=$(awk '$1 == ".data" && $2 == "0" { print $4; exit }' "$WORK/m2.map") +[ -n "$DATA_LENGTH" ] || fail "linker map has no .data section" +[ "$DATA_LENGTH" != "00000000" ] || fail ".data is empty (no initialized globals)" +echo " linked .text at 018A0000, .data at $DATA_ORIGIN ($DATA_LENGTH bits), .bss at $BSS_ORIGIN ($BSS_LENGTH bits)" + +echo "== run the COFF in MAME" +[ -x "$MAME" ] || fail "MAME binary not built: $MAME" +[ -f "$LOADER_LUA" ] || fail "COFF loader missing: $LOADER_LUA" +[ -f "$LOADER_BIN" ] || fail "COFF loader payload missing: $LOADER_BIN" + +VT1200_SKIP_LEVEL1=1 \ +LOADER_AT="$LOADER_AT" LOADER_END="$LOADER_END" \ +LOADER_BIN="$LOADER_BIN" LOADER_COFF="$COFF" \ +LOADER_OUT="$WORK/evidence" \ +M2_LOADER_LUA="$LOADER_LUA" \ +M2_EXPECT_CHECK=4 M2_EXPECT_FIRST=20 M2_EXPECT_SECOND=22 \ +M2_EXPECT_CALLER=42 M2_EXPECT_LOCALS=7 M2_EXPECT_KEEP=16 \ +timeout "${MAME_TIMEOUT:-600}" "$MAME" vt1200 \ + -rompath "$ROOT/vendor/vt1200/harness/mame/roms" \ + -video none -sound none -nothrottle -skip_gameinfo \ + -nvram_directory "$WORK/run/nvram" \ + -cfg_directory "$WORK/run/cfg" \ + -debug -debugger none \ + -autoboot_script "$DIR/m2-check.lua" -autoboot_delay 0 \ + > "$WORK/mame.log" 2>&1 \ + || { tail -30 "$WORK/mame.log" >&2; fail "MAME run failed"; } + +MARKERS=$(grep -E '^M2: check=' "$WORK/mame.log" | tail -1 || true) +[ -n "$MARKERS" ] || { tail -30 "$WORK/mame.log" >&2; fail "MAME produced no M2 markers"; } +echo " $MARKERS" + +grep -q 'LOADER: result status=0 (OK)' "$WORK/mame.log" \ + || { grep 'LOADER: result' "$WORK/mame.log" >&2 || true; fail "COFF loader did not report status 0"; } +grep -q 'placed \.text .* 0 differing' "$WORK/mame.log" \ + || fail "COFF loader could not verify the placed .text bytes" +grep -q 'placed \.data .* 0 differing' "$WORK/mame.log" \ + || fail "COFF loader could not verify the placed .data bytes" +grep -q '^M2 MAME: PASS' "$WORK/mame.log" \ + || { tail -30 "$WORK/mame.log" >&2; fail "marker check failed"; } + +echo "M2 MAME: PASS" diff --git a/scripts/tests/m2/dis-roundtrip.sh b/scripts/tests/m2/dis-roundtrip.sh new file mode 100755 index 0000000..c956186 --- /dev/null +++ b/scripts/tests/m2/dis-roundtrip.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# M2 test gate: rebuild the linked COFF through the normal GSPA/GSPLNK path, +# then verify dis34 decodes the TI-ABI instructions at the linked addresses. +set -eu +ROOT=$(cd "$(dirname "$0")/../../.." && pwd) + +# Always rebuild: a stale build/m2/m2.coff would let an asm-encoding +# regression pass without ever touching the compiler or assembler. Keep +# dis-roundtrip.sh pointed at the file this run produces, including when +# M2_WORK/M2_COFF are overridden. +WORK=${M2_WORK:-$ROOT/build/m2} +M2_COFF=${M2_COFF:-$WORK/m2.coff} +export M2_COFF + +"$ROOT/scripts/m2/run.sh" "$@" +exec "$ROOT/scripts/m2/dis-roundtrip.sh" "$@" diff --git a/scripts/tests/m2/run.sh b/scripts/tests/m2/run.sh new file mode 100755 index 0000000..2a979d7 --- /dev/null +++ b/scripts/tests/m2/run.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# M2 test gate: compile five C functions, assemble/link with GSPA/GSPLNK, +# load the COFF in MAME and check the TI-ABI marker words and ROM strlen call. +set -eu +ROOT=$(cd "$(dirname "$0")/../../.." && pwd) +exec "$ROOT/scripts/m2/run.sh" "$@" diff --git a/scripts/tms-clang b/scripts/tms-clang index 8c6aa9e..f2ba0a5 100755 --- a/scripts/tms-clang +++ b/scripts/tms-clang @@ -43,7 +43,8 @@ trap 'rm -rf "$TMP"' EXIT HUP INT TERM # shellcheck disable=SC2086 "$TMS_CLANG" --target=tms34010-unknown-none -O0 -Xclang -disable-O0-optnone \ - -S -emit-llvm -ffreestanding -nostdinc $INCLUDES -o "$TMP/step1.ll" "$IN" + -S -emit-llvm -ffreestanding -fno-builtin -nostdinc $INCLUDES \ + -o "$TMP/step1.ll" "$IN" "$TMS_BITADDR" --scale "$TMP/step1.ll" "$TMP/scaled.ll" 2>/dev/null