-- loader.lua -- debugger-assisted COFF loader acceptance harness. -- -- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to -- the OS, then at frame LOADER_AT pokes: -- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin) -- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000) -- * the loader descriptor at 0x01882C00 -- and sets PC = 0x01880000. It does not use the network, the rdebug service -- or any firmware transport: this is the debugger-assisted milestone. -- -- The loader itself copies the COFF sections to their linked bit addresses and -- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes -- the input and reads the result; it never writes the app's code or markers. -- -- Env: -- LOADER_BIN loader payload (default dwt/loader/build/loader.bin) -- LOADER_COFF app COFF (default dwt/loader/build/marker.coff) -- LOADER_BUF COFF buffer bit address (default 0x01900000) -- LOADER_AT injection frame (default 2500) -- LOADER_END inspection/exit frame (default 2700) -- LOADER_OUT evidence directory (default dwt/loader/evidence) -- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0) -- LOADER_VERBOSE 1 = print the result block every 25 frames 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("LOADER: debugger not enabled; pass -debug") machine:exit() return end local AT = tonumber(os.getenv("LOADER_AT") or "2500") local ENDF = tonumber(os.getenv("LOADER_END") or "2700") local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin" local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff" local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000") local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence" local SNAP = os.getenv("LOADER_SNAP") == "1" local MODE = tonumber(os.getenv("LOADER_MODE") or "0") local A8DESC = os.getenv("LOADER_A8_DESC") == "1" local VERBOSE = os.getenv("LOADER_VERBOSE") == "1" local LOADER_BASE = 0x01880000 local DESC = 0x01882C00 local RESULT = 0x01882D00 local RETURN_STUB = 0x01883100 local RETURN_SP = 0x01883110 local function rd(p) return io.open(p, "rb") end local function slurp(p) local f, err = rd(p) if not f then error("cannot open " .. p .. ": " .. tostring(err)) end local s = f:read("*a") f:close() return s end local function u16(s, o) return s:byte(o + 1) + s:byte(o + 2) * 256 end local function u32(s, o) return s:byte(o + 1) + s:byte(o + 2) * 256 + s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216 end local function parse_coff(d) local c = { data = d } c.magic = u16(d, 0) c.nscns = u16(d, 2) c.opthdr = u16(d, 16) c.sections = {} if c.opthdr > 0 then c.optmagic = u16(d, 20) c.entry = u32(d, 20 + 16) c.textstart = u32(d, 20 + 20) c.datastart = u32(d, 20 + 24) end local base = 20 + c.opthdr for i = 0, c.nscns - 1 do local o = base + i * 40 local name = d:sub(o + 1, o + 8):gsub("%z", "") c.sections[#c.sections + 1] = { name = name, paddr = u32(d, o + 8), size = u32(d, o + 16), -- bits scnptr = u32(d, o + 20), flags = u32(d, o + 36), } end return c end local function poke_bytes(bitaddr, data) local n = math.floor((#data + 1) / 2) for i = 0, n - 1 do local lo = data:byte(i * 2 + 1) or 0 local hi = data:byte(i * 2 + 2) or 0 prog:write_u16(bitaddr + i * 16, lo + hi * 256) end return n end local function verify_bytes(bitaddr, data) local n = math.floor((#data + 1) / 2) local bad = 0 for i = 0, n - 1 do local lo = data:byte(i * 2 + 1) or 0 local hi = data:byte(i * 2 + 2) or 0 if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then bad = bad + 1 end end return n, bad end local function w32(a, v) prog:write_u32(a, v) end local function r32(a) local ok, v = pcall(function() return prog:read_u32(a) end) if ok then return v end return 0xDEADBEEF end local loader = slurp(BIN) local coff = slurp(COFF) local app = parse_coff(coff) print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE))) print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF))) print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s", app.magic, app.nscns, app.opthdr, app.optmagic and string.format("%04X", app.optmagic) or "none", hex(app.entry or 0), hex(app.textstart or 0))) for _, s in ipairs(app.sections) do print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x", s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags)) end local TRACE = os.getenv("LOADER_TRACE") == "1" local frames = 0 local injected = false local done = false local last_count = -1 local function status_name(v) local t = { [0] = "OK", [1] = "bad descriptor", [2] = "truncated COFF / section headers", [3] = "bad COFF magic", [4] = "section size not whole words", [5] = "target outside RAM", [6] = "target overlaps loader page or COFF buffer", [7] = "relocation records present (unsupported)", [8] = "target not in a free-list block", [9] = "no entry point", } return t[v] or ("error " .. tostring(v)) end local function result_dump() local st = r32(RESULT + 0x000) print(string.format( "LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d", st, status_name(st), hex(r32(RESULT + 0x020)), hex(r32(RESULT + 0x040)), r32(RESULT + 0x060), hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0), hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0), r32(RESULT + 0x100), hex(r32(RESULT + 0x120)), r32(RESULT + 0x140), r32(RESULT + 0x160))) end local function inject() -- 1. loader itself local n, bad = 0, 0 poke_bytes(LOADER_BASE, loader) n, bad = verify_bytes(LOADER_BASE, loader) print(string.format("LOADER: loader poked %d words, readback %d bad at %s", n, bad, hex(LOADER_BASE))) -- 2. COFF image poke_bytes(BUF, coff) n, bad = verify_bytes(BUF, coff) print(string.format("LOADER: COFF poked %d words, readback %d bad at %s", n, bad, hex(BUF))) -- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful for _, s in ipairs(app.sections) do if s.flags & 0x80 ~= 0 then for i = 0, math.floor(s.size / 16) - 1 do prog:write_u16(s.paddr + i * 16, 0xA5A5) end print(string.format("LOADER: pre-filled .bss %s..%s with A5A5", hex(s.paddr), hex(s.paddr + s.size))) end end -- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin) w32(DESC + 0x00, 0) prog:write_u16(DESC + 0x00, 0x4C44) prog:write_u16(DESC + 0x10, 0x5231) w32(DESC + 0x20, BUF) w32(DESC + 0x40, #coff) w32(DESC + 0x60, MODE) print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d", hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10), hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60))) -- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC -- itself); mode 1 returns here, and the stub resumes the interrupted -- PC so the caller yields and the scheduler can run the child. local orig_pc = cpu.state['PC'].value if MODE == 1 then prog:write_u16(RETURN_STUB, 0xC080) -- jauc prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF) prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF) else prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self end -- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at -- the return slot so mode=1 would return to the spin stub. -- Leave SP alone for mode 0: the loader pushes onto the current -- process stack, then restores it and resumes at ORIG_PC. (mode 1 -- would use the return stub.) w32(0x01882A20, orig_pc) -- LD_ORIG_PC print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc))) cpu.state['A8'].value = A8DESC and DESC or 0 if MODE == 1 then -- Simulate a CALLA from the current process: push the return -- address on the *firmware* stack (a real transport would do this). local sp = cpu.state['SP'].value - 0x20 w32(sp, RETURN_STUB) cpu.state['SP'].value = sp end cpu.state['PC'].value = LOADER_BASE injected = true print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)", frames, hex(LOADER_BASE), hex(cpu.state['SP'].value))) end local function inspect() result_dump() -- Verify the placement from RAM: .text/.data must equal the file bytes. for _, s in ipairs(app.sections) do if s.scnptr ~= 0 and s.size > 0 then local nb = math.floor(s.size / 8) local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb) local _, bad = verify_bytes(s.paddr, bytes) print(string.format("LOADER: placed %s at %s: %d bytes, %d differing", s.name, hex(s.paddr), nb, bad)) end end local base = COFF:match("([^/]+)$") or COFF if base:find("marker") then print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d", hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)), hex(r32(0x018E0060)), r32(0x018E0080))) elseif base:find("xhello") then print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s", r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)), hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130)))) elseif base:find("xtext") then print(string.format("LOADER: xtext: stage=%d strlen=%d display=%s font=%s window=%s gc=%s draw1=%s draw2=%s", r32(0x01842000), r32(0x01842160), hex(r32(0x01842020)), hex(r32(0x01842040)), hex(r32(0x01842060)), hex(r32(0x01842080)), hex(r32(0x01842100)), hex(r32(0x01842120)))) end print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value))) end emu.register_frame_done(function() frames = frames + 1 if frames % 50 == 0 then print(string.format("XTEXT f=%d pc=%s st=%d str=%d disp=%s font=%s win=%s gc=%s d1=%s d2=%s sp0=%s a140=%s wpc=%s wa=%s wd=%s", frames, hex(cpu.state['PC'].value), r32(0x01842000), r32(0x01842160), hex(r32(0x01842020)), hex(r32(0x01842040)), hex(r32(0x01842060)), hex(r32(0x01842080)), hex(r32(0x01842100)), hex(r32(0x01842120)), hex(r32(0x018420A0)), hex(r32(0x018420C0)), hex(r32(0x01896000)), hex(r32(0x01896020)), hex(r32(0x01896040)))) end if injected and frames >= AT and frames <= AT + 8 then print(string.format("XTSP f=%d pc=%s sp=%s a14=%s sp0=%s a140=%s st=%d", frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value), hex(cpu.state['A14'].value), hex(r32(0x01894500)), hex(r32(0x01894520)), r32(0x01895000))) end if frames == AT and not injected then inject() if TRACE then dbg:command("trace dwt/loader/evidence/real-unit-xtext/xtrace.tr") end end if injected and TRACE and frames == AT + 80 then dbg:command("trace off") print("LOADER: trace written to dwt/loader/evidence/real-unit-xtext/xtrace.tr") end if injected and not done then if TRACE and frames <= AT + 40 then print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s", frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value), hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)), hex(r32(RESULT + 0x020)))) end local st = r32(RESULT + 0x000) -- status 0 means OK; nonzero is final too once parsing has begun if st ~= 0xFFFFFFFF and not done then -- The loader writes 0 on success. A nonzero status is final -- only after it has been written once (it is 0 from power-on, -- so require the loader to have run: PC no longer at entry or -- the app counter moved). end if VERBOSE and frames % 25 == 0 then result_dump() end if base_contains_marker then local c = r32(0x018E0080) if c ~= last_count then if last_count ~= -1 then print(string.format("LOADER: marker counter %d -> %d (child running)", last_count, c)) end last_count = c end end end if frames >= ENDF then if injected then done = true inspect() if SNAP then local screen = nil for _, s in pairs(machine.screens) do screen = s break end if screen then local out = OUT .. "/loader_frame" .. frames .. ".png" local ok, err = pcall(function() return screen:snapshot(out) end) print(string.format("LOADER: snapshot %s ok=%s %s", out, tostring(ok), tostring(err or ""))) end end end machine:exit() end end, "vt1200-loader") -- (Lua has no forward use of a name defined later; set a small flag here.) base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil -- (no debugger breakpoints; the app is observed through its marker cells.) dbg.visible_cpu = cpu dbg.execution_state = 'run'