Files

465 lines
17 KiB
Bash
Executable File

#!/bin/sh
# M3 dis34 round-trip.
#
# scripts/m3/dis-roundtrip.sh
#
# Parses build/m3/m3.coff, disassembles its linked .text at 018A0000 with the
# VT1200 tree's dis34, and checks that GSPA encoded the compiler output:
#
# _XOpenDisplay/_XCreateSimpleWindow/_XMapWindow/_XSync JAUC <ROM bit addr>
# (XMapWindow is 3FD64F60, MapWindow opcode 8; 3FD645B0 is XRaiseWindow)
# _xhello: 1 push -> CALLA _XOpenDisplay, 9 pushes -> CALLA
# _XCreateSimpleWindow (arguments 1,0,2,200,400,100,100,root,
# display in that order, display pushed last), 2 -> _XMapWindow,
# 2 -> _XSync, each followed by the ABI's SETF 16/32 pair
# branch code: CMP, JALT, JAGT, JAEQ, JAUC and LBB… labels (never .LBB)
# .data: 30 3A 3A 30 00 ("0::0")
# old-STK restore offsets: *SP((pushes+1)*32),A14
#
# Overridable: M3_COFF M3_ASM DIS34
set -eu
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
WORK=${M3_WORK:-$ROOT/build/m3}
COFF=${M3_COFF:-$WORK/m3.coff}
ASM=${M3_ASM:-$WORK/m3.asm}
DIS34=${DIS34:-$ROOT/vendor/vt1200/tools/smart34010/dis34}
fail() {
echo "M3 dis34 round-trip: FAIL: $*" >&2
exit 1
}
[ -f "$COFF" ] || fail "linked COFF not found: $COFF (run scripts/m3/run.sh)"
[ -f "$ASM" ] || fail "linked assembly not found: $ASM (run scripts/m3/run.sh)"
[ -x "$DIS34" ] || fail "dis34 not found or not executable: $DIS34"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/m3-dis34.XXXXXX")
FAIL_LOG=${M3_DIS_FAIL_LOG:-${TMPDIR:-/tmp}/m3-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
_strtab=$((symptr + nsyms * 18))
_i=0
while [ "$_i" -lt "$nsyms" ]; do
_o=$((symptr + _i * 18))
# COFF inline names are 8 bytes; longer names are 0,0,0,0 + a
# string-table offset (the table starts with its own 4-byte size).
if [ "$(u32 "$_o")" -eq 0 ]; then
_soff=$(u32 $((_o + 4)))
_n=$(dd if="$COFF" bs=1 skip=$((_strtab + _soff)) count=64 2>/dev/null |
tr '\000' '\n' | head -1)
else
_n=$(dd if="$COFF" bs=1 skip="$_o" count=8 2>/dev/null | tr -d '\000')
fi
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"
}
XOPEN=$(get_sym _XOpenDisplay)
XCREATE=$(get_sym _XCreateSimpleWindow)
XMAP=$(get_sym _XMapWindow)
XSYNC=$(get_sym _XSync)
XHELLO=$(get_sym _xhello)
SUMTO=$(get_sym _sumto)
MAX2=$(get_sym _max2)
LT=$(get_sym _lt)
BRANCH=$(get_sym _branch_test)
ROOTPROBE=$(get_sym _root_probe)
XOPEN_HEX=$(printf '%08X' "$XOPEN")
XCREATE_HEX=$(printf '%08X' "$XCREATE")
XMAP_HEX=$(printf '%08X' "$XMAP")
XSYNC_HEX=$(printf '%08X' "$XSYNC")
XHELLO_HEX=$(printf '%08X' "$XHELLO")
XOPEN_L=$(printf '%x' "$XOPEN")
XCREATE_L=$(printf '%x' "$XCREATE")
XMAP_L=$(printf '%x' "$XMAP")
XSYNC_L=$(printf '%x' "$XSYNC")
echo " _XOpenDisplay=$XOPEN_HEX _XCreateSimpleWindow=$XCREATE_HEX"
echo " _XMapWindow=$XMAP_HEX _XSync=$XSYNC_HEX"
echo " _xhello=$XHELLO_HEX _sumto=$(printf '%08X' "$SUMTO") _max2=$(printf '%08X' "$MAX2") _lt=$(printf '%08X' "$LT")"
echo " _branch_test=$(printf '%08X' "$BRANCH") _root_probe=$(printf '%08X' "$ROOTPROBE")"
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
match_at() {
_file=$1
_addr=$2
_pat=$3
_laddr=$(printf '%08x' "$_addr")
grep -Eiq "^${_laddr}:[[:space:]]+${_pat}([[:space:]]*(;|$))" "$_file"
}
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 "M3 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() {
_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() {
_file=$1
_want=$2
_pat=$3
if grep -Eiq "$_pat" "$_file"; then
echo " ok $_want"
return 0
fi
echo "M3 dis34 round-trip: FAIL: missing $_want" >&2
sed 's/^/ /' "$_file" >&2
exit 1
}
# count_between FILE START_RE END_RE PUSH_RE: count push encodings after the
# START_RE line up to (not including) the END_RE line.
count_between() {
_file=$1
_start=$2
_end=$3
_push=$4
awk -v s="$_start" -v e="$_end" -v p="$_push" '
{ low = tolower($0) }
on && low ~ e { exit }
on && low ~ p { n++ }
low ~ s { on = 1 }
END { print n + 0 }
' "$_file"
}
echo "== check the ROM import stubs"
expect_at "_XOpenDisplay" "$XOPEN" "JAUC 3F9C2570" \
'jauc[[:space:]]+0x3f9c2570'
expect_at "_XCreateSimpleWindow" "$XCREATE" "JAUC 3FD41B90" \
'jauc[[:space:]]+0x3fd41b90'
expect_at "_XMapWindow" "$XMAP" "JAUC 3FD64F60" \
'jauc[[:space:]]+0x3fd64f60'
expect_at "_XSync" "$XSYNC" "JAUC 3FD9F6B0" \
'jauc[[:space:]]+0x3fd9f6b0'
echo "== check the compiled Xlib call sequence"
func_body "$XHELLO" > "$TMP/xhello.txt"
expect_in "$TMP/xhello.txt" "CALLA _XOpenDisplay ($XOPEN_HEX)" \
"calla[[:space:]]+l${XOPEN_L}"
expect_in "$TMP/xhello.txt" "CALLA _XCreateSimpleWindow ($XCREATE_HEX)" \
"calla[[:space:]]+l${XCREATE_L}"
expect_in "$TMP/xhello.txt" "CALLA _XMapWindow ($XMAP_HEX)" \
"calla[[:space:]]+l${XMAP_L}"
expect_in "$TMP/xhello.txt" "CALLA _XSync ($XSYNC_HEX)" \
"calla[[:space:]]+l${XSYNC_L}"
# The arguments are pushed last-first onto the program stack; the first
# argument (background=1 for XCreateSimpleWindow) is pushed last.
P1=$(count_between "$TMP/xhello.txt" '^[0-9a-f]+:[[:space:]]+setf[[:space:]]+16' \
"calla[[:space:]]+l${XOPEN_L}" 'move[[:space:]]+[^,]+, *\*a14\+, *1')
P2=$(count_between "$TMP/xhello.txt" "calla[[:space:]]+l${XOPEN_L}" \
"calla[[:space:]]+l${XCREATE_L}" 'move[[:space:]]+[^,]+, *\*a14\+, *1')
P3=$(count_between "$TMP/xhello.txt" "calla[[:space:]]+l${XCREATE_L}" \
"calla[[:space:]]+l${XMAP_L}" 'move[[:space:]]+[^,]+, *\*a14\+, *1')
P4=$(count_between "$TMP/xhello.txt" "calla[[:space:]]+l${XMAP_L}" \
"calla[[:space:]]+l${XSYNC_L}" 'move[[:space:]]+[^,]+, *\*a14\+, *1')
[ "$P1" -eq 1 ] || fail "XOpenDisplay has $P1 argument pushes, want 1"
[ "$P2" -eq 9 ] || fail "XCreateSimpleWindow has $P2 argument pushes, want 9"
[ "$P3" -eq 2 ] || fail "XMapWindow has $P3 argument pushes, want 2"
[ "$P4" -eq 2 ] || fail "XSync has $P4 argument pushes, want 2"
echo " ok argument pushes: XOpenDisplay=1 XCreateSimpleWindow=9 XMapWindow=2 XSync=2"
# Counting pushes is not enough: the nine words must reach the callee in the
# ABI order. build/m3/m3.lst records background 1, border 0, border width 2,
# height 200, width 400, y 100, then x 100 (x reuses y's register, so there are
# six MOVIs for the seven constants), followed by the root (A2) and display
# (A0) pushes. Assert the immediates and push registers as one sequence, so a
# swapped pair fails even though the count stays nine, and require the final
# push before CALLA to be the display register.
awk '
tolower($0) ~ /calla[[:space:]]+l'"$XOPEN_L"'/ { on = 1; next }
tolower($0) ~ /calla[[:space:]]+l'"$XCREATE_L"'/ { exit }
!on { next }
tolower($0) ~ /move[[:space:]]+a14,[[:space:]]*-\*sp,[[:space:]]*1/ { started = 1; next }
!started { next }
tolower($0) ~ /movi[[:space:]]+/ {
line = $0
sub(/^[^:]*:[[:space:]]+movi[[:space:]]+/, "", line)
sub(/[[:space:]]*;.*/, "", line)
sub(/[[:space:]]+/, " ", line)
split(line, p, /,[[:space:]]*/)
printf "IMM %s %s\n", tolower(p[1]), tolower(p[2])
next
}
tolower($0) ~ /move[[:space:]]+a[0-9]+,[[:space:]]*\*a14\+,[[:space:]]*1/ {
line = $0
sub(/^[^:]*:[[:space:]]+move[[:space:]]+/, "", line)
sub(/,.*/, "", line)
printf "PUSH %s\n", tolower(line)
next
}
' "$TMP/xhello.txt" > "$TMP/xcreate.seq"
cat > "$TMP/xcreate.want" <<'EOF'
IMM 0x1 a1
PUSH a1
IMM 0x0 a1
PUSH a1
IMM 0x2 a3
PUSH a3
IMM 0xc8 a3
PUSH a3
IMM 0x190 a3
PUSH a3
IMM 0x64 a3
PUSH a3
PUSH a3
PUSH a2
PUSH a0
EOF
if ! cmp -s "$TMP/xcreate.want" "$TMP/xcreate.seq"; then
{
echo "M3 dis34 round-trip: FAIL: XCreateSimpleWindow argument order"
echo " expected (background,border,border_width,height,width,y,x,root,display):"
sed 's/^/ /' "$TMP/xcreate.want"
echo " got:"
sed 's/^/ /' "$TMP/xcreate.seq"
} >&2
exit 1
fi
echo " ok XCreateSimpleWindow argument order: 1,0,2,200,400,100,100,root,display (display pushed last)"
# Xlib clobbers SETF; the compiler must re-establish 16/FS0 and 32/FS1 after
# every call. Check in the linked assembly, where CALLA symbols survive.
awk '
/^_xhello:/ { on = 1; next }
on && /-- End function/ { exit }
on && /^[[:space:]]*CALLA/ { pending = 1; next }
on && pending && /SETF[[:space:]]+16,[[:space:]]*0,[[:space:]]*0/ { saw16 = 1; next }
on && pending && saw16 && /SETF[[:space:]]+32,[[:space:]]*0,[[:space:]]*1/ {
n++; pending = 0; saw16 = 0; next
}
END { print n + 0 }
' "$ASM" > "$TMP/setf_count"
SETF_COUNT=$(cat "$TMP/setf_count")
[ "$SETF_COUNT" -eq 4 ] \
|| fail "found $SETF_COUNT post-call SETF 16/32 pairs in _xhello, want 4"
echo " ok four post-call SETF 16,0,0 + SETF 32,0,1 pairs"
# _xhello takes no arguments of its own (the nine-argument call is inside it),
# so it returns with RETS 0; the argument-taking branch functions use RETS 2.
func_body "$XHELLO" > "$TMP/xhello.txt"
expect_in "$TMP/xhello.txt" "_xhello RETS 0" 'rets([[:space:]]+0)?([[:space:]]*(;|$))'
for pair in "$SUMTO:_sumto" "$MAX2:_max2" "$LT:_lt"; do
addr=${pair%%:*}
label=${pair##*:}
func_body "$addr" > "$TMP/fn.txt"
expect_in "$TMP/fn.txt" "$label RETS 2" 'rets[[:space:]]+2([[:space:]]*(;|$))'
done
echo "== check the branch code"
for pair in "$SUMTO:_sumto" "$MAX2:_max2" "$LT:_lt"; do
addr=${pair%%:*}
label=${pair##*:}
func_body "$addr" > "$TMP/fn.txt"
if ! grep -Eiq 'cmp' "$TMP/fn.txt"; then
sed 's/^/ /' "$TMP/fn.txt" >&2
fail "$label has no CMP"
fi
if ! grep -Eiq '\bjauc\b' "$TMP/fn.txt"; then
sed 's/^/ /' "$TMP/fn.txt" >&2
fail "$label has no JAUC"
fi
echo " ok $label: CMP + JAUC"
done
for pair in "$XHELLO:_xhello"; do
addr=${pair%%:*}
label=${pair##*:}
func_body "$addr" > "$TMP/fn.txt"
if ! grep -Eiq '\bjauc\b' "$TMP/fn.txt"; then
sed 's/^/ /' "$TMP/fn.txt" >&2
fail "$label has no JAUC"
fi
echo " ok $label: JAUC"
done
func_body "$BRANCH" > "$TMP/branch.txt"
for callee in _sumto _max2 _lt _layout_screens _layout_root; do
grep -Eiq "calla[[:space:]]+L$(get_sym "$callee" | xargs printf '%X')" "$TMP/branch.txt" \
|| fail "_branch_test does not call $callee"
done
echo " ok _branch_test calls sumto, max2, lt, layout_screens, layout_root"
# Conditional branch opcodes emitted by this compiled image. JALE/JALO/JAHI
# coverage lives in the m3-loop/m3-unsigned/m3-cmpi lit tests; this image has
# the signed compare, loop-exit and smax branches.
for op in jalt jagt jaeq; do
grep -Eiq "^[0-9a-f]+:[[:space:]]+$op([[:space:]]|;|$)" "$TMP/dis34.txt" \
|| fail "no $op instruction in the linked .text"
echo " ok $op present"
done
# GSPA rejects dotted .L labels; the harness gives each TU an LBB prefix.
grep -q 'LBB' "$ASM" || fail "linked assembly has no LBB block labels"
if grep -q '\.LBB' "$ASM"; then
fail "linked assembly contains dotted .LBB labels (GSPA rejects them)"
fi
echo " ok LBB… block labels (no dot)"
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 "_sumto" "$SUMTO"
check_stk "_max2" "$MAX2"
check_stk "_lt" "$LT"
check_stk "_root_probe" "$ROOTPROBE"
# Negative control for the RETS operand boundary: "rets 1" must not match.
printf '%08x:\trets\t1\t; 0961\n' "$SUMTO" > "$TMP/rets-neg.txt"
if match_at "$TMP/rets-neg.txt" "$SUMTO" 'rets([[:space:]]+0)?'; then
fail "negative control: the RETS pattern accepted 'rets 1'"
fi
echo " ok negative control @ 0x$(printf '%08x' "$SUMTO"): 'rets 1' rejected"
echo "== check the copied .data bytes"
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 "M3 dis34 round-trip: PASS"