328 lines
11 KiB
Bash
Executable File
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"
|