An 82-byte payload written in GSPA assembly, linked at bit 01800000h and loaded into main RAM by harness/mame/hello.lua, executes on the CPU: it writes DEAD/BEEF and a 1024-iteration counter into RAM and fills a 256x64 block in video RAM, which appears in the frame dump at x 256..511, y 136..199. Entry is a debugger breakpoint on the level-0 menu idle loop, not a delivery mechanism. Includes the build script, a COFF .text extractor (gsprom's hex addresses wrap at 64 KB), the run log, RAM excerpt, frame crops including a no-load control, and HELLO_WORLD_REPORT.md.
8.8 KiB
Hello world: the TMS34010 SDK builds code that runs
Status: DONE. The 1987 TI toolchain (GSPA 2.00 / GSPLNK 2.00) assembles
and links an 82-byte payload from our own source, and the payload executes on
the emulated VT1200 CPU: it writes checkable values into main RAM and a solid
block into video RAM, and the block appears in a frame dump.
This is the first time code we generated has run here. The payload is written
from scratch — it is not a reassembly of the firmware. The ROM
(vt1200_v22.bin, md5 b46a3e442ccb86134d2356bd70d22fd2) is only read.
What the payload does
Source: harness/hello-world/hello.asm. Linked image: payload.bin
(md5 db91788991ebba476b55a11127a36475), independently disassembled in
evidence/hello-payload.dis.
.text
_start:
dint ; no firmware ISR while we run
setf 16, 0, 0 ; field 0 = 16 bits
setf 32, 0, 1 ; field 1 = 32 bits
movi 0DEADh, a0, L
move a0, @01810000h, 0 ; MAGIC1
movi 0BEEFh, a0, L
move a0, @01810010h, 0 ; MAGIC2
movi 04019040h, a2, L ; VRAM base + row 100*1024 + col 64
movi 64, a3, L ; 64 rows
movi 16, a4, L ; 16 words (256 px) per row
sub a1, a1 ; counter = 0
movi 0FFFFh, a0, L
row_loop:
move a4, a5
word_loop:
move a0, *a2+, 0 ; store word, postincrement 16 bit-address units
addi 1, a1
dsjs a5, word_loop
addi 0300h, a2 ; 1024 - 256 bits to the next row
dsjs a3, row_loop
move a1, @01810020h, 0 ; COUNT = 0400h only if all 1024 stores ran
spin:
nop
jruc spin
.end
That is two independent channels:
- RAM —
0xDEADat bit0x01810000,0xBEEFat0x01810010, and a counter at0x01810020that reaches0x0400only if the two nested loops really executed 64 x 16 stores. - Video RAM — 1024 words at bit
0x04019040(framebuffer row 100, column 64) all become0xFFFF, i.e. a filled 256x64 block.
Placement and loading
- Linked origin: bit
0x01800000= byte0x00300000of the 4 MiB main RAM. The linker map (payload.map) says.text origin 01800000 length 00000290— those are bit addresses, so the image is0x290 >> 3 = 82bytes. cofextract.pyreads the linked COFF directly: the.textsection header carriespaddr = 01800000(full 32-bit bit address),size = 0x290bits andscnptr = 0xa8(file offset). It does not usegsprom, whose hex addresses wrap at 64 KB — the documented trap.harness/mame/hello.luawrites the 41 words into program space withwrite_u16(ORIGIN + i*16, lo | hi<<8), then reads every word back and compares it with the file. Result:0 bad. The frame-145 RAM dump also containspayload.binbyte-for-byte at byte0x00300000(evidence/hello-ram-marker.txt).- The load address is far above the firmware's working set at that point
(the frame-600 RAM dump's highest non-zero byte is
0x503F1), so nothing the firmware does before entry can overwrite it. - The image has no absolute self-references, so it also runs relinked at
01840000hwithHELLO_ORIGIN=0x01840000(checked: same markers, PC01840280in the spin). Only the code's placement changes; the RAM and VRAM operand addresses are absolute by design.
Entry is debugger entry, not a delivery mechanism. hello.lua lets the
firmware boot normally to the level-0 diagnostics menu, loads the payload at
frame 120, and arms a breakpoint on the menu idle loop (3FDCE400,
dsjs a9, itself). The action is { pc = 01800000 ; g }. This proves the
toolchain produces runnable machine code and the payload runs on the emulated
CPU. It says nothing about how code would reach a real terminal, and nothing
here should be read as a network or firmware-loading route.
Evidence observed (not inferred)
Run: HELLO_END=150 VT1200_DUMP_FRAME=145 VT1200_DUMP_RAM=145 VT1200_DUMP_DIR=harness/hello-world/evidence, no other environment.
Transcript: evidence/hello-run.log.
HELLO: loaded 41 words at bit 01800000; readback OK (0 bad)
HELLO: payload has run by frame 121 (PC=01800270)
HELLO: MAGIC1@01810000 = DEAD (DEAD ok)
HELLO: MAGIC2@01810010 = BEEF (BEEF ok)
HELLO: COUNT @01810020 = 0400 (1024 stores ok)
HELLO: VRAM block 1024/1024 words are FFFF, 0 holes
HELLO: VRAM row100 word before start = 003C, after end = 0000
HELLO: PC at inspection = 01800280 (payload spin expected)
0x01800280 is the jruc spin instruction — the CPU is still in the payload's
own spin loop at inspection time, not back in the firmware.
The frame dump shows the block at screen x 256..511, y 136..199 — exactly
64 rows of 256 pixels. That matches the renderer's geometry
(render_active_line(): x = heblnk + fx, y = veblnk + fby with the
firmware's heblnk = 192, veblnk = 36), so the pixels are the ones the
payload wrote, at the row and column it wrote them.
evidence/hello-frame-145.png is the crop (menu title + block).
evidence/control-frame-145.png is the same run with HELLO_AT=9999, so
the payload never loads:
- markers all
0000,0/1024words areFFFF, PC is3FDCE400(firmware); - the full-frame difference between the two runs is 13,900 pixels — the 16,384 pixels of the block minus the 2,484 text pixels that were already white.
So the block and the markers exist only when the payload is entered.
Toolchain traps that cost time (and how this build avoids them)
| trap | what happened | resolution |
|---|---|---|
jr is not a mnemonic |
INVALID OPCODE; GSPA has JReq/JRge/… and JRUC for unconditional |
use jruc |
MOVI …,W truncation |
movi 0FFFFh,a0,W gives WARNING - VALUE TRUNCATED (short immediate is sign-extended) |
use the ,L form |
gsprom hex addresses wrap at 64 KB |
cannot be used for placement | parse the COFF section header (paddr, size, scnptr); cofextract.py |
| bit vs byte | linker origin/length are bit addresses (0x290 = 82 bytes) |
verified by RAM byte comparison and execution |
| headless DOSBox | console output invisible; DOS upper-cases 8.3 names | judge success by the .LST listing and the exit files; HELLO.ASM/HELLO.CMD |
One thing that did not cause trouble: word order. Loading the gsprom/COFF
bytes low-byte-first as little-endian write_u16 words produced the intended
instructions, confirmed both by readback and by the CPU executing them
(dint, setf, stores, loops, spin).
Build and reproduce
harness/hello-world/build.sh # GSPA + GSPLNK under DOSBox, ~2 s
Then the run in harness/hello-world/README.md. payload.bin and
payload.map are committed so the emulator run needs no DOS toolchain.
Configurations and baselines
No driver or existing script was modified. Checked after the work:
| configuration | result |
|---|---|
no env, progress.lua, 200 frames |
PC=3FDCE400 samples=198 (3 distinct PCs) — unchanged |
VT1200_IO_PATH_STUB=1, 200 frames |
samples=53 (75 distinct PCs) — unchanged |
VT1200_SKIP_LEVEL1=1, frame 3000 |
Terminal Manager renders; VT1200_MOUSE=1 frame dump is pixel-identical (magick compare -metric AE = 0) |
| ROM | md5 b46a3e442ccb86134d2356bd70d22fd2, unchanged |
One documentation observation, not a regression: the committed
harness/mame/duart2_default_tm_frame3000.png differs from a current
frame-3000 dump by 4,118 pixels around the Messages pane and shows
Ethernet Address 00-00-00-00-00-00, i.e. it predates the LANCE address ROM.
It is a stale artifact; the 0-pixel tablet-vs-mouse comparison is the
configuration check that matters here.
Not attempted
The stretch goal — calling the firmware's printer routine (3FDE00C0) to put
a string on the host/printer port — was skipped deliberately. That routine
walks a null-terminated list of string pointers in a9 and calls the
firmware's output path at 3FDF97F0, which is ISR-driven on DUART1
(DUART_REPORT.md); the payload masks interrupts, and re-enabling the
firmware's interrupt state to satisfy a printer test would confound the
"payload runs" result. The RAM marker and the pixels are complete evidence on
two independent channels, and both are machine-checked.
Files added
harness/hello-world/hello.asm payload source (GSPA syntax)
harness/hello-world/build.sh assemble + link + extract
harness/hello-world/cofextract.py COFF .text extractor (bit addresses intact)
harness/hello-world/payload.bin 82-byte linked image
harness/hello-world/payload.map GSPLNK map (origin 01800000 bits)
harness/hello-world/README.md build/run/mechanism
harness/hello-world/evidence/ run log, RAM excerpt, frame crops, disassembly
harness/mame/hello.lua loader + entry breakpoint + checks