forked from arti/vt1200
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).
114 lines
4.6 KiB
Python
114 lines
4.6 KiB
Python
#!/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))
|