checkpoint: VT1200 reverse-engineering workspace before board-model work

This commit is contained in:
SDD committed 2026-10-16 01:37:50 +03:00
commit acb92d3e44
325 files changed
+951894

No files matched your search

+418
View File
@@ -0,0 +1,418 @@
#!/usr/bin/env python3
"""VT1200 font delivery harness.
Two jobs:
1. **PCF fonts** — parse/modify Portable Compiled Format fonts (the format the
VT1200's MIT-X11-derived font parser reads; see EXPLOIT_NOTES.md). The
primitive we care about is the BITMAPS table's ``num_chars`` field, which
``pcfReadBitmaps`` multiplies by 4 without an overflow check.
2. **TFTP server** — the terminal is the TFTP *client* for its font path, so we
serve the font files (and ``fonts.dir``) to it.
CLI::
python3 tools/fontgen.py baseline out.pcf
python3 tools/fontgen.py mutate base.pcf evil.pcf --num-chars 0x40000001
python3 tools/fontgen.py serve evil.pcf --host 0.0.0.0 --port 69
"""
import argparse
import os
import socket
import struct
import subprocess
import sys
import tempfile
import threading
import time
PCF_MAGIC = b"\x01fcp" # PCF_FILE_VERSION, little-endian ('p','c','f',1)
PCF_VERSION = 0x70636601
FMT_MS_BYTE = 1 << 2 # fontlib.h: format bit 2 -> MSBFirst byte order
# font.h table labels
PROPERTIES = 1 << 0
ACCELERATORS = 1 << 1
METRICS = 1 << 2
BITMAPS = 1 << 3
INK_METRICS = 1 << 4
ENCODINGS = 1 << 5
SWIDTHS = 1 << 6
GLYPH_NAMES = 1 << 7
BDF_ENCODINGS = 1 << 8
LABELS = {
PROPERTIES: "PROPERTIES", ACCELERATORS: "ACCELERATORS", METRICS: "METRICS",
BITMAPS: "BITMAPS", INK_METRICS: "INK_METRICS", ENCODINGS: "ENCODINGS",
SWIDTHS: "SWIDTHS", GLYPH_NAMES: "GLYPH_NAMES", BDF_ENCODINGS: "BDF_ENCODINGS",
}
TFTP_RRQ, TFTP_DATA, TFTP_ACK, TFTP_ERROR = 1, 3, 4, 5
# --------------------------------------------------------------------------- PCF
def pcf_tables(data):
"""Return the table of contents as a list of dicts."""
if len(data) < 8 or data[:4] != PCF_MAGIC:
raise ValueError("not a PCF file (bad magic)")
n = struct.unpack_from("<I", data, 4)[0]
if 8 + 16 * n > len(data):
raise ValueError("truncated table of contents")
tables = []
for i in range(n):
label, fmt, size, off = struct.unpack_from("<IIII", data, 8 + 16 * i)
tables.append({"label": label, "format": fmt, "size": size, "offset": off})
return tables
def find_table(data, label):
for t in pcf_tables(data):
if t["label"] == label:
return t
return None
def _order(fmt):
"""Byte order of a table body, from its format field (FMT_MS_BYTE)."""
return ">" if (fmt & FMT_MS_BYTE) else "<"
def get_num_chars(data):
"""The BITMAPS 'num_chars' field at table_offset + 4 (in the table's byte order)."""
t = find_table(data, BITMAPS)
if t is None:
raise ValueError("no BITMAPS table")
return struct.unpack_from(_order(t["format"]) + "I", data, t["offset"] + 4)[0]
def set_num_chars(data, n):
"""Return *data* with the BITMAPS num_chars rewritten (bytes untouched elsewhere)."""
t = find_table(data, BITMAPS)
if t is None:
raise ValueError("no BITMAPS table")
out = bytearray(data)
struct.pack_into(_order(t["format"]) + "I", out, t["offset"] + 4, n & 0xFFFFFFFF)
return bytes(out)
# --------------------------------------------------------------------------- BDF
BDF_TEMPLATE = """STARTFONT 2.1
FONT -misc-{name}-medium-r-normal--{h}-{pt}-75-75-c-{avg}-iso8859-1
SIZE {pt} 75 75
FONTBOUNDINGBOX {w} {h} 0 -{d}
STARTPROPERTIES 3
FONT_ASCENT {a}
FONT_DESCENT {d}
DEFAULT_CHAR 0
ENDPROPERTIES
CHARS 1
STARTCHAR {char}
ENCODING {enc}
SWIDTH 500 0
DWIDTH {w} 0
BBX {w} {h} 0 -{d}
BITMAP
{bitmap}
ENDCHAR
ENDFONT
"""
def minimal_bdf(name="vt1200test", char="A", encoding=65, w=8, h=8, ascent=6, descent=2):
"""A tiny but valid BDF, good enough for bdftopcf.
The XLFD uses PIXEL_SIZE={h}, POINT_SIZE={h*10}, AVERAGE_WIDTH={w*10} so the
name bdftopcf stores is the canonical, 10x-scaled form.
"""
rows = ["1C", "3E", "63", "63", "7F", "63", "63", "63"][:h]
rows += ["00"] * (h - len(rows))
return BDF_TEMPLATE.format(name=name, char=char, enc=encoding, w=w, h=h,
a=ascent, d=descent, pt=h * 10, avg=w * 10,
bitmap="\n".join(rows))
def build_baseline_pcf(out_path, font_name="vt1200test"):
"""Write a valid PCF font produced by bdftopcf from our generated BDF."""
with tempfile.TemporaryDirectory() as d:
bpath = os.path.join(d, "f.bdf")
with open(bpath, "w") as fh:
fh.write(minimal_bdf(font_name))
r = subprocess.run(["bdftopcf", "-o", out_path, bpath],
capture_output=True, text=True)
if r.returncode != 0:
raise SystemExit(f"bdftopcf failed: {r.stderr}")
return out_path
# --------------------------------------------------------------------------- SNF
SNF_VERSION = 4
SNF_GLYPH = bytes([0x1C, 0x3E, 0x63, 0x63, 0x7F, 0x63, 0x63, 0x63])
def _xcharinfo(metrics, e="<"):
return struct.pack(e + "6H", *[m & 0xFFFF for m in metrics])
def make_snf(name, char=65, w=8, h=8, ascent=6, descent=2, e="<", dec_magic=False):
"""Build a minimal SNF (Server Normal Font) file.
Layout per snfstr.h: snfFontInfoRec, char-info array, glyphs (padded to a
32-bit boundary), snfFontPropRec array, then the property string table.
SNF is CPU/framebuffer dependent, so the byte order must match the host.
``dec_magic=True`` stamps version1/version2 with DEC's own value
(0x01DF5E76 = pi*1e7) instead of the MIT version 4 — the VT1200 firmware
checks for DEC's value.
"""
version = 0x01DF5E76 if dec_magic else SNF_VERSION
first_col, last_col = 0, 255
first_row, last_row = 0, 0
n2d = (last_row - first_row + 1) * (last_col - first_col + 1)
glyph = SNF_GLYPH[:h]
glyphs = glyph + b"\x00" * ((4 - len(glyph) % 4) % 4)
metrics = (0, w, w, ascent, descent, 0)
zero = (0, 0, 0, 0, 0, 0)
ci = []
for code in range(first_col, last_col + 1):
if code == char:
word = (0 & 0xFFFFFF) | (1 << 24) # byteOffset=0, exists=1
ci.append(_xcharinfo(metrics, e) + struct.pack(e + "I", word))
else:
ci.append(_xcharinfo(zero, e) + struct.pack(e + "I", 0))
charinfo = b"".join(ci)
strings = name.encode("latin1") + b"\x00"
nprops = 1
props = struct.pack(e + "III", 0, 0, 1) # name=0, value=0, indirect
minb = _xcharinfo(zero, e) + struct.pack(e + "I", 0)
maxb = _xcharinfo(metrics, e) + struct.pack(e + "I", len(glyphs))
hdr = struct.pack(e + "6I", version, 1, 0, 1, 1, 0)
hdr += struct.pack(e + "I", 0x0E) # linear|constantWidth|inkInside
hdr += struct.pack(e + "IIII", first_col, last_col, first_row, last_row)
hdr += struct.pack(e + "III", nprops, len(strings), char)
hdr += struct.pack(e + "ii", -descent, ascent)
hdr += minb + maxb
hdr += struct.pack(e + "III", 1, 1, version)
if len(hdr) != 108:
raise AssertionError(f"SNF header is {len(hdr)} bytes, expected 108")
return hdr + charinfo + glyphs + props + strings
def make_dec_snf(name="VT1200PROBE1", e="<", hdr=b"\x00" * 0x54, extra=b""):
"""Minimal DEC font container satisfying the firmware's entry checks.
Recovered from the VT1200 reader (0x0ED70A-0x0ED8C0):
file[0:4] == 0x01DF5E76 (pi*1e7)
file[4:6] == 0x0054
file[12:14] == 0x0054 (start of the 0x54-byte header)
file[44:46] == 0x0001 (header + 0x20)
file[60:62] == 0x0006 (header + 0x30)
"""
h = bytearray(hdr)
struct.pack_into(e + "H", h, 0x00, 0x54)
struct.pack_into(e + "H", h, 0x20, 0x01)
struct.pack_into(e + "H", h, 0x30, 0x06)
return (struct.pack(e + "I", 0x01DF5E76) # file[0:4]
+ struct.pack(e + "H", 0x54) # file[4:6]
+ b"\x00" * 6 # file[6:12]
+ bytes(h)) + extra
# --------------------------------------------------------------------------- TFTP
class TFTPServer:
"""Minimal RFC 783 TFTP read server (octet mode), single transfer at a time."""
def __init__(self, address, files, blksize=512, timeout=2.0, retries=5, resolver=None):
self.files = dict(files)
self.resolver = resolver
self.blksize = blksize
self.timeout = timeout
self.retries = retries
self.log = []
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self._sock.bind(address)
self._sock.settimeout(timeout)
self.address = self._sock.getsockname()
self._stop = threading.Event()
self._thread = None
def start(self):
self._thread = threading.Thread(target=self._serve, daemon=True)
self._thread.start()
return self
def stop(self):
self._stop.set()
try:
self._sock.close()
except OSError:
pass
if self._thread:
self._thread.join(timeout=1)
def _serve(self):
while not self._stop.is_set():
try:
data, addr = self._sock.recvfrom(4096)
except socket.timeout:
continue # idle: keep serving
except OSError:
return # socket closed -> stop
if len(data) < 4 or data[:2] != TFTP_RRQ.to_bytes(2, "big"):
continue
name = data[2:].split(b"\x00", 1)[0].decode("latin1")
self.log.append((time.time(), name, addr))
try:
content = self.files.get(name)
if content is None and self.resolver is not None:
content = self.resolver(name)
if content is None:
self._send_error(addr, 1, "File not found")
continue
self._send_file(addr, content)
except Exception as exc: # never let one transfer kill the server
print(f"tftp: transfer error for {name!r}: {exc!r}", file=sys.stderr)
def _send_error(self, addr, code, msg):
pkt = TFTP_ERROR.to_bytes(2, "big") + code.to_bytes(2, "big") + msg.encode() + b"\x00"
try:
self._sock.sendto(pkt, addr)
except OSError:
pass
def _send_file(self, addr, content):
if content:
blocks = [content[i:i + self.blksize] for i in range(0, len(content), self.blksize)]
if len(content) % self.blksize == 0:
blocks.append(b"") # short block terminates the transfer
else:
blocks = [b""]
for idx, blk in enumerate(blocks, start=1):
num = idx & 0xFFFF
pkt = TFTP_DATA.to_bytes(2, "big") + num.to_bytes(2, "big") + blk
for _ in range(self.retries):
if self._stop.is_set():
return
try:
self._sock.sendto(pkt, addr)
except OSError as exc:
print(f"tftp: sendto {addr} failed: {exc!r}", file=sys.stderr, flush=True)
return
try:
resp, raddr = self._sock.recvfrom(4096)
except OSError:
continue
if raddr != addr:
continue
if len(resp) >= 2 and resp[:2] == TFTP_ERROR.to_bytes(2, "big"):
print(f"tftp: client aborted transfer: "
f"{resp[2:].decode('latin1', 'replace')!r}",
file=sys.stderr, flush=True)
return
if (len(resp) >= 4 and resp[:2] == TFTP_ACK.to_bytes(2, "big")
and int.from_bytes(resp[2:4], "big") == num):
break
else:
return
if self._stop.is_set():
return
# --------------------------------------------------------------------------- CLI
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
b = sub.add_parser("baseline", help="generate a valid PCF via bdftopcf")
b.add_argument("out")
b.add_argument("--name", default="vt1200test")
m = sub.add_parser("mutate", help="rewrite BITMAPS num_chars")
m.add_argument("infile")
m.add_argument("out")
m.add_argument("--num-chars", type=lambda s: int(s, 0), default=0x40000001)
s = sub.add_parser("serve", help="serve a font over TFTP")
s.add_argument("font")
s.add_argument("--host", default="0.0.0.0")
s.add_argument("--port", type=int, default=69)
s.add_argument("--name", default="font.DECW$FONT")
s.add_argument("--fonts-dir", help="also serve this file as 'fonts.dir'")
a = sub.add_parser("info", help="print a PCF's table of contents")
a.add_argument("infile")
n = sub.add_parser("snf", help="generate a minimal SNF font")
n.add_argument("out")
n.add_argument("--name", default="VT1200PROBE1")
n.add_argument("--dec-magic", action="store_true",
help="stamp DEC's version (pi*1e7) instead of MIT's 4")
n.add_argument("--dec-layout", action="store_true",
help="emit the DEC container layout the firmware checks for")
args = ap.parse_args(argv)
if args.cmd == "baseline":
build_baseline_pcf(args.out, args.name)
print(f"wrote {args.out}")
elif args.cmd == "mutate":
with open(args.infile, "rb") as fh:
data = fh.read()
was = get_num_chars(data)
out = set_num_chars(data, args.num_chars)
with open(args.out, "wb") as fh:
fh.write(out)
print(f"BITMAPS num_chars: {was} (0x{was:X}) -> {args.num_chars} (0x{args.num_chars:X})")
print(f"wrote {args.out} ({len(out)} bytes)")
elif args.cmd == "info":
with open(args.infile, "rb") as fh:
data = fh.read()
for t in pcf_tables(data):
name = LABELS.get(t["label"], hex(t["label"]))
extra = ""
if t["label"] == BITMAPS:
extra = f" num_chars={get_num_chars(data)}"
print(f" {name:14} fmt={t['format']:#x} size={t['size']:6} off={t['offset']:6}{extra}")
elif args.cmd == "snf":
data = (make_dec_snf(args.name) if args.dec_layout
else make_snf(args.name, dec_magic=args.dec_magic))
with open(args.out, "wb") as fh:
fh.write(data)
print(f"wrote {args.out} ({len(data)} bytes), font name {args.name!r}")
elif args.cmd == "serve":
with open(args.font, "rb") as fh:
content = fh.read()
files = {args.name: content, os.path.basename(args.font): content}
if args.fonts_dir:
with open(args.fonts_dir, "rb") as fh:
files["fonts.dir"] = fh.read()
srv = TFTPServer((args.host, args.port), files).start()
print(f"TFTP serving {len(files)} file(s) on {srv.address[0]}:{srv.address[1]}")
print(f" names: {', '.join(sorted(files))}")
try:
while True:
time.sleep(1)
while srv.log:
ts, name, addr = srv.log.pop(0)
print(f" {time.strftime('%H:%M:%S', time.localtime(ts))} {addr[0]}:{addr[1]} requested {name}")
except KeyboardInterrupt:
pass
finally:
srv.stop()
return 0
if __name__ == "__main__":
sys.exit(main())
Submodule tools/ghidra-headless-mcp added at 307b461cdb.
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
#
# Ghidra Headless MCP launcher for the VT1200 project.
#
# OpenCode starts this over stdio (see opencode.json in the repo root). It drives
# the vendored mrphrazer/ghidra-headless-mcp server against Ghidra 12.1.2 at
# /opt/ghidra through PyGhidra. The custom tms34010 processor module is installed
# system-wide, so the server can open this project's programs with no extra setup.
#
# Open the existing analysis (reuses the FixPtrConstants cross-references) with a
# tool call such as:
#
# project.program.open_existing
# project_location = /home/arti/code/k-space/vt1200/ghidra
# project_name = vt1200
# program_name = vt1200_v22.bin
#
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
venv="$here/ghidra-headless-mcp/.venv"
# Ghidra 12.1.2 requires Java 21+ (application.java.min=21, no max).
export GHIDRA_INSTALL_DIR="${GHIDRA_INSTALL_DIR:-/opt/ghidra}"
# If the default JDK is ever rejected, point JAVA_HOME at a JDK 21 here:
# export JAVA_HOME=/path/to/jdk-21
exec "$venv/bin/ghidra-headless-mcp" "$@"
+187
View File
@@ -0,0 +1,187 @@
diff -ruN -x .git upstream/tms34010.pspec vendored/tms34010.pspec
--- upstream/tms34010.pspec 2026-10-15 20:04:50.507977274 +0300
+++ vendored/tms34010.pspec 2026-10-15 20:04:28.658144901 +0300
@@ -1,130 +1,53 @@
-<processor_spec>
- <programcounter register="PC"/>
-
- <data_space space ="ROM" ptr_wordsize="4"/>
-
- <register_data>
- <register name="SP"/>
- <register name="ST"/>
- <register name="A0"/>
- <register name="A1"/>
- <register name="A2"/>
- <register name="A3"/>
- <register name="A4"/>
- <register name="A5"/>
- <register name="A6"/>
- <register name="A7"/>
- <register name="A8"/>
- <register name="A9"/>
- <register name="A10"/>
- <register name="A11"/>
- <register name="A12"/>
- <register name="A13"/>
- <register name="A14"/>
- <register name="B0"/>
- <register name="B1"/>
- <register name="B2"/>
- <register name="B3"/>
- <register name="B4"/>
- <register name="B5"/>
- <register name="B6"/>
- <register name="B7"/>
- <register name="B8"/>
- <register name="B9"/>
- <register name="B10"/>
- <register name="B11"/>
- <register name="B12"/>
- <register name="B13"/>
- <register name="B14"/>
- </register_data>
-
- <default_symbols>
- <symbol name="Trap_00_Address_Reset" address="rom:0xffffc" type="code_ptr" entry ="true" />
- <symbol name="Trap_01_External_Interrupt_1" address="rom:0xffff8" type="code_ptr" entry ="false" />
- <symbol name="Trap_02_External_Interrupt_2" address="rom:0xffff4" type="code_ptr" entry ="false" />
- <symbol name="Trap_03" address="rom:0xffff0" type="code_ptr" entry ="false" />
- <symbol name="Trap_04" address="rom:0xfffec" type="code_ptr" entry ="false" />
- <symbol name="Trap_05" address="rom:0xfffe8" type="code_ptr" entry ="false" />
- <symbol name="Trap_06" address="rom:0xfffe4" type="code_ptr" entry ="false" />
- <symbol name="Trap_07" address="rom:0xfffe0" type="code_ptr" entry ="false" />
- <symbol name="Trap_08_NonMaskable" address="rom:0xfffdc" type="code_ptr" entry ="false" />
- <symbol name="Trap_09_Host_Interrupt" address="rom:0xfffd8" type="code_ptr" entry ="false" />
- <symbol name="Trap_10_Display_Interrupt" address="rom:0xfffd4" type="code_ptr" entry ="false" />
- <symbol name="Trap_11_Window_Violation" address="rom:0xfffd0" type="code_ptr" entry ="false" />
- <symbol name="Trap_12" address="rom:0xfffcc" type="code_ptr" entry ="false" />
- <symbol name="Trap_13" address="rom:0xfffc8" type="code_ptr" entry ="false" />
- <symbol name="Trap_14" address="rom:0xfffc4" type="code_ptr" entry ="false" />
- <symbol name="Trap_15" address="rom:0xfffc0" type="code_ptr" entry ="false" />
- <symbol name="Trap_16" address="rom:0xfffbc" type="code_ptr" entry ="false" />
- <symbol name="Trap_17" address="rom:0xfffb8" type="code_ptr" entry ="false" />
- <symbol name="Trap_18" address="rom:0xfffb4" type="code_ptr" entry ="false" />
- <symbol name="Trap_19" address="rom:0xfffb0" type="code_ptr" entry ="false" />
- <symbol name="Trap_20" address="rom:0xfffac" type="code_ptr" entry ="false" />
- <symbol name="Trap_21" address="rom:0xfffa8" type="code_ptr" entry ="false" />
- <symbol name="Trap_22" address="rom:0xfffa4" type="code_ptr" entry ="false" />
- <symbol name="Trap_23" address="rom:0xfffa0" type="code_ptr" entry ="false" />
- <symbol name="Trap_24" address="rom:0xfff9c" type="code_ptr" entry ="false" />
- <symbol name="Trap_25" address="rom:0xfff98" type="code_ptr" entry ="false" />
- <symbol name="Trap_26" address="rom:0xfff94" type="code_ptr" entry ="false" />
- <symbol name="Trap_27" address="rom:0xfff90" type="code_ptr" entry ="false" />
- <symbol name="Trap_28" address="rom:0xfff8c" type="code_ptr" entry ="false" />
- <symbol name="Trap_29" address="rom:0xfff88" type="code_ptr" entry ="false" />
- <symbol name="Trap_30_Illegal_Opcode" address="rom:0xfff84" type="code_ptr" entry ="false" />
- <symbol name="Trap_31" address="rom:0xfff80" type="code_ptr" entry ="false" />
-
- <symbol name="HORIZONTAL_END_SYNC" address="ram:0xC0000000" entry ="false" />
- <symbol name="HORIZONTAL_END_BLANK" address="ram:0xC0000010" entry ="false" />
- <symbol name="HORIZONTAL_START_BLANK" address="ram:0xC0000020" entry ="false" />
- <symbol name="HORIZONTAL_TOTAL" address="ram:0xC0000030" entry ="false" />
- <symbol name="VERTICAL_SYNC" address="ram:0xC0000040" entry ="false" />
- <symbol name="VERTICAL_BLANK" address="ram:0xC0000050" entry ="false" />
- <symbol name="VERTICAL_START_BLANK" address="ram:0xC0000060" entry ="false" />
- <symbol name="VERTICAL_TOTAL" address="ram:0xC0000070" entry ="false" />
- <symbol name="DISPLAY_CONTROL" address="ram:0xC0000080" entry ="false" />
- <symbol name="DISPLAY_START" address="ram:0xC0000090" entry ="false" />
- <symbol name="DISPLAY_INTERRUPT" address="ram:0xC00000A0" entry ="false" />
- <symbol name="CONTROL" address="ram:0xC00000B0" entry ="false" />
- <symbol name="HOST_DATA" address="ram:0xC00000C0" entry ="false" />
- <symbol name="HOST_ADDRESS_LSB" address="ram:0xC00000D0" entry ="false" />
- <symbol name="HOST_ADDRESS_MSB" address="ram:0xC00000E0" entry ="false" />
- <symbol name="HOST_CONTROL_LSB" address="ram:0xC00000F0" entry ="false" />
- <symbol name="HOST_CONTROL_MSB" address="ram:0xC0000100" entry ="false" />
- <symbol name="INTERRUPT_ENABLE" address="ram:0xC0000110" entry ="false" />
- <symbol name="INTERRUPT_PENDING" address="ram:0xC0000120" entry ="false" />
- <symbol name="SOURCE_CONVERSION_PITCH" address="ram:0xC0000130" entry ="false" />
- <symbol name="DESTINATION_CONVERSION_PITCH" address="ram:0xC0000140" entry ="false" />
- <symbol name="PIXEL_SIZE" address="ram:0xC0000150" entry ="false" />
- <symbol name="PIXEL_MASK" address="ram:0xC0000160" entry ="false" />
- <symbol name="RESERVED" address="ram:0xC0000170" entry ="false" />
- <symbol name="RESERVED" address="ram:0xC0000180" entry ="false" />
- <symbol name="RESERVED" address="ram:0xC0000190" entry ="false" />
- <symbol name="RESERVED" address="ram:0xC00001A0" entry ="false" />
- <symbol name="DISPLAY_TAP_POINT" address="ram:0xC00001B0" entry ="false" />
- <symbol name="HORIZONTAL_COUNT" address="ram:0xC00001C0" entry ="false" />
- <symbol name="VERTICAL_COUNT" address="ram:0xC00001D0" entry ="false" />
- <symbol name="DISPLAY_ADDRESS" address="ram:0xC00001E0" entry ="false" />
- <symbol name="DRAM_REFRESH_COUNT" address="ram:0xC00001F0" entry ="false" />
- </default_symbols>
-
- <default_memory_blocks>
- <memory_block name="VRAM" start_address="ram:0x0" length="0xffffff" mode="rw" />
- <memory_block name="RAM" start_address="ram:0x01000000" length="0x003fffff" mode="rw" />
- <memory_block name="NVRAM" start_address="ram:0x01400000" length="0x1ffff" mode="rw" />
- <memory_block name="CMOS_ENABLE" start_address="ram:0x01480000" length="0x7ffff" mode="w" />
- <memory_block name="INPUT_0" start_address="ram:0x01600000" length="0xf" mode="r" />
- <memory_block name="INPUT_1" start_address="ram:0x01600010" length="0xf" mode="r" />
- <memory_block name="INPUT_2" start_address="ram:0x01600020" length="0xf" mode="r" />
- <memory_block name="DIPSWITCHES" start_address="ram:0x01600030" length="0xf" mode="r" />
- <memory_block name="PALETTE" start_address="ram:0x01800000" length="0x7ffff" mode="rw" />
- <memory_block name="DMA" start_address="ram:0x01a80000" length="0xff" mode="rw" />
- <memory_block name="midtunit_control_w" start_address="ram:0x01b00000" length="0x1f" mode="w" />
- <memory_block name="midtunit_sound_state_r" start_address="ram:0x01d00000" length="0x1f" mode="r" />
- <memory_block name="midtunit_sound_rw" start_address="ram:0x01d01020" length="0x1f" mode="rw" />
- <memory_block name="WATCHDOG" start_address="ram:0x01d81060" length="0x1f" mode="w" />
- <memory_block name="midtunit_control_w" start_address="ram:0x01f00000" length="0x1f" mode="w" />
- <memory_block name="REGISTERS" start_address="ram:0xc0000000" length="0x1ff" mode="rw" />
- <memory_block name="GFX" start_address="rom:0x02000000" length="0x07ffffff" mode="r" />
- <memory_block name="ROM" start_address="rom:0xff800000" length="0x7fffff" mode="r" />
- </default_memory_blocks>
-
-</processor_spec>
+<processor_spec>
+ <programcounter register="PC"/>
+
+ <data_space space="rom" ptr_wordsize="4"/>
+
+ <register_data>
+ <register name="SP"/>
+ <register name="ST"/>
+ <register name="A0"/>
+ <register name="A1"/>
+ <register name="A2"/>
+ <register name="A3"/>
+ <register name="A4"/>
+ <register name="A5"/>
+ <register name="A6"/>
+ <register name="A7"/>
+ <register name="A8"/>
+ <register name="A9"/>
+ <register name="A10"/>
+ <register name="A11"/>
+ <register name="A12"/>
+ <register name="A13"/>
+ <register name="A14"/>
+ <register name="B0"/>
+ <register name="B1"/>
+ <register name="B2"/>
+ <register name="B3"/>
+ <register name="B4"/>
+ <register name="B5"/>
+ <register name="B6"/>
+ <register name="B7"/>
+ <register name="B8"/>
+ <register name="B9"/>
+ <register name="B10"/>
+ <register name="B11"/>
+ <register name="B12"/>
+ <register name="B13"/>
+ <register name="B14"/>
+ </register_data>
+
+ <!--
+ VT1200 specifics are intentionally NOT declared here.
+
+ The TMS34010 ROM is loaded at byte base 0x07E40000; see MEMORY_MAP.md.
+ Upstream shipped a Midway-arcade board map (VRAM/RAM/NVRAM/PALETTE/
+ GFX/ROM blocks plus WATCHDOG, DIPSWITCHES, INPUT_n, midtunit_* and a
+ rom:0xFFFFC trap table). None of that describes the VT1200, and the
+ ROM block collided with the loaded image, so it has been removed.
+
+ To add a VT1200 map later, use MEMORY_MAP.md; note that the firmware
+ stores *bit* addresses while this language models *byte* addresses.
+ -->
+</processor_spec>
+6
View File
@@ -0,0 +1,6 @@
# Ghidra_TMS34010
A processor module of the Texas Instruments TMS34010 processor for Ghidra.
Current status is incomplete, some semantics are still needed to be finished as well as other fixes.
12/27/2022 - Added python script for importing symbols into Ghidra.
@@ -0,0 +1,88 @@
#
#@author ZPaul2Fresh8
#@category Import
#@keybinding
#@menupath
#@toolbar
#format = TYPE("FUNCTION") | NAME | ADDRESS | DESCRIPTION
from ghidra.program.model.symbol.SourceType import *
import string
functionManager = currentProgram.getFunctionManager()
c = 0 # created routines
r = 0 # renamed routines
o = 0 # omissions
l = 0 # labels
f = askFile("Give me a file to open", "Go baby go!")
for line in file(f.absolutePath): # note, cannot use open(), since that is in GhidraScript
line = line.strip()
pieces = line.split("|")
if len((pieces)) < 3:
#line_d ="{} has less than 3 pieces, skipping it."
#print(line_d.format(line_d))
o += 1
continue
try:
name = pieces[1]
value_str = "0x" + pieces[2]
value = int(value_str, 16) # convert to dec
value = (value / 8) & 0xFFFFF
#value_h = hex(value_int) # convert to hex
#value.format(value, format_spec)
address = toAddr(value)
comment = pieces[3]
try:
function_or_label = pieces[0]
except IndexError:
function_or_label = "l" or "RAM"
if function_or_label == "f" or function_or_label == "ROUTINE":
func = functionManager.getFunctionAt(address)
if func is not None:
old_name = func.getName()
func.setName(name, USER_DEFINED)
setPlateComment(address, comment)
r+=1
else:
func = createFunction(address, name)
setPlateComment(address, comment)
c+=1
else:
#print(value)
if value < 0xfffff:
#address_line = "ram:" + value
#createLabel(address, name, False)
l+=1
elif value < 0xff800000: # ram locations
print(value)
#address_line = "rom:{}" + value
createLabel(address, name, False)
#createLabel(value_str, name, 'ram', False, DEFAULT)
l+=1
#print("Created label {} at address {}".format(name, address))
except:
error = "Error: Line was not correct, trying next one."
#print(error.format(pieces[0],pieces[1],pieces[2],pieces[3]))
o+=1
continue
print("")
print("+=======================================RESULTS========================================".format(c))
print("| {} function created".format(c))
print("| {} function renamed".format(r))
print("| {} labels created".format(l))
print("| {} lines omitted due to unexpected format.".format(o))
print("+======================================================================================".format(c))
print("")
@@ -0,0 +1,81 @@
#@author ZPaul2Fresh8
#@category Import
#@keybinding
#@menupath
#@toolbar
# Python 3
#format = TYPE("FUNCTION") | NAME | ADDRESS | DESCRIPTION
from ghidra.program.model.symbol import SourceType # FIXED: Don't use * import here
import string
functionManager = currentProgram.getFunctionManager()
c = 0 # created routines
r = 0 # renamed routines
o = 0 # omissions
l = 0 # labels
f = askFile("Give me a file to open", "Go baby go!")
# FIXED: 'file()' is removed in Python 3, using 'open()'
with open(f.absolutePath, 'r') as file_handle:
for line in file_handle:
line = line.strip()
pieces = line.split("|")
if len((pieces)) < 3:
o += 1
continue
try:
name = pieces[1]
value_str = "0x" + pieces[2]
value = int(value_str, 16) # convert to dec
# FIXED: Python 3 division returns float, used // for integer division
value = (value // 8) & 0xFFFFF
address = toAddr(value)
comment = pieces[3]
try:
function_or_label = pieces[0]
except IndexError:
function_or_label = "l" # Default
if function_or_label == "f" or function_or_label == "ROUTINE":
func = functionManager.getFunctionAt(address)
if func is not None:
old_name = func.getName()
# FIXED: Explicitly calling SourceType.USER_DEFINED
func.setName(name, SourceType.USER_DEFINED)
setPlateComment(address, comment)
r+=1
else:
func = createFunction(address, name)
setPlateComment(address, comment)
c+=1
else:
if value < 0xfffff:
# createLabel(address, name, False)
l+=1
elif value < 0xff800000: # ram locations
print(value)
createLabel(address, name, False)
l+=1
except Exception as e: # FIXED: Catching specific exception to see errors
print("Error on line: " + line + " -> " + str(e))
o+=1
continue
print("")
print("+=======================================RESULTS========================================")
print("| {} function created".format(c))
print("| {} function renamed".format(r))
print("| {} labels created".format(l))
print("| {} lines omitted due to unexpected format or error.".format(o))
print("+======================================================================================")
print("")
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

+180
View File
@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="UTF-8"?>
<compiler_spec>
<data_organization>
<absolute_max_alignment value="4" />
<machine_alignment value="2" />
<default_alignment value="2" />
<default_pointer_alignment value ="2" />
<pointer_size value ="4"/>
<wchar_size value="2"/>
<short_size value="2" />
<integer_size value="4" />
<size_alignment_map>
<entry size="2" alignment="2" />
<entry size="4" alignment="4" />
</size_alignment_map>
</data_organization>
<global>
<range space="rom"/>
<range space="ram"/>
</global>
<readonly>
<range space="ram" first="0x01600000" last="0x160003f" />
</readonly>
<stackpointer register="SP" space="ram" />
<default_proto>
<prototype name="default" extrapop="0" stackshift="0" strategy="register">
<input>
<pentry minsize="4" maxsize="4" align="4">
<addr offset="0x0" space="register"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="SP"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="ST"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A0"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A1"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A2"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A3"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A4"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A5"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A6"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A7"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A8"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A9"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A10"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A11"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A12"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A13"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="A14"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B0"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B1"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B2"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B3"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B4"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B5"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B6"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B7"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B8"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B9"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B10"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B11"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B12"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B13"/>
</pentry>
<pentry minsize="4" maxsize="4">
<register name="B14"/>
</pentry>
</input>
<output>
</output>
<unaffected>
<register name="SP" />
</unaffected>
</prototype>
</default_proto>
</compiler_spec>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<language_definitions>
<language processor="TMS 34010"
endian="little"
size="32"
variant="default"
version="1.0"
slafile="tms34010.sla"
processorspec="tms34010.pspec"
id="tms34010:LE:32:default">
<description>Texas Instruments 34010</description>
<compiler name="default" spec="tms34010.cspec" id="default"/>
</language>
</language_definitions>
+53
View File
@@ -0,0 +1,53 @@
<processor_spec>
<programcounter register="PC"/>
<data_space space="rom" ptr_wordsize="4"/>
<register_data>
<register name="SP"/>
<register name="ST"/>
<register name="A0"/>
<register name="A1"/>
<register name="A2"/>
<register name="A3"/>
<register name="A4"/>
<register name="A5"/>
<register name="A6"/>
<register name="A7"/>
<register name="A8"/>
<register name="A9"/>
<register name="A10"/>
<register name="A11"/>
<register name="A12"/>
<register name="A13"/>
<register name="A14"/>
<register name="B0"/>
<register name="B1"/>
<register name="B2"/>
<register name="B3"/>
<register name="B4"/>
<register name="B5"/>
<register name="B6"/>
<register name="B7"/>
<register name="B8"/>
<register name="B9"/>
<register name="B10"/>
<register name="B11"/>
<register name="B12"/>
<register name="B13"/>
<register name="B14"/>
</register_data>
<!--
VT1200 specifics are intentionally NOT declared here.
The TMS34010 ROM is loaded at byte base 0x07E40000; see MEMORY_MAP.md.
Upstream shipped a Midway-arcade board map (VRAM/RAM/NVRAM/PALETTE/
GFX/ROM blocks plus WATCHDOG, DIPSWITCHES, INPUT_n, midtunit_* and a
rom:0xFFFFC trap table). None of that describes the VT1200, and the
ROM block collided with the loaded image, so it has been removed.
To add a VT1200 map later, use MEMORY_MAP.md; note that the firmware
stores *bit* addresses while this language models *byte* addresses.
-->
</processor_spec>
File diff suppressed because it is too large. Load diff
+51
View File
@@ -0,0 +1,51 @@
# TODO: JRcc Long Are not calculating correctly. Shorts Seem Good. 4bf84 ex
# DSJS Calcs off as well. (4bff4 ex.)
define endian=little;
define alignment=2;
define space register type=register_space size=4;
define space ram type=ram_space size=4;
define space input type=ram_space size=4;
define space graphic type=ram_space size=1;
define space rom type=ram_space size=4 default; #wordsize=2
define register offset=0x00 size=4 [ A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13 A14 ] ;
define register offset=0x40 size=4 [ B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 B12 B13 B14 ] ;
define register offset=0x80 size=4 [ PC ];
define register offset=0x7C size=4 [ SP ];
define register offset=0x84 size=4 [ ST ];
define context ST
N = (31,31)
C = (30,30)
Z = (29,29)
V = (28,28)
unused2 = (26,27)
pbx = (25,25)
unused1 = (22,24)
ie = (21,21)
reserved1 = (12,20)
fs_1_ext = (11,11)
fs_1_size = (6,10)
fs_0_ext = (5,5)
fs_0_size = (0,4)
;
@define N_Flag "ST[31,1]"
@define C_Flag "ST[30,1]"
@define Z_Flag "ST[29,1]"
@define V_Flag "ST[28,1]"
@define Interrupt "ST[21,1]"
@define FS1_Ext "ST[11,1]"
@define FS1_Size "ST[6,5]"
@define FS0_Ext "ST[5,1]"
@define FS0_Size "ST[0,4]"
# not used, vectors
@define Trap_0 "0xFFFFC" # Reset
@define Trap_1 "0xFFFF8" # External Interrupt 1
@define Trap_2 "0xFFFF4" # External Interrupt 2
@include "tms34010.sinc"
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
#
# Install (or reinstall) the patched TMS34010 processor module into Ghidra.
#
# The vendored source in ./ghidra-tms34010 is upstream
# https://github.com/ZPaul2Fresh8/Ghidra_TMS34010
# with two fixes needed for Ghidra 12 (see ghidra-tms34010-ghidra12.patch):
# - pspec <data_space space="ROM"> -> lowercase "rom" (matches the slaspec)
# - every <memory_block> gains initialized="false" (required by the new schema)
#
# Override the install location with GHIDRA_INSTALL_DIR.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
GHIDRA="${GHIDRA_INSTALL_DIR:-/opt/ghidra}"
DEST="$GHIDRA/Ghidra/Processors/TMS34010"
SUDO="${SUDO:-sudo}"
if [[ ! -x "$GHIDRA/support/sleigh" ]]; then
echo "error: $GHIDRA/support/sleigh not found (set GHIDRA_INSTALL_DIR)" >&2
exit 1
fi
$SUDO mkdir -p "$DEST/data/languages"
$SUDO cp "$HERE/ghidra-tms34010"/tms34010.{slaspec,sinc,ldefs,pspec,cspec} \
"$DEST/data/languages/"
$SUDO touch "$DEST/Module.manifest"
echo "compiling sleigh -> sla ..."
$SUDO "$GHIDRA/support/sleigh" -a "$DEST/data/languages"
# The sleigh compiler writes the .sla as root; make it readable by users.
$SUDO chmod -R a+rX "$DEST"
echo "installed TMS34010 language into $DEST"
echo "language id: tms34010:LE:32:default"
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Find the TMS34010 bit-address constants in a VT1200 ROM image.
The firmware stores absolute **bit** addresses: a 32-bit value ``P`` refers to
physical byte ``P >> 3``. A byte-addressed tool such as Ghidra therefore cannot
resolve those constants directly. This tool scans an address-ordered image
(see ``reconstruct.py``) for 32-bit little-endian values whose ``>> 3`` falls
inside the ROM, and reports where each one points.
Typical uses:
python3 tools/ptr_constants.py vt1200_v22.bin
python3 tools/ptr_constants.py vt1200_v22.bin --tsv pointers.tsv
python3 tools/ptr_constants.py vt1200_v22.bin --strings
The report is the input for the next step: a Ghidra script that rewrites these
constants (``P`` -> ``P >> 3``) so cross-references resolve. See MEMORY_MAP.md.
"""
import argparse
import struct
import sys
ROM_BASE = 0x07E40000 # physical byte base of the ROM
DEFAULT_STEP = 2 # TMS34010 is bit-addressed; 2-byte alignment
MIN_STRING = 4 # printable run length that counts as a string
def classify(image, target):
"""Return 'string' if *target* starts a printable run, else 'other'.
Returns None when *target* is outside the image.
"""
if not 0 <= target < len(image):
return None
if 0x20 <= image[target] < 0x7F:
end = target
while end < len(image) and 0x20 <= image[end] < 0x7F:
end += 1
if end - target >= MIN_STRING:
return "string"
return "other"
def scan(image, base=ROM_BASE, step=DEFAULT_STEP):
"""Yield (offset, value, target_offset, kind) for each pointer constant.
*offset* is the file offset of the 32-bit constant, *value* its raw value,
*target_offset* the ROM-relative byte it refers to.
"""
size = len(image)
for off in range(0, size - 3, step):
value = struct.unpack_from("<I", image, off)[0]
rel = (value >> 3) - base
if 0 <= rel < size:
yield off, value, rel, classify(image, rel)
def cstring(image, offset, limit=64):
end = offset
while end < len(image) and end - offset < limit and 0x20 <= image[end] < 0x7F:
end += 1
return image[offset:end].decode("latin1")
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("image", help="address-ordered ROM image")
ap.add_argument("--base", type=lambda s: int(s, 0), default=ROM_BASE,
help=f"ROM physical byte base (default 0x{ROM_BASE:X})")
ap.add_argument("--step", type=int, default=DEFAULT_STEP,
help=f"scan step in bytes (default {DEFAULT_STEP})")
ap.add_argument("--tsv", metavar="FILE",
help="write every hit as offset<TAB>value<TAB>target<TAB>kind")
ap.add_argument("--strings", action="store_true",
help="list distinct string targets")
args = ap.parse_args(argv)
image = open(args.image, "rb").read()
hits = list(scan(image, base=args.base, step=args.step))
strings = {}
other = 0
for off, value, tgt, kind in hits:
if kind == "string":
strings.setdefault(tgt, (off, value))
else:
other += 1
print(f"image : {args.image} ({len(image)} bytes)")
print(f"pointer constants: {len(hits)} "
f"({len(strings)} distinct string targets, {other} other)")
print(f"ROM byte range : 0x{args.base:08X}-0x{args.base + len(image) - 1:08X}")
print(f"bit range : 0x{args.base << 3:08X}-0x{((args.base + len(image)) << 3) - 1:08X}")
if args.strings:
print(f"\ndistinct string targets ({len(strings)}):")
for tgt in sorted(strings):
off, value = strings[tgt]
print(f" 0x{tgt:06X} <- 0x{value:08X} @file 0x{off:06X} {cstring(image, tgt)!r}")
if args.tsv:
with open(args.tsv, "w") as fh:
fh.write("offset\tvalue\ttarget\tkind\n")
for off, value, tgt, kind in hits:
fh.write(f"0x{off:06X}\t0x{value:08X}\t0x{tgt:06X}\t{kind}\n")
print(f"\nwrote {len(hits)} hits to {args.tsv}")
return 0
if __name__ == "__main__":
sys.exit(main())
+10
View File
@@ -0,0 +1,10 @@
CC = gcc
CFLAGS = -Wall -O2
dis34: dis34.c
$(CC) $(CFLAGS) -o $@ $<
clean:
rm -f dis34
.PHONY: clean
+490
View File
@@ -0,0 +1,490 @@
# TMS34010 Disassembler Project
This is a (hopefully portable) TMS34010 disassembler which has built-in options to 'intelligently' analyze ROM contents and determine which areas are most likely to be executed, and then disassembles the corresponding areas as code or data. Also include are a couple of options to load multiple ROMs in 16- and 8-bit format. The skeleton with the code base and heuristics was human-written, but I had Claude Code fill in the gaps and test it extensively.
The output format can be assembled with as34, which is a TMS34010 assembler that (as far as I remember) I got from a Texas Instruments' SDK/Devkit in the early 2000's.
---
## dis34 Usage
```
dis34 [-a] [-b base_addr] [-s skip] [file]
dis34 [-a] -e file1 [file2 ...]
dis34 [-a] -E lo1 hi1 [lo2 hi2 ...]
-a analyze mode: heuristic code/data separation
-b base bit-address (hex, default 0)
-e EPROM mode: concatenate 16-bit files, last byte at top of address space
-E 8-bit EPROM mode: interleave pairs (lo,hi), last byte at top
-s skip bytes at start of file
-p import code entry points from file (one hex address per line)
-P export discovered entry points to file
-l show address prefix on each output line (off by default)
-o show opcode bytes as comment after each instruction
-h show help
reads stdin if no file given
```
Examples:
```
./dis34 -s 80 -b fff00000 somefile.bin
./dis34 -a -E u4.bin u7.bin # single pair
./dis34 -a -E u4.bin u7.bin u6.bin u9.bin # multiple pairs
./dis34 -a -P points.txt -E u4.bin u7.bin # export entry points
./dis34 -a -p points.txt -E u4.bin u7.bin # import entry points
```
---
## TMS34010 Instruction Set Architecture
### Registers
- **A-file**: a0-a14, accessed when register bit 4 = 0
- **B-file**: b0-b14, accessed when register bit 4 = 1
- **sp**: register 15 in either file (stack pointer)
- Register encoding: 5-bit field, bit 4 = file select, bits 3:0 = register number
### Instruction Word Format
All instructions are 16-bit words (little-endian). Multi-word instructions
follow with 16-bit or 32-bit immediates/addresses.
```
Bits 15:9 - Primary opcode (op & 0xFE00)
Bits 8:5 - Sub-opcode or source register (RS)
Bits 4:0 - Destination register (RD), bit 4 = file select
Bit 9 - Field selector for MOVE/SETF (0 or 1)
```
### Primary Decode: `op & 0xFE00`
| Range | Instructions |
|-------|-------------|
| 0x0000 | rev, emu, exgpc, getpc, jump, getst, putst, popst, pushst |
| 0x0200 | nop, clrc, movb @,@, dint, abs, neg, negb, not |
| 0x0400-0x0600 | sext, zext, setf, move r/@, move @/r, move @/@, movb r/@, move @/r (with field) |
| 0x0800 | trap, call, reti, rets, mmtm, mmfm, moviw, movi (32-bit) |
| 0x0A00 | addiw, addi, cmpiw, cmpi, andi, ori, xori, subiw |
| 0x0C00 | subi (32-bit), callr, calla, eint, dsj, dsjeq, dsjne, setc |
| 0x0E00 | pixblt l/xy/b variants, fill l/xy |
| 0x1000-0x1200 | addk / inc (k=1) |
| 0x1400-0x1600 | subk / dec (k=1) |
| 0x1800-0x1A00 | movk |
| 0x1C00-0x1E00 | btst (K-form, 1's complement) |
| 0x2000-0x2200 | sla (K-form) |
| 0x2400-0x2600 | sll (K-form) |
| 0x2800-0x2A00 | sra (K-form, 2's complement: 32-stored) |
| 0x2C00-0x2E00 | srl (K-form, 2's complement: 32-stored) |
| 0x3000-0x3200 | rl (K-form) |
| 0x3800-0x3E00 | dsjs (5-bit displacement, bit 10 = negate) |
| 0x4000-0x6E00 | Register-register ALU (add, sub, cmp, and, or, xor, shift, etc.) |
| 0x8000-0xBC00 | MOVE/MOVB addressing modes |
| 0xC000-0xCE00 | Conditional jumps (JR/JA) |
| 0xD000-0xD600 | MOVE *rs(d),*rd+; MOVE @abs,*rd+; exgf |
| 0xDE00 | LINE 0 / LINE 1 |
| 0xE000-0xEE00 | addxy, subxy, cmpxy, cpw, cvxyl, movx, movy |
| 0xF000-0xFC00 | PIXT / drav |
### Complete Opcode Table
#### Register-Register (REGREG): `opval | (rs<<5) | rd`
| Mnemonic | Opval | Notes |
|----------|-------|-------|
| add | 0x4000 | |
| addc | 0x4200 | |
| sub | 0x4400 | |
| subb | 0x4600 | |
| cmp | 0x4800 | |
| btst | 0x4A00 | Register form |
| and | 0x5000 | |
| andn | 0x5200 | |
| or | 0x5400 | |
| xor | 0x5600 | clr when rs==rd |
| divs | 0x5800 | |
| divu | 0x5A00 | |
| mpys | 0x5C00 | |
| mpyu | 0x5E00 | |
| sla | 0x6000 | Register form |
| sll | 0x6200 | Register form |
| sra | 0x6400 | Register form |
| srl | 0x6600 | Register form |
| rl | 0x6800 | Register form |
| lmo | 0x6A00 | |
| mods | 0x6C00 | |
| modu | 0x6E00 | |
| addxy | 0xE000 | |
| subxy | 0xE200 | |
| cmpxy | 0xE400 | |
| cpw | 0xE600 | |
| cvxyl | 0xE800 | |
| movx | 0xEC00 | |
| movy | 0xEE00 | |
| drav | 0xF600 | |
#### One-Register (ONEREG): `opval | rd`
| Mnemonic | Opval |
|----------|-------|
| rev | 0x0020 |
| exgpc | 0x0120 |
| getpc | 0x0140 |
| jump | 0x0160 |
| getst | 0x0180 |
| putst | 0x01A0 |
| abs | 0x0380 |
| neg | 0x03A0 |
| negb | 0x03C0 |
| not | 0x03E0 |
| call | 0x0920 |
| inc | 0x1020 |
| dec | 0x1420 |
#### Implied (no operands)
| Mnemonic | Opval |
|----------|-------|
| emu | 0x0100 |
| popst | 0x01C0 |
| pushst | 0x01E0 |
| nop | 0x0300 |
| clrc | 0x0320 |
| dint | 0x0360 |
| reti | 0x0940 |
| eint | 0x0D60 |
| setc | 0x0DE0 |
| pixblt l,l | 0x0F00 |
| pixblt l,xy | 0x0F20 |
| pixblt xy,l | 0x0F40 |
| pixblt xy,xy | 0x0F60 |
| pixblt b,l | 0x0F80 |
| pixblt b,xy | 0x0FA0 |
| fill l | 0x0FC0 |
| fill xy | 0x0FE0 |
| line 0 | 0xDF1A |
| line 1 | 0xDF9A |
#### Immediate-Long + Register (ILREG): `opval | rd`, then 32-bit immediate
| Mnemonic | Opval | Notes |
|----------|-------|-------|
| addi | 0x0B20 | Also word form at 0x0B00 |
| movi | 0x09E0 | Also word form at 0x09C0 |
| ori | 0x0BA0 | |
| xori | 0x0BC0 | |
#### Negated Immediate-Long + Register (NILREG): stored as ~value
| Mnemonic | Opval | Notes |
|----------|-------|-------|
| andi | 0x0B80 | 1's complement |
| cmpi | 0x0B60 | 1's complement; also word form at 0x0B40 |
| subi | 0x0D00 | 1's complement; also word form at 0x0BE0 |
#### Constant + Register (KREG): `opval | (k<<5) | rd`, K range 1-32 (0=32)
| Mnemonic | Opval | Notes |
|----------|-------|-------|
| addk | 0x1000 | inc when k=1 |
| subk | 0x1400 | dec when k=1 |
| movk | 0x1800 | |
#### Constant/Register dual-form (KREGORREGREG)
| Mnemonic | K-form | R-form | K encoding |
|----------|--------|--------|------------|
| sla | 0x2000 | 0x6000 | Direct |
| sll | 0x2400 | 0x6200 | Direct |
| rl | 0x3000 | 0x6800 | Direct |
#### Negated Constant/Register dual-form (NKREGORREGREG)
| Mnemonic | K-form | R-form | K encoding |
|----------|--------|--------|------------|
| btst | 0x1C00 | 0x4A00 | 1's complement: (~op>>5)&0x1F |
| sra | 0x2800 | 0x6400 | 2's complement: 32-((op>>5)&0x1F) |
| srl | 0x2C00 | 0x6600 | 2's complement: 32-((op>>5)&0x1F) |
#### Special Instructions
| Mnemonic | Opval | Format |
|----------|-------|--------|
| callr | 0x0D3F | Fixed opcode + 16-bit PC-relative displacement |
| calla | 0x0D5F | Fixed opcode + 32-bit absolute address |
| mmtm | 0x0980 | opval\|rd + 16-bit register bitmask (MSB=reg0) |
| mmfm | 0x09A0 | opval\|rd + 16-bit register bitmask (LSB=reg0) |
| trap | 0x0900 | opval\|(trap_num & 0x1F) |
| rets | 0x0960 | opval\|(pop_count & 0x1F) |
| setf | 0x0540/0x0740 | opval\|fs\|(fe<<5), field in bit 9 |
| exgf | 0xD500 | opval\|rd, field in bit 9 |
| dsj | 0x0D80 | opval\|rd + 16-bit displacement |
| dsjeq | 0x0DA0 | opval\|rd + 16-bit displacement |
| dsjne | 0x0DC0 | opval\|rd + 16-bit displacement |
| dsjs | 0x3800+ | opval\|rd\|(disp<<5), bit 10 = negate |
### MOVE Addressing Modes (18 variants)
| Opval | Format | Field |
|-------|--------|-------|
| 0x4C00 | move rs, rd | No |
| 0x4E00 | move rs, rd (cross-file) | No |
| 0x8000/0x8200 | move rs, *rd | Yes |
| 0x8400/0x8600 | move *rs, rd | Yes |
| 0x8800/0x8A00 | move *rs, *rd | Yes |
| 0x9000/0x9200 | move rs, *rd+ | Yes |
| 0x9400/0x9600 | move *rs+, rd | Yes |
| 0x9800/0x9A00 | move *rs+, *rd+ | Yes |
| 0xA000/0xA200 | move rs, -*rd | Yes |
| 0xA400/0xA600 | move -*rs, rd | Yes |
| 0xA800/0xAA00 | move -*rs, -*rd | Yes |
| 0xB000/0xB200 | move rs, *rd(disp) | Yes |
| 0xB400/0xB600 | move *rs(disp), rd | Yes |
| 0xB800/0xBA00 | move *rs(d), *rd(d) | Yes |
| 0xD000/0xD200 | move *rs(d), *rd+ | Yes |
| 0x0580/0x0780 | move rd, @addr | Yes |
| 0x05A0/0x07A0 | move @addr, rd | Yes |
| 0x05C0/0x07C0 | move @addr1, @addr2 | Yes |
| 0xD400/0xD600 sub0 | move @addr, *rd+ | Yes |
"Field" column: `Yes` = appends `, 0` or `, 1` to select field register.
Paired opcodes differ by bit 9 (field select): low = field 0, high = field 1.
### MOVB Addressing Modes (9 variants)
| Opval | Format |
|-------|--------|
| 0x8C00 | movb rs, *rd |
| 0x8E00 | movb *rs, rd |
| 0x9C00 | movb *rs, *rd |
| 0xAC00 | movb rs, *rd(disp) |
| 0xAE00 | movb *rs(disp), rd |
| 0xBC00 | movb *rs(d), *rd(d) |
| 0x0340 | movb @addr1, @addr2 |
| 0x05E0 | movb rd, @addr |
| 0x07E0 | movb @addr, rd |
### PIXT Addressing Modes (6 variants)
| Opval | Format |
|-------|--------|
| 0xF000 | pixt rs, *rd, xy |
| 0xF200 | pixt *rs, xy, rd |
| 0xF400 | pixt *rs, xy, *rd, xy |
| 0xF800 | pixt rs, *rd |
| 0xFA00 | pixt *rs, rd |
| 0xFC00 | pixt *rs, *rd |
### Conditional Jump Encoding
Condition code in bits 11:8 of the instruction word:
| CC | Code | Meaning |
|----|------|---------|
| 0 | uc | Unconditional |
| 1 | p | Plus (positive) |
| 2 | ls | Low or same |
| 3 | hi | High |
| 4 | lt | Less than |
| 5 | ge | Greater or equal |
| 6 | le | Less or equal |
| 7 | gt | Greater than |
| 8 | c | Carry |
| 9 | nc | No carry |
| 10 | eq | Equal (zero) |
| 11 | ne | Not equal |
| 12 | v | Overflow |
| 13 | nv | No overflow |
| 14 | n | Negative |
| 15 | nn | Not negative |
Jump forms:
- **JR cc** (relative): `0xC000 | (cc<<8) | 0x00` + 16-bit displacement
- **JR cc** (short): `0xC000 | (cc<<8) | disp8` (8-bit signed displacement)
- **JA cc** (absolute): `0xC000 | (cc<<8) | 0x80` + 32-bit address
Displacement formula:
- 16-bit: `target = pc + 32 + (disp << 4)`
- 8-bit: `target = pc + 16 + (disp << 4)`
### The `issrl` Flag and NKREGORREGREG K-Encoding
The NKREGORREGREG instruction class (BTST, SRA, SRL) uses negated K constants,
but the negation method differs between instructions, controlled by the `issrl`
flag in the as34 assembler:
| Instruction | `issrl` | Negation | Encode formula | Decode formula |
|-------------|---------|----------|----------------|----------------|
| BTST | 0 | 1's complement | `(~K) & 0x1F` | `(~stored) & 0x1F` |
| SRA | 1 | 2's complement | `(-K) & 0x1F` | `32 - stored` |
| SRL | 1 | 2's complement | `(-K) & 0x1F` | `32 - stored` |
The two methods produce different stored values for every K from 1-31:
```
K: 1 2 3 ... 27 28 29 30 31
1's: 30 29 28 ... 4 3 2 1 0
2's: 31 30 29 ... 5 4 3 2 1
```
Using the wrong decode gives K±1 errors. In `dis34.c`:
- BTST (0x1C00): `(~op >> 5) & 0x1f` — 1's complement — correct
- SRA (0x2800): `32 - ((op >> 5) & 0x1f)` — 2's complement — correct
- SRL (0x2C00): `32 - ((op >> 5) & 0x1f)` — 2's complement — correct
---
### Round-Trip Design Decisions
1. **`jacc` for 16-bit displacement JR** -- dis34 outputs `jacc` (not `jrcc`)
for the 16-bit displacement form of conditional jumps. This ensures as34
emits the same 4-byte encoding. The 8-bit short form uses `jrcc`. The
32-bit absolute form uses `jacc`.
2. **Branch-only labels** -- Only branch/call targets (jr, ja, callr, calla,
dsj, dsjeq, dsjne, dsjs) get labels. MOVI and other data-reference
instructions keep literal hex addresses to avoid address shifts when
code section size changes.
3. **Raw data section** -- dis2asm.py reads the data section directly from the
binary (-i flag) rather than from dis34 output, because the disassembler
may misinterpret data words as instructions.
### Known Formatting Differences from tos.asm
These are formatting-only differences that do not affect binary output:
1. **`move a0, a1` vs `move a0, a1, 1`** -- Field specifier not encoded in
reg-to-reg MOVE; assembler accepts but ignores it
2. **Hex vs decimal** -- dis34 always uses `0x` prefix; tos.asm mixes decimal
and hex
3. **MMTM/MMFM register order** -- dis34 outputs registers in bit order
(ascending); assembler accepts any order
4. **Data section** -- Disassembles as `.word` directives and garbage
instructions (expected for a linear disassembler)
---
## Analyze Mode (`-a`)
Heuristic code/data separation using two passes:
1. **Pass 1 (sequential scan)**: Finds blocks of valid instructions terminated
by unconditional transfers, validates them (min 4 instructions, has a branch,
consistent register file usage, valid branch targets).
2. **Pass 2 (recursive descent)**: Seeds worklist from pass 1 branch targets
and TMS34010 hardware vector table (traps 0-31 at 0xFFFFFFE0 downward).
Traces execution paths, validating each block (min 2 instructions, register
file consistency) before committing.
### Unified Address Space in EPROM Mode
When multiple EPROM pairs are loaded (`-E lo1 hi1 lo2 hi2 ...`), the entire
combined ROM is analyzed as a single unified address space. The last pair maps
to the top of the 32-bit address space, where the TMS34010 hardware vector
table lives (0xFFFFFFE0 downward). Branches between pairs are followed
normally, allowing the recursive descent to trace execution across the full ROM.
### Heuristic Filters
- **Register file consistency**: Real TMS34010 code uses either A-file or B-file
registers consistently. Blocks with >=25% minority register file usage are
rejected as likely data.
- **Minimum trace length**: Single-instruction traces from recursive descent are
rejected to prevent cascading false positives.
### Entry Point Import/Export (`-p` / `-P`)
The `-P FILE` option exports all discovered code entry points (branch/call
targets and vector table handlers) to a file, one hex address per line. This
allows inspection and manual curation of the entry point list.
The `-p FILE` option imports entry points from a file and uses them as
additional seeds for the recursive descent analysis. The file format is one
hex address per line; lines starting with `#` are comments, blank lines are
ignored.
Typical workflow:
1. Run with `-P points.txt` to export discovered entry points
2. Edit `points.txt` to add known code addresses or remove false positives
3. Re-run with `-p points.txt` to improve analysis coverage
---
## Labels and Vector Table
### Labels
Branch and call targets are automatically labelled using the format
`L` followed by the uppercase hexadecimal bit-address (e.g., `LFFF00820:`).
Only addresses that are actually referenced by a branch, call, or vector table
entry receive labels. The corresponding branch/call instructions use the label
name instead of a raw hex address.
### Vector Table Display
When the ROM maps to the top of the 32-bit address space (as in EPROM modes),
the TMS34010 hardware vector table (traps 0–31 at 0xFFFFFFE0 downward) is
displayed as labeled `.long` entries with standard vector names:
```
RESET:
ffffffe0: .long LFFCEF750
INT1:
ffffffc0: .long LFFD7D290
INT2:
ffffffa0: .long LFFD7DDB0
ILLOP:
fffffe80: .long LFF8632E0
```
The handler addresses are shown as label references, and the corresponding
handler code receives matching labels in the disassembly output.
---
## TMS34010 I/O Registers
The TMS34010 has 28 memory-mapped I/O registers in the bit-address range
`0xC0000000`–`0xC00001F0`, spaced 0x10 apart (16 bits each). The disassembler
automatically annotates instructions that reference these addresses with the
register name as a comment.
| Bit Address | Register | Description |
|----------------|----------|---------------------------------|
| `0xC0000000` | HESYNC | Horizontal End Sync |
| `0xC0000010` | HEBLNK | Horizontal End Blank |
| `0xC0000020` | HSBLNK | Horizontal Start Blank |
| `0xC0000030` | HTOTAL | Horizontal Total |
| `0xC0000040` | VESYNC | Vertical End Sync |
| `0xC0000050` | VEBLNK | Vertical End Blank |
| `0xC0000060` | VSBLNK | Vertical Start Blank |
| `0xC0000070` | VTOTAL | Vertical Total |
| `0xC0000080` | DPYCTL | Display Control |
| `0xC0000090` | DPYSTRT | Display Start Address |
| `0xC00000A0` | DPYINT | Display Interrupt |
| `0xC00000B0` | CONTROL | Control Register |
| `0xC00000C0` | HSTDATA | Host Data |
| `0xC00000D0` | HSTADRL | Host Address Low |
| `0xC00000E0` | HSTADRH | Host Address High |
| `0xC00000F0` | HSTCTLL | Host Control Low |
| `0xC0000100` | HSTCTLH | Host Control High |
| `0xC0000110` | INTENB | Interrupt Enable |
| `0xC0000120` | INTPEND | Interrupt Pending |
| `0xC0000130` | CONVSP | Conversion Source Pitch |
| `0xC0000140` | CONVDP | Conversion Destination Pitch |
| `0xC0000150` | PSIZE | Pixel Size |
| `0xC0000160` | PMASK | Pixel/Plane Mask |
| `0xC00001B0` | DPYTAP | Display Tap Point |
| `0xC00001C0` | HCOUNT | Horizontal Counter (read-only) |
| `0xC00001D0` | VCOUNT | Vertical Counter (read-only) |
| `0xC00001E0` | DPYADR | Display Address |
| `0xC00001F0` | REFCNT | DRAM Refresh Counter |
Registers at `0xC0000170`–`0xC00001A0` are reserved/unused.
References to these registers also serve as a heuristic signal in analyze mode:
blocks that access I/O registers are almost certainly real code, so the
validation requirements are relaxed for such blocks.
BIN
View File
Binary file not shown.
File diff suppressed because it is too large. Load diff
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Inspect an *address-ordered* VT1200 ROM image.
Expects the image produced by ``reconstruct.py`` in the default (linear) mode,
where file offset == physical byte - 0x07E40000. Reports the reset vector, the
interrupt vector page and each bank header checksum.
Usage:
python3 vt1200_map.py vt1200_v22.bin
"""
import struct
import sys
ROM_BASE = 0x07E40000
BANK = 0x40000 # 256 KiB per EPROM pair
N_BANKS = 7
VECTOR_PAGE = 0x80 # 32 x 32-bit entries at the top of the image
def main(path):
d = open(path, "rb").read()
n = len(d)
print(f"image : {path} ({n} bytes = 0x{n:X})")
print(f"load base : byte 0x{ROM_BASE:08X} bit 0x{ROM_BASE << 3:08X}")
print(f"top byte : 0x{ROM_BASE + n - 1:08X} bit 0x{(ROM_BASE + n - 1) << 3:08X}")
# --- reset vector: last entry of the vector page ---
rv = struct.unpack_from("<I", d, n - 4)[0]
tgt = rv >> 3
off = tgt - ROM_BASE
where = f"file 0x{off:06X}" if 0 <= off < n else "OUT OF RANGE"
print(f"\nreset vector: 0x{rv:08X} -> byte 0x{tgt:08X} ({where})")
print(f" [8:16] = {d[8:16].hex(' ')}")
# --- interrupt vector page (last 128 bytes) ---
print(f"\nvector page @ file 0x{n - VECTOR_PAGE:06X} "
f"(logical bit 0xFFFFFC00..0xFFFFFFFF):")
base = n - VECTOR_PAGE
for k in range(VECTOR_PAGE // 4):
v = struct.unpack_from("<I", d, base + 4 * k)[0]
o = (v >> 3) - ROM_BASE
tag = f"-> file 0x{o:06X}" if 0 <= o < n else f"(0x{v:08X})"
print(f" vec{k:2d} = 0x{v:08X} {tag}")
# --- per-bank header: [checksum16][?][0xAAAA][0x5555] ---
print("\nbank headers (h0 = 16-bit word sum from the magic word to bank end):")
for b in range(N_BANKS):
off = b * BANK
h0, h1 = struct.unpack_from("<HH", d, off)
words = struct.unpack_from(f"<{(BANK - 4) // 2}H", d, off + 4)
s = sum(words) & 0xFFFF
print(f" bank {b} byte 0x{ROM_BASE + off:08X}"
f" hdr=0x{h0:04X} 0x{h1:04X} aaaa 5555"
f" sum=0x{s:04X} checksum_ok={s == h0}")
if __name__ == "__main__":
if len(sys.argv) != 2:
raise SystemExit(__doc__)
main(sys.argv[1])