mirror of
https://git.k-space.ee/arti/vt1200.git
synced 2026-10-30 12:03:54 +02:00
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.
153 lines
5.5 KiB
Lua
153 lines
5.5 KiB
Lua
-- hello.lua -- load the hello-world payload into RAM and enter it.
|
|
--
|
|
-- MECHANISM: this is DEBUGGER ENTRY, not a delivery mechanism. The firmware
|
|
-- boots normally to the level-0 diagnostics menu; at frame HELLO_AT this script
|
|
-- writes the payload bytes into main RAM and arms a breakpoint on the menu's
|
|
-- idle loop (3FDCE400). The breakpoint action sets PC to the payload's bit
|
|
-- address and resumes. Nothing is patched on disk; the ROM is untouched.
|
|
--
|
|
-- Evidence printed from inside the emulator:
|
|
-- * payload bytes read back from RAM equal the linked binary
|
|
-- * the two magic words and the loop counter written by the payload
|
|
-- * the video-RAM block the payload wrote
|
|
--
|
|
-- <mame> vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \
|
|
-- -debug -debugger none \
|
|
-- -autoboot_script harness/mame/hello.lua -autoboot_delay 0
|
|
--
|
|
-- Env:
|
|
-- HELLO_AT frame at which to load and enter (default 120)
|
|
-- HELLO_END frame at which to inspect and exit (default 150)
|
|
-- HELLO_PAYLOAD linked payload (default harness/hello-world/payload.bin)
|
|
-- HELLO_ORIGIN payload bit address (default 01800000)
|
|
|
|
local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end
|
|
|
|
local machine = manager.machine
|
|
local cpu = machine.devices[':maincpu']
|
|
local dbg = machine.debugger
|
|
local prog = cpu.spaces['program']
|
|
|
|
if not dbg then
|
|
print("HELLO ERROR: debugger not enabled; pass -debug")
|
|
machine:exit()
|
|
return
|
|
end
|
|
|
|
local AT = tonumber(os.getenv("HELLO_AT") or "120")
|
|
local STOP_AT = tonumber(os.getenv("HELLO_END") or "150")
|
|
local ORIGIN = tonumber(os.getenv("HELLO_ORIGIN") or "0x01800000")
|
|
local PATH = os.getenv("HELLO_PAYLOAD") or "harness/hello-world/payload.bin"
|
|
local MAGIC1 = 0x01810000
|
|
local MAGIC2 = 0x01810010
|
|
local COUNT = 0x01810020
|
|
local VRAM_ROW = 0x04019040 -- 04000000h + 100*1024 + 64
|
|
local IDLE_PC = 0x3FDCE400 -- level-0 menu idle loop (dsjs a9, itself)
|
|
|
|
local payload = nil
|
|
do
|
|
local f, err = io.open(PATH, "rb")
|
|
if not f then
|
|
print("HELLO ERROR: cannot open " .. PATH .. ": " .. tostring(err))
|
|
machine:exit()
|
|
return
|
|
end
|
|
payload = f:read("*a")
|
|
f:close()
|
|
print(string.format("HELLO: payload %s bytes=%d", PATH, #payload))
|
|
end
|
|
|
|
local frames = 0
|
|
local loaded = false
|
|
local entered = false
|
|
local hist = {}
|
|
|
|
local function load_payload()
|
|
local n = math.floor(#payload / 2)
|
|
local words = {}
|
|
for i = 0, n - 1 do
|
|
local lo = payload:byte(i * 2 + 1)
|
|
local hi = payload:byte(i * 2 + 2)
|
|
words[i] = lo | (hi << 8)
|
|
prog:write_u16(ORIGIN + i * 16, words[i])
|
|
end
|
|
-- Read back and compare: this is the check that the load is placed at the
|
|
-- right bit address with the right word order.
|
|
local bad = 0
|
|
for i = 0, n - 1 do
|
|
if prog:read_u16(ORIGIN + i * 16) ~= words[i] then
|
|
bad = bad + 1
|
|
if bad <= 4 then
|
|
print(string.format("HELLO MISMATCH word %d at bit %s: wrote %04X read %04X",
|
|
i, hex(ORIGIN + i * 16), words[i], prog:read_u16(ORIGIN + i * 16)))
|
|
end
|
|
end
|
|
end
|
|
print(string.format("HELLO: loaded %d words at bit %s; readback %s (%d bad)",
|
|
n, hex(ORIGIN), bad == 0 and "OK" or "FAILED", bad))
|
|
|
|
-- Entry is a breakpoint action: the firmware reaches IDLE_PC every frame.
|
|
-- (The action only assigns PC and resumes; the payload proves itself.)
|
|
dbg:command("bpset " .. hex(IDLE_PC) .. ",,{ pc = " .. hex(ORIGIN) .. " ; g }")
|
|
end
|
|
|
|
local function inspect()
|
|
local m1 = prog:read_u16(MAGIC1)
|
|
local m2 = prog:read_u16(MAGIC2)
|
|
local ct = prog:read_u16(COUNT)
|
|
print(string.format("HELLO: MAGIC1@%s = %04X (%s)", hex(MAGIC1), m1,
|
|
m1 == 0xDEAD and "DEAD ok" or "WRONG"))
|
|
print(string.format("HELLO: MAGIC2@%s = %04X (%s)", hex(MAGIC2), m2,
|
|
m2 == 0xBEEF and "BEEF ok" or "WRONG"))
|
|
print(string.format("HELLO: COUNT @%s = %04X (%s)", hex(COUNT), ct,
|
|
ct == 0x0400 and "1024 stores ok" or "WRONG"))
|
|
|
|
-- The 16 words of row 100, then the same columns of row 163 (last row).
|
|
local function rowdump(bitaddr, tag)
|
|
local s = {}
|
|
for i = 0, 15 do
|
|
s[#s + 1] = string.format("%04X", prog:read_u16(bitaddr + i * 16))
|
|
end
|
|
print(string.format("HELLO: VRAM %s @%s: %s", tag, hex(bitaddr), table.concat(s, " ")))
|
|
end
|
|
rowdump(VRAM_ROW, "row100 ")
|
|
rowdump(VRAM_ROW + 63 * 1024, "row163 ")
|
|
-- Every word of the 64x16 region must be FFFF: no holes, no short rows.
|
|
local filled, holes = 0, 0
|
|
for r = 0, 63 do
|
|
for c = 0, 15 do
|
|
local v = prog:read_u16(VRAM_ROW + r * 1024 + c * 16)
|
|
if v == 0xFFFF then filled = filled + 1 else holes = holes + 1 end
|
|
end
|
|
end
|
|
print(string.format("HELLO: VRAM block %d/1024 words are FFFF, %d holes", filled, holes))
|
|
-- One word either side of the block must be untouched by us.
|
|
print(string.format("HELLO: VRAM row100 word before start = %04X, after end = %04X",
|
|
prog:read_u16(VRAM_ROW - 16), prog:read_u16(VRAM_ROW + 16 * 16)))
|
|
print(string.format("HELLO: PC at inspection = %s (payload spin expected)", hex(cpu.state['PC'].value)))
|
|
end
|
|
|
|
emu.register_frame_done(function()
|
|
frames = frames + 1
|
|
local pcv = cpu.state['PC'].value
|
|
hist[pcv] = (hist[pcv] or 0) + 1
|
|
if frames == AT and not loaded then
|
|
loaded = true
|
|
load_payload()
|
|
end
|
|
if loaded and not entered and prog:read_u16(MAGIC1) == 0xDEAD then
|
|
entered = true
|
|
print(string.format("HELLO: payload has run by frame %d (PC=%s)",
|
|
frames, hex(cpu.state['PC'].value)))
|
|
end
|
|
if frames >= STOP_AT then
|
|
print(string.format("--- frame %d, %d distinct PCs sampled ---", frames,
|
|
(function() local n = 0 for _ in pairs(hist) do n = n + 1 end return n end)()))
|
|
inspect()
|
|
machine:exit()
|
|
end
|
|
end, "vt1200-hello")
|
|
|
|
dbg.visible_cpu = cpu
|
|
dbg.execution_state = 'run'
|