Files

328 lines
11 KiB
Bash
Executable File

#!/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:]]+16,[[:space:]]*0,[[:space:]]*0' \
'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"