mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-30 12:03:54 +02:00
dis34: print long relative JR as jr and surface B-file SP as b15
The 16-bit-displacement jump (low byte 0x00, two words) is JRcc, not the three-word JAcc (low byte 0x80). Printing it as ja made linear disassembly report the wrong control flow and made every such branch grow by a word when reassembled. Register 15 is the stack pointer in either file: 0x0F is A15 and 0x1F is B15. The disassembler printed both as sp, hiding B-file stack-pointer code. Print b15 when the R bit selects the B file. Validated with the 1987 TI GSPA/GSPLNK/GSPROM round trip: the 32 KB region at bit 3F208000 still reassembles byte-for-byte (md5 43adb6669571041985f3e26fd9ff7cb9), as do the 4 KB and 16 KB spans and the 0x8000/3F240000 region. The converter no longer needs its ja->jr or sp->b15 rewrites (5 fixups -> 3); a whole-ROM diff contains only those two output categories. The ROM is unchanged (b46a3e442ccb86134d2356bd70d22fd2).
This commit is contained in:
1 parent
8bc8d90ea6
commit
af7d3565a5
9 files changed
+512
-10
No files matched your search
@@ -0,0 +1,18 @@
|
|||||||
|
# The 1987 DOS toolchain is extracted here on first use, and round-trip
|
||||||
|
# products are written here by roundtrip.sh. Neither is source.
|
||||||
|
work/
|
||||||
|
*.bin
|
||||||
|
*.obj
|
||||||
|
*.out
|
||||||
|
*.map
|
||||||
|
*.lst
|
||||||
|
*.hex
|
||||||
|
*.tag
|
||||||
|
*.HI
|
||||||
|
*.LO
|
||||||
|
*.$$$
|
||||||
|
*.asm
|
||||||
|
*.dis
|
||||||
|
*.c
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Round-trip validation for the two `dis34` fixes
|
||||||
|
|
||||||
|
This directory proves the two corrections in `tools/smart34010/dis34.c`
|
||||||
|
against the 1987 TI TMS34010 toolchain (`GSPA`/`GSPLNK`/`GSPROM` under
|
||||||
|
DOSBox), using the same method as `reference/tms34010-sdk.md`. The ROM is
|
||||||
|
only read; its md5 must stay `b46a3e442ccb86134d2356bd70d22fd2`.
|
||||||
|
|
||||||
|
## The encodings (from `docs/TMS34010-users-guide.txt`)
|
||||||
|
|
||||||
|
**Long relative jump.** The Program Control summary (PDF p. A-16) gives:
|
||||||
|
|
||||||
|
```
|
||||||
|
JAcc Address Jump Absolute Conditional 3 3,4 1 100 code 1000 0000
|
||||||
|
JRcc Address Jump Relative Conditional 2 3,2 1100 code 0000 0000
|
||||||
|
JRcc Address Jump Relative Conditional - Short 2,1 1 100 code xxxx xxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
The ±32K-word `JRcc` description (p. 12-96) says:
|
||||||
|
|
||||||
|
> The assembler calculates the offset as (Address - PC')/16 and inserts the
|
||||||
|
> resulting offset into the second instruction word of the opcode. The range
|
||||||
|
> for this form of the JRcc instruction is -32,768 to +32,767 words.
|
||||||
|
|
||||||
|
So low byte `0x00` + a 16-bit displacement is `JRcc`, not `JAcc`; only low
|
||||||
|
byte `0x80` + a 32-bit address is `JAcc`.
|
||||||
|
|
||||||
|
**B-file SP.** Section 5.1.3 (p. 5-8):
|
||||||
|
|
||||||
|
> The TMS34010 contains only a single SP. However, this SP can be addressed as
|
||||||
|
> a member of either register file, as register A15 or register B15.
|
||||||
|
|
||||||
|
and the MMFM description (p. 12-109):
|
||||||
|
|
||||||
|
> The R bit (bit 4) in the first word indicates which register file is
|
||||||
|
> affected.
|
||||||
|
|
||||||
|
`0x0F` is A15 (`sp`); `0x1F` is B15 (`b15`). GSPA accepts `b15` and rejects
|
||||||
|
`a15`.
|
||||||
|
|
||||||
|
## Fixups required by the converter, before and after
|
||||||
|
|
||||||
|
`reference/tms34010-sdk.md` lists five fixups for `dis34` output. The old
|
||||||
|
converter (`harness/sdk-scratch/dis2as34.py`) implemented all five:
|
||||||
|
|
||||||
|
| # | fixup | before | after |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | hex constants (`0x220` → `0220h`) | needed | needed |
|
||||||
|
| 2 | `MOVI`/`ADDI`/`CMPI`/`SUBI` short vs long (`,W` / `,L`) | needed | needed |
|
||||||
|
| 3 | B-file SP (`rl …, sp` → `b15`) | needed | **gone** — `dis34` prints `b15` |
|
||||||
|
| 4 | long relative `ja<cc>` → `jr<cc>` (2 words) | needed | **gone** — `dis34` prints `jr<cc>` |
|
||||||
|
| 5 | branch target not an instruction boundary → `.word` | needed | needed |
|
||||||
|
|
||||||
|
The shipped converter here (`dis2as34.py`) drops 3 and 4, so its fixup list is
|
||||||
|
**5 → 3**. The `JR` one is the expected disappearance; the SP one disappears
|
||||||
|
as a direct consequence of surfacing B15. (`harness/sdk-scratch/` is left
|
||||||
|
untouched as the record of the pre-fix method.)
|
||||||
|
|
||||||
|
## Results (new `dis34` + `dis2as34.py` with fixups 3 and 4 removed)
|
||||||
|
|
||||||
|
| region (file offset) | base (bit) | size | differing | md5 |
|
||||||
|
|---|---|---:|---:|---|
|
||||||
|
| `0x1000` | `3F208000` | 4 KB | 0 | `a4018fed12e062658bc81957f149265b` |
|
||||||
|
| `0x1000` | `3F208000` | 16 KB | 0 | `0a2e679bc864d52d32c59740142c5b96` |
|
||||||
|
| `0x1000` | `3F208000` | 32 KB | 0 | `43adb6669571041985f3e26fd9ff7cb9` |
|
||||||
|
| `0x8000` | `3F240000` | 4 KB | 0 | `438e743431d56e0c12ededce1c36952a` |
|
||||||
|
|
||||||
|
The same 32 KB region disassembled by the **old** `dis34` and fed through the
|
||||||
|
converter with the removed rewrites fails (`assembled 32912 bytes, original
|
||||||
|
32768`, 27247 differing): the long relative branches grow by a word and the
|
||||||
|
`rl … sp` destination is encoded as A15. With the fixed `dis34` the converter
|
||||||
|
source is equivalent to the old one: only 3 lines change `sp` to `b15` and
|
||||||
|
2 differ in whitespace (`rl 09h,b15` vs `rl 09h, b15`); assembly is
|
||||||
|
byte-identical.
|
||||||
|
|
||||||
|
## Proof that the fixes change nothing else
|
||||||
|
|
||||||
|
`verify_dis.py` disassembles the whole ROM in 32 KB chunks with both a control
|
||||||
|
binary and the fixed binary and classifies every changed line:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./verify_dis.py work/dis34.orig work/dis34.sponly ../../vt1200_v22.bin
|
||||||
|
long-relative JA->JR changes: 0
|
||||||
|
B-file SP sp->b15 changes: 2623
|
||||||
|
all differences are the two intended categories
|
||||||
|
|
||||||
|
$ ./verify_dis.py work/dis34.orig ../../tools/smart34010/dis34 ../../vt1200_v22.bin
|
||||||
|
long-relative JA->JR changes: 6235
|
||||||
|
B-file SP sp->b15 changes: 2623
|
||||||
|
all differences are the two intended categories
|
||||||
|
```
|
||||||
|
|
||||||
|
The SP-only comparison is the additivity check: **zero** mnemonic changes, and
|
||||||
|
every change is exactly an `sp` operand becoming `b15` on an instruction whose
|
||||||
|
encoding can name B15 (bit 4 set, or the cross-file `MOVE Rs,Rd` dest). The
|
||||||
|
full-fix comparison adds only the `JA`→`JR` mnemonic changes on 2-word
|
||||||
|
`0xC?00` instructions. Labels and line counts are unchanged (labels do not
|
||||||
|
depend on either fix).
|
||||||
|
|
||||||
|
## Reproducing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd harness/sdk-roundtrip
|
||||||
|
./prepare_work.sh # builds work/dis34.orig and dis34.sponly
|
||||||
|
./roundtrip.sh 0x1000 32768 3F208000 # prints BYTE-EXACT + md5
|
||||||
|
DIS34=$PWD/work/dis34.orig ./roundtrip.sh 0x1000 32768 3F208000 # expected FAIL
|
||||||
|
./verify_dis.py work/dis34.orig work/dis34.sponly ../../vt1200_v22.bin
|
||||||
|
./verify_dis.py work/dis34.orig ../../tools/smart34010/dis34 ../../vt1200_v22.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
`work/` also holds the pre-fix source/binary (`dis34.orig*`) and an SP-only
|
||||||
|
binary (`dis34.sponly*`) used for the isolation checks. The SDK image is
|
||||||
|
extracted from `tools/tms34010-sdk/` into `work/` on first use.
|
||||||
|
|
||||||
|
Cached disassemblies elsewhere (`reference/dis34-a-v22.txt.gz`,
|
||||||
|
`harness/dis/*.txt`) were produced by the buggy binary and still show
|
||||||
|
`ja<cc>` for the 2-word relative jumps; regenerate them with the fixed
|
||||||
|
`dis34` before trusting their control flow.
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Convert smart34010 (dis34 -o) output into TI gspa/as34 2.00 syntax.
|
||||||
|
|
||||||
|
Usage: dis2as34.py BASE_HEX SIZE_BYTES INPUT.ASM > OUT.ASM
|
||||||
|
|
||||||
|
The input must be produced with the fixed ``dis34 -o`` (opcode words in a
|
||||||
|
trailing comment), which prints both relative-jump forms as ``jr<cc>`` and
|
||||||
|
the B-file stack pointer as ``b15``. The 1987 assembler is *not*
|
||||||
|
byte-faithful on its own:
|
||||||
|
|
||||||
|
* ``MOVI`` is one mnemonic with a short (16-bit, sign-extended) and a long
|
||||||
|
(32-bit) form. gspa auto-selects by "does the value fit in 16 bits", so a
|
||||||
|
long MOVI of 0x53E0 (which the ROM encodes as a 3-word instruction) is
|
||||||
|
silently shortened -- and sign extension then changes its meaning. The
|
||||||
|
explicit ``W`` / ``L`` operand forces the form.
|
||||||
|
* the same applies to ADDI/CMPI/SUBI.
|
||||||
|
* register-indirect displacements are signed 16-bit fields. dis34 prints the
|
||||||
|
raw two's-complement bits (0xFFE0); gspa rejects those as out of bounds, so
|
||||||
|
they are rendered as signed decimal (-32).
|
||||||
|
* constants: dis34 emits ``0xHHHH``; gspa wants ``0HHHHh`` (must start with a
|
||||||
|
decimal digit, so 0xC00000B0 becomes 0C00000B0h).
|
||||||
|
* a PC-relative branch whose target is not an instruction boundary in the
|
||||||
|
region (outside it, or -- as happens in data tables -- inside a multi-word
|
||||||
|
instruction) cannot be expressed as a label. gspa also mishandles a
|
||||||
|
negative absolute ``.set`` target in a JR. The exact opcode words are
|
||||||
|
therefore emitted as ``.word`` for those branches.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
DISP = re.compile(r"(\*\s*[ab]\d+|\*\s*sp)\(\s*0x([0-9a-fA-F]+)\s*\)")
|
||||||
|
HEX = re.compile(r"0x([0-9a-fA-F]+)")
|
||||||
|
FORM_OPS = {"movi", "addi", "cmpi", "subi"}
|
||||||
|
RELATIVE = re.compile(r"^(jreq|jrne|jrlt|jrle|jrgt|jrge|jrc|jrnc|jrlo|jrls|jrhi|jrhs|jrnz|jrz|"
|
||||||
|
r"jruc|jrxn|jrxnn|jrxz|jrxeq|jrxgt|jrxle|jryn|jrynn|jryz|jryeq|jrygt|jryle|"
|
||||||
|
r"call|callr)$")
|
||||||
|
DSJ_OPS = {"dsj", "dsjs", "dsjeq", "dsjne"}
|
||||||
|
|
||||||
|
|
||||||
|
def ti_hex(digits: str) -> str:
|
||||||
|
return "0" + digits.upper() + "h"
|
||||||
|
|
||||||
|
|
||||||
|
def fix_disp(m):
|
||||||
|
base, digits = m.group(1), m.group(2)
|
||||||
|
v = int(digits, 16)
|
||||||
|
if v >= 0x8000:
|
||||||
|
return f"{base}({v - 0x10000})"
|
||||||
|
return f"{base}(0{digits.upper()}h)"
|
||||||
|
|
||||||
|
|
||||||
|
def raw_words(words):
|
||||||
|
return " .word " + ", ".join(ti_hex(w) for w in words)
|
||||||
|
|
||||||
|
|
||||||
|
def convert(src: str, base: int, size: int) -> str:
|
||||||
|
end = base + size * 8
|
||||||
|
lines = src.splitlines()
|
||||||
|
defined = set()
|
||||||
|
for line in lines:
|
||||||
|
code = line.partition(";")[0]
|
||||||
|
dm = re.match(r"^(L[0-9A-Fa-f]{8}):", code)
|
||||||
|
if dm:
|
||||||
|
defined.add(dm.group(1).upper())
|
||||||
|
|
||||||
|
out = []
|
||||||
|
referenced = set()
|
||||||
|
for line in lines:
|
||||||
|
code, sep, comment = line.partition(";")
|
||||||
|
for lm in re.findall(r"\b(L[0-9A-Fa-f]{8})\b", code):
|
||||||
|
referenced.add(lm.upper())
|
||||||
|
words = re.findall(r"\b[0-9a-fA-F]{4}\b", comment)
|
||||||
|
stripped = code.rstrip()
|
||||||
|
parts = stripped.split()
|
||||||
|
op = parts[0].lower() if parts else ""
|
||||||
|
# short/long immediate forms (see docstring)
|
||||||
|
if op in FORM_OPS and words:
|
||||||
|
if len(words) == 2:
|
||||||
|
stripped += ", W"
|
||||||
|
mm = re.match(r"^(\s*\w+\s+)0x([0-9a-fA-F]+)(,.*)$", stripped)
|
||||||
|
if mm and int(mm.group(2), 16) >= 0x8000:
|
||||||
|
stripped = (mm.group(1) + str(int(mm.group(2), 16) - 0x10000)
|
||||||
|
+ mm.group(3))
|
||||||
|
elif len(words) == 3:
|
||||||
|
stripped += ", L"
|
||||||
|
# relative branch to a label that is not an instruction boundary
|
||||||
|
mm = re.match(r"^(\s*)(\w+)(\s+.*?)(L[0-9A-Fa-f]{8})\s*$", stripped)
|
||||||
|
if mm and mm.group(4).upper() not in defined:
|
||||||
|
op2 = mm.group(2).lower()
|
||||||
|
if op2 in DSJ_OPS or RELATIVE.match(op2):
|
||||||
|
stripped = raw_words(words)
|
||||||
|
# relative branch to an absolute address outside the region
|
||||||
|
mm = re.match(r"^(\s*)([a-z]+)(\s+)0x([0-9a-fA-F]+)\s*$", stripped)
|
||||||
|
if mm and RELATIVE.match(mm.group(2).lower()):
|
||||||
|
target = int(mm.group(4), 16)
|
||||||
|
if not (base <= target < end):
|
||||||
|
stripped = raw_words(words)
|
||||||
|
stripped = DISP.sub(fix_disp, stripped)
|
||||||
|
stripped = HEX.sub(lambda mm: ti_hex(mm.group(1)), stripped)
|
||||||
|
out.append(stripped)
|
||||||
|
missing = sorted(referenced - defined)
|
||||||
|
if missing:
|
||||||
|
out.append("")
|
||||||
|
for name in missing:
|
||||||
|
off = int(name[1:], 16) - base
|
||||||
|
out.append(f"{name} .set " + ("0%Xh" % off if off >= 0 else str(off)))
|
||||||
|
return "\n".join(out) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
base = int(sys.argv[1], 16)
|
||||||
|
size = int(sys.argv[2])
|
||||||
|
sys.stdout.write(convert(open(sys.argv[3]).read(), base, size))
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Build the reference binaries used by verify_dis.py.
|
||||||
|
#
|
||||||
|
# work/dis34.orig - the pre-fix dis34 (from the checkpoint commit)
|
||||||
|
# work/dis34.sponly - the same source with *only* the B-file SP fix applied
|
||||||
|
#
|
||||||
|
# Usage: prepare_work.sh [GIT_REV]
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||||
|
DIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
WORK="$DIR/work"
|
||||||
|
REV=${1:-acb92d3e44ec}
|
||||||
|
|
||||||
|
mkdir -p "$WORK"
|
||||||
|
git -C "$ROOT" show "$REV:tools/smart34010/dis34.c" > "$WORK/dis34.orig.c"
|
||||||
|
cc -Wall -O2 -o "$WORK/dis34.orig" "$WORK/dis34.orig.c"
|
||||||
|
|
||||||
|
sed 's/strcpy (p, "sp");/strcpy (p, file ? "b15" : "sp");/' \
|
||||||
|
"$WORK/dis34.orig.c" > "$WORK/dis34.sponly.c"
|
||||||
|
cc -Wall -O2 -o "$WORK/dis34.sponly" "$WORK/dis34.sponly.c"
|
||||||
|
|
||||||
|
echo "built $WORK/dis34.orig and $WORK/dis34.sponly"
|
||||||
Executable
+109
@@ -0,0 +1,109 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Assemble a region of the VT1200 ROM with the 1987 TI TMS34010 toolchain
|
||||||
|
# (GSPA/GSPLNK/GSPROM, run under DOSBox) and compare the result byte-for-byte
|
||||||
|
# with the original.
|
||||||
|
#
|
||||||
|
# Usage: roundtrip.sh OFFSET SIZE BASE_BIT
|
||||||
|
# OFFSET byte offset into vt1200_v22.bin
|
||||||
|
# SIZE region size in bytes
|
||||||
|
# BASE_BIT bit address the region is linked at (e.g. 3F208000)
|
||||||
|
#
|
||||||
|
# Example: roundtrip.sh 0x1000 16384 3F208000
|
||||||
|
#
|
||||||
|
# The ROM is only read. All work happens in this directory.
|
||||||
|
#
|
||||||
|
# Set DIS34=/path/to/dis34 to test a different build (used for the pre-fix
|
||||||
|
# control run: it should FAIL, because the converter no longer rewrites
|
||||||
|
# long-relative JA back to JR).
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||||
|
DIR=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
WORK="$DIR/work"
|
||||||
|
ROM="$ROOT/vt1200_v22.bin"
|
||||||
|
DIS="${DIS34:-$ROOT/tools/smart34010/dis34}"
|
||||||
|
DOSBOX_CONF="$ROOT/harness/sdk-scratch/dosbox.conf"
|
||||||
|
|
||||||
|
OFF=${1:?offset}
|
||||||
|
SIZE=${2:?size}
|
||||||
|
BASE=${3:?base bit address}
|
||||||
|
OFF=$((OFF))
|
||||||
|
SIZE=$((SIZE))
|
||||||
|
|
||||||
|
mkdir -p "$WORK"
|
||||||
|
# tools: extract once from the 1987-11-06 floppy image
|
||||||
|
if [ ! -x "$WORK/GSPA.EXE" ]; then
|
||||||
|
IMG="$ROOT/tools/tms34010-sdk/1987-05 TMS34010 SDK/1987-11-06 TMS34010 Simulator-C Tools-ASM Tools.img"
|
||||||
|
mcopy -s -n -i "$IMG" "::/A2/*" "$WORK/"
|
||||||
|
mcopy -s -n -i "$IMG" "::/C2/*" "$WORK/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TI hex constants must begin with a decimal digit
|
||||||
|
TBASE=$BASE
|
||||||
|
case "$TBASE" in
|
||||||
|
[0-9]*) ;;
|
||||||
|
*) TBASE="0$TBASE" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
dd if="$ROM" of="$WORK/rt.bin" bs=1 skip="$OFF" count="$SIZE" 2>/dev/null
|
||||||
|
"$DIS" -b "$BASE" -o "$WORK/rt.bin" > "$WORK/rt.dis"
|
||||||
|
python3 "$DIR/dis2as34.py" "$BASE" "$SIZE" "$WORK/rt.dis" > "$WORK/RT.ASM"
|
||||||
|
sed -i '1i .text' "$WORK/RT.ASM"
|
||||||
|
echo '.end' >> "$WORK/RT.ASM"
|
||||||
|
|
||||||
|
cat > "$WORK/RT.CMD" <<EOF
|
||||||
|
-a
|
||||||
|
-o rt.out
|
||||||
|
-m rt.map
|
||||||
|
rt.obj
|
||||||
|
SECTIONS
|
||||||
|
{
|
||||||
|
.text ${TBASE}h: {}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
sed -i 's/$/\r/' "$WORK/RT.CMD"
|
||||||
|
|
||||||
|
rm -f "$WORK"/RT.OBJ "$WORK"/RT.OUT "$WORK"/RT.LST "$WORK"/RTHI.HI "$WORK"/RTLO.LO
|
||||||
|
SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
|
||||||
|
timeout 180 dosbox -conf "$DOSBOX_CONF" -noprimaryconf -nolocalconf \
|
||||||
|
-c "mount c $WORK" -c "c:" \
|
||||||
|
-c "gspa rt rt.obj rt.lst -l" \
|
||||||
|
-c "gsplnk rt.cmd" \
|
||||||
|
-c "gsprom -i rt.out rthi rtlo" \
|
||||||
|
-c "exit" -exit >/dev/null 2>&1
|
||||||
|
|
||||||
|
python3 - "$WORK" "$SIZE" <<'EOF'
|
||||||
|
import sys
|
||||||
|
work, size = sys.argv[1], int(sys.argv[2])
|
||||||
|
|
||||||
|
def readhex(p):
|
||||||
|
d = {}
|
||||||
|
for line in open(p):
|
||||||
|
line = line.strip()
|
||||||
|
if not line.startswith(':'):
|
||||||
|
continue
|
||||||
|
b = bytes.fromhex(line[1:])
|
||||||
|
if b[3] != 0:
|
||||||
|
continue
|
||||||
|
a = (b[1] << 8) | b[2]
|
||||||
|
for i, x in enumerate(b[4:4 + b[0]]):
|
||||||
|
d[a + i] = x
|
||||||
|
return d
|
||||||
|
|
||||||
|
hi = readhex(work + "/RTHI.HI")
|
||||||
|
lo = readhex(work + "/RTLO.LO")
|
||||||
|
out = bytearray()
|
||||||
|
for a in sorted(hi):
|
||||||
|
out.append(lo[a])
|
||||||
|
out.append(hi[a])
|
||||||
|
orig = open(work + "/rt.bin", "rb").read()
|
||||||
|
n = min(len(orig), len(out))
|
||||||
|
bad = [i for i in range(n) if orig[i] != out[i]]
|
||||||
|
print("assembled %d bytes, original %d bytes, %d differing" % (len(out), len(orig), len(bad)))
|
||||||
|
if len(out) != len(orig) or bad:
|
||||||
|
print("FAIL")
|
||||||
|
for i in bad[:5]:
|
||||||
|
print(" %#x: orig %s out %s" % (i, orig[i:i+8].hex(), out[i:i+8].hex()))
|
||||||
|
sys.exit(1)
|
||||||
|
print("md5 " + __import__('hashlib').md5(out).hexdigest() + " BYTE-EXACT")
|
||||||
|
EOF
|
||||||
Executable
+107
@@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare two dis34 builds over the whole ROM and classify every difference.
|
||||||
|
|
||||||
|
Purpose: prove the two `dis34` fixes are the *only* changes to its output.
|
||||||
|
|
||||||
|
* long-relative JR fix: the line must change from ``ja<cc>`` to ``jr<cc>``
|
||||||
|
and carry exactly two opcode words, the first being ``0xC?00``;
|
||||||
|
* B-file SP fix: the mnemonic must be unchanged, every ``sp`` operand must
|
||||||
|
become ``b15``, and the encoding must be able to name B15: bit 4 set, or
|
||||||
|
the cross-file MOVE Rs,Rd whose destination is the other file.
|
||||||
|
|
||||||
|
Usage: verify_dis.py OLD_DIS34 NEW_DIS34 [ROM]
|
||||||
|
|
||||||
|
Chunks are disassembled with 16 zero bytes of padding so a multi-word
|
||||||
|
instruction at the end of a chunk cannot abort the run. The padding is
|
||||||
|
identical for both builds and its decoded lines are still compared.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HEXWORD = re.compile(r"\b[0-9a-fA-F]{4}\b")
|
||||||
|
|
||||||
|
|
||||||
|
def disassemble(exe, chunk, base, work):
|
||||||
|
part = os.path.join(work, "verify_chunk.bin")
|
||||||
|
with open(part, "wb") as f:
|
||||||
|
f.write(chunk + b"\0" * 16)
|
||||||
|
p = subprocess.run([exe, "-b", "%X" % base, "-o", part],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||||
|
check=True)
|
||||||
|
return p.stdout.decode("latin-1").splitlines()
|
||||||
|
|
||||||
|
|
||||||
|
def words(line):
|
||||||
|
code, _, comment = line.partition(";")
|
||||||
|
return HEXWORD.findall(comment)
|
||||||
|
|
||||||
|
|
||||||
|
def can_name_b15(w0):
|
||||||
|
"""True if the instruction's register field can select B-file reg 15.
|
||||||
|
|
||||||
|
Bit 4 (the R bit) normally selects the file. MOVE Rs,Rd cross-file
|
||||||
|
(op 0x4E00) is the exception: its *destination* is the other file
|
||||||
|
(dis34 prints ``regname(rd, rf ^ 1)``), so bit 4 = 0 still means B15
|
||||||
|
for the destination.
|
||||||
|
"""
|
||||||
|
op = int(w0, 16)
|
||||||
|
return bool(op & 0x10) or (op & 0xfe00) == 0x4e00
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
old_exe, new_exe = sys.argv[1], sys.argv[2]
|
||||||
|
rom_path = sys.argv[3] if len(sys.argv) > 3 else "vt1200_v22.bin"
|
||||||
|
work = os.path.dirname(os.path.abspath(__file__)) + "/work"
|
||||||
|
os.makedirs(work, exist_ok=True)
|
||||||
|
rom = open(rom_path, "rb").read()
|
||||||
|
|
||||||
|
chunk_size = 32768
|
||||||
|
base0 = 0x3F200000
|
||||||
|
n_jr = n_sp = 0
|
||||||
|
errors = []
|
||||||
|
for start in range(0, len(rom), chunk_size):
|
||||||
|
chunk = rom[start:start + chunk_size]
|
||||||
|
base = base0 + start * 8
|
||||||
|
old = disassemble(old_exe, chunk, base, work)
|
||||||
|
new = disassemble(new_exe, chunk, base, work)
|
||||||
|
if len(old) != len(new):
|
||||||
|
errors.append("chunk %#x: line count %d != %d" %
|
||||||
|
(start, len(old), len(new)))
|
||||||
|
continue
|
||||||
|
for i, (a, b) in enumerate(zip(old, new)):
|
||||||
|
if a == b:
|
||||||
|
continue
|
||||||
|
w = words(a)
|
||||||
|
code_a = a.partition(";")[0].strip()
|
||||||
|
code_b = b.partition(";")[0].strip()
|
||||||
|
tok_a = code_a.split()[0].lower() if code_a.split() else ""
|
||||||
|
tok_b = code_b.split()[0].lower() if code_b.split() else ""
|
||||||
|
ja = re.fullmatch(r"ja([a-z]+)", tok_a)
|
||||||
|
if (ja and tok_b == "jr" + ja.group(1) and len(w) == 2
|
||||||
|
and re.fullmatch(r"c[0-9a-f]00", w[0].lower())):
|
||||||
|
n_jr += 1
|
||||||
|
continue
|
||||||
|
# SP fix: same mnemonic, only sp -> b15, R bit set
|
||||||
|
if (tok_a == tok_b and "sp" in code_a and "b15" in code_b
|
||||||
|
and code_a.replace("sp", "b15") == code_b
|
||||||
|
and w and can_name_b15(w[0])):
|
||||||
|
n_sp += 1
|
||||||
|
continue
|
||||||
|
errors.append("chunk %#x line %d:\n - %s\n + %s" %
|
||||||
|
(start, i + 1, a, b))
|
||||||
|
|
||||||
|
print("long-relative JA->JR changes: %d" % n_jr)
|
||||||
|
print("B-file SP sp->b15 changes: %d" % n_sp)
|
||||||
|
if errors:
|
||||||
|
print("UNEXPECTED changes: %d" % len(errors))
|
||||||
|
for e in errors[:10]:
|
||||||
|
print(e)
|
||||||
|
return 1
|
||||||
|
print("all differences are the two intended categories")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -42,7 +42,10 @@ Examples:
|
|||||||
|
|
||||||
- **A-file**: a0-a14, accessed when register bit 4 = 0
|
- **A-file**: a0-a14, accessed when register bit 4 = 0
|
||||||
- **B-file**: b0-b14, accessed when register bit 4 = 1
|
- **B-file**: b0-b14, accessed when register bit 4 = 1
|
||||||
- **sp**: register 15 in either file (stack pointer)
|
- **sp**: register 15 in either file (stack pointer), encoded as `0x0F`
|
||||||
|
(A15, printed `sp`) or `0x1F` (B15, printed `b15`). Both are the same
|
||||||
|
physical stack pointer, but the encoding names a different file; a
|
||||||
|
disassembler must not collapse them.
|
||||||
- Register encoding: 5-bit field, bit 4 = file select, bits 3:0 = register number
|
- Register encoding: 5-bit field, bit 4 = file select, bits 3:0 = register number
|
||||||
|
|
||||||
### Instruction Word Format
|
### Instruction Word Format
|
||||||
@@ -299,9 +302,13 @@ Condition code in bits 11:8 of the instruction word:
|
|||||||
| 15 | nn | Not negative |
|
| 15 | nn | Not negative |
|
||||||
|
|
||||||
Jump forms:
|
Jump forms:
|
||||||
- **JR cc** (relative): `0xC000 | (cc<<8) | 0x00` + 16-bit displacement
|
- **JR cc** (relative, 2 words): `0xC000 | (cc<<8) | 0x00` + 16-bit displacement
|
||||||
- **JR cc** (short): `0xC000 | (cc<<8) | disp8` (8-bit signed displacement)
|
- **JR cc** (short, 2 words): `0xC000 | (cc<<8) | disp8` (8-bit signed displacement)
|
||||||
- **JA cc** (absolute): `0xC000 | (cc<<8) | 0x80` + 32-bit address
|
- **JA cc** (absolute, 3 words): `0xC000 | (cc<<8) | 0x80` + 32-bit address
|
||||||
|
|
||||||
|
Only low byte `0x80` is the absolute form. Low byte `0x00` carries a 16-bit
|
||||||
|
*displacement* and is still `JRcc`; the word count must not be used to invent
|
||||||
|
an absolute mnemonic for it.
|
||||||
|
|
||||||
Displacement formula:
|
Displacement formula:
|
||||||
- 16-bit: `target = pc + 32 + (disp << 4)`
|
- 16-bit: `target = pc + 32 + (disp << 4)`
|
||||||
@@ -337,10 +344,11 @@ Using the wrong decode gives K±1 errors. In `dis34.c`:
|
|||||||
|
|
||||||
### Round-Trip Design Decisions
|
### Round-Trip Design Decisions
|
||||||
|
|
||||||
1. **`jacc` for 16-bit displacement JR** -- dis34 outputs `jacc` (not `jrcc`)
|
1. **`jrcc` for both relative forms** -- the 8-bit and 16-bit displacement
|
||||||
for the 16-bit displacement form of conditional jumps. This ensures as34
|
forms are both `JRcc` (2 words); only the 3-word `0x80` form is `JAcc`.
|
||||||
emits the same 4-byte encoding. The 8-bit short form uses `jrcc`. The
|
(Until 2026-10-18 the 16-bit form was printed as `jacc`, which made a
|
||||||
32-bit absolute form uses `jacc`.
|
linear disassembly's control flow wrong and forced the round-trip
|
||||||
|
converter to rewrite it back to `jrcc`.)
|
||||||
|
|
||||||
2. **Branch-only labels** -- Only branch/call targets (jr, ja, callr, calla,
|
2. **Branch-only labels** -- Only branch/call targets (jr, ja, callr, calla,
|
||||||
dsj, dsjeq, dsjne, dsjs) get labels. MOVI and other data-reference
|
dsj, dsjeq, dsjne, dsjs) get labels. MOVI and other data-reference
|
||||||
|
|||||||
Binary file not shown.
@@ -70,7 +70,12 @@ regname (int reg, int file)
|
|||||||
char *p = s[idx++ & 7];
|
char *p = s[idx++ & 7];
|
||||||
if (reg == 15)
|
if (reg == 15)
|
||||||
{
|
{
|
||||||
strcpy (p, "sp");
|
/* Register 15 is the stack pointer in *either* file: the R bit
|
||||||
|
(bit 4 of the register field) selects A15 or B15. Printing both
|
||||||
|
as "sp" hides which file the instruction actually names. GSPA
|
||||||
|
spells the B-file one "b15"; it rejects "a15", and bare "sp" is
|
||||||
|
the A-file register. */
|
||||||
|
strcpy (p, file ? "b15" : "sp");
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
snprintf (p, 8, "%c%d", file ? 'b' : 'a', reg);
|
snprintf (p, 8, "%c%d", file ? 'b' : 'a', reg);
|
||||||
@@ -2163,9 +2168,11 @@ main (int argc, char **argv)
|
|||||||
}
|
}
|
||||||
else if (lo8 == 0x00)
|
else if (lo8 == 0x00)
|
||||||
{
|
{
|
||||||
|
/* 16-bit displacement form: still JRcc (2 words), not
|
||||||
|
the 3-word absolute JAcc (lo8 == 0x80). */
|
||||||
int16_t disp = (int16_t) r16 ();
|
int16_t disp = (int16_t) r16 ();
|
||||||
uint32_t target = pc + 32 + ((int32_t) disp << 4);
|
uint32_t target = pc + 32 + ((int32_t) disp << 4);
|
||||||
printf ("ja%s %s", condnames[cc], fmt_target (target));
|
printf ("jr%s %s", condnames[cc], fmt_target (target));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user