Files
vt1200/dwt/notes/LOADER.md
T
SDD 8588340527 dwt/loader: end-to-end host loader over rdebug
hostload.py drives one rdebug connection: jump(malloc) for the COFF buffer,
poke the COFF, poke the loader at 0x01880000, write the descriptor, call the
loader, and verify status 0 plus app evidence (marker cells or xhello stage 6
and a VRAM screenshot). --tftp uses the existing tftpc.bin fetch instead of a
host poke. Adds evidence for two/three fresh boots and appends the integrate
section to dwt/notes/LOADER.md, including the bit-offset result-block layout
and the create_process clobber of the descriptor.
2026-10-18 15:34:25 +03:00

25 KiB

The COFF loader: GSPLNK images, position dependence, and create_process

Status: DONE for the loader half. A GSPLNK COFF image poked into RAM is parsed, its sections are placed at the addresses in their headers, .bss is zeroed, and the image runs as a first-class process from create_process(entry, "LOADER", 0x6000, 1, 0). Two apps prove it: a marker app (MAGIC/counter cells) and xhello (an X window in a frame dump).

The transport half is separate. The loader takes an already-received image and chooses no address of its own: the COFF section headers carry the addresses.

All work is emulator-only: no driver edits, no ROM change (md5 b46a3e442ccb86134d2356bd70d22fd2), private NVRAM/cfg, no network.


1. What GSPLNK emits

dwt/loader/coff.py is the reference reader. All addresses and sizes in the section headers and optional header are bit addresses/bits; file offsets (scnptr, relptr, symptr) are bytes. This is the same model as harness/hello-world/cofextract.py, confirmed again here.

1.1 The file header (20 bytes)

00000000: 90 00 03 00 de e0 d4 6a 02 01 00 00 0e 00 00 00
00000010: 1c 00 03 01 08 01 c7 00 90 02 00 00 40 00 00 00
00000020: 40 00 00 00 00 00 8c 01 00 00 8c 01 00 00 8d 01
00000030: 2e 74 65 78 74 00 00 00 00 00 8c 01 00 00 8c 01
00000040: 90 02 00 00 a8 00 00 00 00 00 00 00 00 00 00 00
00000050: 00 00 00 00 20 00 00 00 2e 64 61 74 61 00 00 00
00000060: 00 00 8d 01 00 00 8d 01 40 00 00 00 fa 00 00 00
00000070: 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00
00000080: 2e 62 73 73 00 00 00 00 00 80 8d 01 00 80 8d 01
00000090: 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000000a0: 00 00 00 00 80 00 00 00 50 05 40 07 e2 09 00 00

This is dwt/loader/build/marker.coff (510 bytes). The header at 0x00:

offset size field this file
0x00 2 magic 0x0090
0x02 2 section count 3
0x04 4 time stamp 0x6AD4E0DE
0x08 4 symbol table offset (bytes) 0x102
0x0C 4 symbol count 14
0x10 2 optional header size (bytes) 0x1c (28)
0x12 2 flags 0x0103

0x0100 is F_RELFLG (relocation info stripped); 0x0003 is the executable flag set. The same magic 0x0090 appears in .obj files, whose flags are 0x0100 and whose optional header size is 0.

1.2 The optional header (28 bytes, at 0x14)

offset size field this file
0x14 2 magic 0x0108
0x16 2 version 0x00c7
0x18 4 text size (bits) 0x290 = 656 bits = 82 B
0x1C 4 data size (bits) 0x40 = 8 B
0x20 4 bss size (bits) 0x40 = 8 B
0x24 4 entry point (bits) 0x018C0000
0x28 4 text start (bits) 0x018C0000
0x2C 4 data start (bits) 0x018D0000

1.3 Section headers (40 bytes each, at 20 + opthdr)

offset size field
+0 8 name, NUL-padded
+8 4 paddr (bits)
+12 4 vaddr (bits)
+16 4 size (bits)
+20 4 scnptr (file offset, bytes)
+24 4 relptr (file offset, bytes)
+28 4 line-number pointer
+32 2 nreloc
+34 2 nlnno
+36 4 flags (0x20 text, 0x40 data, 0x80 bss)

For marker.coff:

sec 1 .text  paddr=018C0000 size=0x290 bits (82 B) scnptr=0xa8 nreloc=0 flags=0x20
sec 2 .data  paddr=018D0000 size=0x40 bits  (8 B) scnptr=0xfa nreloc=0 flags=0x40
sec 3 .bss   paddr=018D8000 size=0x40 bits  (8 B) scnptr=0     nreloc=0 flags=0x80

Note that GSPLNK reorders sections in the output: the input .obj order is .text, .data, .bss; the linked output puts .data, .bss, .text in xhello.coff. A loader must not assume .text is first.

1.4 Symbol table and relocations

Symbols are 18 bytes: 8-byte name, 4-byte value, 2-byte section number, 2-byte type, 1-byte storage class, 1-byte aux count. Section symbols have one 18-byte aux entry (the section size). Names longer than 8 characters live in the string table; the 8-byte field is then an offset.

Relocations are 10 bytes: 4-byte address (bit address in the section), 4-byte symbol index, 2-byte type. Documented types (Assembler Tools Guide, Table A-7):

type mnemonic
0x10 R-RELWORD (16-bit direct)
0x11 R-RELLONG (32-bit direct)
0x18 R-OCRLONG (1's-complement 32-bit)
0x19 R-GSPPCR16 (16-bit relative, words)

The .obj has relocations; the linked -a output has none (see §2).

1.5 Entry point

GSPLNK writes the entry only when -e names a global symbol. With .def _start in the source and -e _start in the command file, the map says:

ENTRY POINT SYMBOL: "_start"  address: 018C0000

and the optional header entry is 0x018C0000. Without a global _start (dwt/gui/xhello.asm has a local _start), the map says ENTRY POINT SYMBOL: 0, the optional entry is 0, but textstart is still 0x01840000. The loader therefore uses:

entry = optional.entry if nonzero, else optional.textstart

For the SDK flow, the entry is the first instruction of .text (_start) when no -e is given. It is not the raw file offset and not the section index.


2. Position-dependent, not relocatable

The linked image is fully resolved and position-dependent. The loader must place each section at the bit address in its header; it cannot load anywhere and relocate.

Evidence, from dwt/loader/reldemo.sh (full output in dwt/loader/evidence/reldemo.txt):

  1. Two objects with cross references (rel_a.asm calls ext_func and loads ext_data; rel_b.asm defines both) carry real relocations in the .obj:

    sec 1 .text ... size=0xa0 bits (20 B) scnptr=0x8c nreloc=2 flags=0x20 [text]
        reloc at 0x10 sym=8 type=0x11 R-RELLONG
        reloc at 0x70 sym=7 type=0x11 R-RELLONG
    
  2. Linking those objects with -a produces an output whose sections all have nreloc=0:

    sec 1 .text paddr=00018c0000 ... nreloc=0
    sec 2 .data paddr=00018d0000 ... nreloc=0
    
  3. Linking the same objects at two origin sets (018C0000/018D0000/018D8000 and 018E0000/018F0000/018F8000) produces different bytes — the absolute ext_data/ext_func operands:

    == .text: 22 vs 22 bytes
       differing bytes: 2
         0x0004: 8d -> 8f
         0x0010: 8c -> 8e
    

    The marker app behaves the same way: with .text linked at 018C0000 it contains the immediate 0x018C0000; at 018E0000 the same byte changes to 0x018E0000. movi _start and calla are absolute.

  4. Linking without -a still yields nreloc=0 (and paddr values relative to 0, i.e. an unbound image). GSPLNK 2.00 does not emit a relocatable executable for this flow; the toolchain's "relocatable" form is the input .obj, not the linked image.

Consequence: the loader copies to the linked address and does not malloc the segments. It refuses any input COFF that still carries relocation records (status 7), rather than guessing at relocation semantics. That also catches the easy mistake of feeding the .obj instead of the linked .out.

What "fixed address" means for the loader

The loader checks that the target range lies inside one block of the firmware's global free list (head at bit 0x1E8AC0, block header {u32 size_bits; …; u32 next@+0x20}, user data at block+0x40; see dwt/notes/HEAP.md). This is a check, not a reservation: the loader cannot make malloc return a chosen address. The caller is responsible for choosing a link address that is free when the app starts and for not handing that range out later. The loader's own page and the COFF buffer are checked separately for overlap.


3. ELF: not produced, not supported

The 1987 toolchain has no ELF output. The Assembler Tools Guide describes COFF as the object format and GSPROM as the converter to EPROM formats:

"The object format converter converts a COFF object file into Intel hex, Tektronix hex, or TI-tagged object format."

The only "ELF" case-insensitive hits in the assembler/C guides are unrelated words (the CIF command text). There is no ELF writer, no ELF reader and no ELF linker option. The 1990 OS/2 r5.01 tools are also TI COFF, not ELF. So the loader targets COFF only; an ELF loader would have nothing in this SDK to load. (dwt/loader/evidence/elf_absent.txt records the check.)


4. The loader

Source dwt/loader/loader.asm, built by dwt/loader/build.sh and delivered as raw bytes at its linked bit address.

item value
loader link/load address bit 0x01880000 (byte 0x310000)
loader .text size 0x2620 bits = 1220 bytes
reserved loader page bits 0x01880000..0x01883200
descriptor bit 0x01882C00
result block bit 0x01882D00
return stub (harness only) bit 0x01883100
app stack create_process allocates it; the loader passes 0x6000 bytes
create_process bit 0x3F706A40 / Ghidra 0x07EE0D48

The loader is position-dependent itself: its labels are absolute, so the raw image must be placed at 0x01880000. That is acceptable for the current delivery (debugger now, transport later); relinking the loader is a build-time change, not a runtime one.

4.1 Interface

The loader takes a 16-byte descriptor either at the fixed address or through A8. Offsets below are byte offsets; the assembly uses the equivalent bit offsets (+0, +0x10, +0x20, +0x40, +0x60):

DESC + 0x00  u16  0x4C44   "LD"   ) both must match
DESC + 0x02  u16  0x5231   "R1"   )
DESC + 0x04  u32  buf            bit address of the COFF image
DESC + 0x08  u32  len            image length in bytes
DESC + 0x0C  u32  mode           1 = return to caller; 0 = debug resume

A8 may be 0 (fixed descriptor) or the descriptor's bit address. Any other value falls back to the fixed descriptor if it does not carry the magic. The loader reads buf, len and mode once and then parses the COFF.

The result block is at 0x01882D00, 32-bit cells at +0x20 intervals:

offset meaning
+0x000 status: 0 = OK, else error 1..10
+0x020 entry passed to create_process (bits)
+0x040 .text paddr (bits)
+0x060 .text size (bits)
+0x080 .data paddr / +0x0A0 size
+0x0C0 .bss paddr / +0x0E0 size
+0x100 relocation records seen
+0x120 PCB returned by create_process
+0x140 section count
+0x160 descriptor length (bytes)
+0x180 loader base sanity value 0x01880000

Status codes: 1 bad descriptor; 2 truncated file/section headers; 3 bad COFF magic; 4 section size not a whole number of 16-bit words; 5 target outside RAM; 6 target overlaps the loader page or the COFF buffer; 7 relocation records present; 8 target not inside a global free-list block; 9 no entry point; 10 create_process returned 0.

4.2 Algorithm

  1. Save A14 and SP, set 16/32-bit field sizes, read the descriptor.

  2. Read the file header: magic 0x0090, nscns, opthdr; check 20 + opthdr + nscns*40 <= len.

  3. Read the optional header: entry, else textstart.

  4. For each 40-byte section header:

    • record the first .text/.data/.bss address and size from flags;
    • refuse if nreloc != 0;
    • require size % 16 == 0;
    • check scnptr + size/8 <= len when scnptr != 0;
    • check 0x100 <= paddr and paddr + size <= 0x2000000 (4 MiB of bits);
    • check the range does not overlap [0x01880000,0x01883200) or the COFF buffer;
    • check the range is inside one global free-list block;
    • if scnptr != 0, copy size/16 words from buf + scnptr*8 to paddr;
    • else store size/16 zero words at paddr (this is .bss and also any uninitialized named section).
  5. Call create_process(entry, "LOADER", 0x6000, 1, 0) with the firmware's upward-argument convention (the same shape as dwt/gui/xlaunch.asm):

    move  a14, -*sp, 1
    movi  ARG_AREA, a14, L
    movi  0, a0, L       / move a0,*a14+,1   ; arg4 = 0
    movi  1, a0, L       / move a0,*a14+,1   ; arg3 = 1
    movi  6000h, a0, L   / move a0,*a14+,1   ; arg2 = stack bytes
    movi  appname, a0, L / move a0,*a14+,1   ; arg1 = name
    move  entry, a0, 1   / move a0,*a14+,1   ; arg0 = entry (bits)
    calla 3F706A40h
    
  6. Store A8 (the PCB) and the status. If mode == 1, restore SP/A14, return the status in A8 and rets. If mode == 0, restore the interrupted state and jump to the PC the harness saved, so the OS idle loop runs and schedules the child.

4.3 Why mode 0 returns to the idle loop

A payload that spins forever in the current process starves the firmware's scheduler in this OS: the child is created but never runs. The first loader version did exactly that (PC=01882460, no markers). The fix is to restore the hijacked context and resume at the saved PC (the OS idle loop), which yields. Mode 1 returns to the caller instead; a transport that returns to its own event loop behaves the same way. The debugger harness also builds a return stub (jauc <orig_pc>) for mode 1, so both modes were exercised without a spin.


5. Evidence

5.1 Marker app: sections placed, .bss zeroed, child scheduled

dwt/loader/build/marker.coff, linked at .text 018C0000, .data 018D0000, .bss 018D8000. The harness pre-fills .bss with 0xA5A5 before the load. LOADER_MODE=0:

LOADER: result status=0 (OK) entry=018C0000 text=018C0000/656 bits data=018D0000/64 bits bss=018D8000/64 bits relocs=0 pcb=003FFBD0 nscns=3 len=510
LOADER: placed .text at 018C0000: 82 bytes, 0 differing
LOADER: placed .data at 018D0000: 8 bytes, 0 differing
LOADER: marker app: start=018C0000 data=DEADBEEF data2=12345678 bss=00000000 count=619
  • start=018C0000 is the app reading its own _start label: it ran at the linked address.
  • data=DEADBEEF, data2=12345678: .data was copied.
  • bss=00000000 after an A5A5 pre-fill: .bss was zeroed by the loader.
  • count grows every sample: the child is live, not a one-shot jump.
  • pcb=003FFBD0: create_process returned a real PCB.

LOADER_MODE=1 (descriptor + A8 variants) gives the same status and markers; the loader returns to the caller (dwt/loader/evidence/marker_mode1_run.log, marker_a8desc_run.log).

5.2 xhello: an X window from a created process

Same loader, dwt/loader/build/xhello.coff (entry 0, textstart=01840000):

LOADER: result status=0 (OK) entry=01840000 text=01840000/3120 bits data=00000000/0 bits bss=00000000/0 bits relocs=0 pcb=003FFBD0 nscns=3 len=774
LOADER: placed .text at 01840000: 390 bytes, 0 differing
LOADER: xhello: stage=6 strlen=4 display=006BC910 root=0001006B window=00010001 sync=00060001

stage=6 is the last marker in xhello.asm (after XSync), and the frame dump dwt/loader/evidence/loader_frame2650.png shows the black 400x200 window with the WM title bar at (100,100), over the Terminal Manager. The root/window cells are unreliable here because xhello's marker cells are only 0x10 bits apart and a 32-bit write overlaps the next cell; stage and the frame are the evidence. (The same overlap exists in the committed xhello markers; it does not affect execution.)

5.3 Negative: an unlinked .obj is refused

LOADER_COFF=dwt/loader/build/rela.obj (the two-object demo input):

LOADER: result status=7 (relocation records present (unsupported)) entry=00000000 ... relocs=2 pcb=00000000
LOADER: placed .text at 00000000: 20 bytes, 5 differing

The loader saw the two R-RELLONG records, refused before copying, and left the target memory untouched. This is the correct failure for the transport sending a .obj rather than the linked .out.


6. What the TFTP transport must hand it

See dwt/notes/LOADER_CROSS.md for the append-only interface note. In short:

  • a RAM buffer holding the complete GSPLNK COFF, with its bit address and byte length;
  • the loader resident at 0x01880000 (raw dwt/loader/build/loader.bin);
  • a descriptor at 0x01882C00 with buf, len, mode=1;
  • a normal calla 0x01880000 (or A8 = descriptor address);
  • the loader chooses the load address from the COFF and returns status in A8.

The transport does not pass a load address, and it must not use mode=0 (that mode resumes the debugger-hijacked PC).


7. Limits and failure modes

  • Position-dependent. The image must be linked for the address it will run at; the loader never relocates. A moved image is a different link.
  • No reservation. The free-list check proves the target is in a free block now; nothing stops a later malloc from splitting that block into the app's address. Link apps away from the allocator's low-address growth, as the current examples do (the big free block grows from byte 0x12191A upward; the apps are at byte 0x318000+).
  • No relocation records. Status 7. The SDK cannot emit them in a linked image anyway.
  • Word-granular. Section sizes must be multiples of 16 bits; the copy and zero loops move 16-bit words. GSPA/GSPLNK always emit such sizes.
  • Fixed loader address. The loader's own code is absolute. A transport that wants the loader elsewhere must relink it, not move it.
  • One process per call, no unload. create_process gets a name string "LOADER", a 0x6000-byte stack and arg = 0; the loader has no argv or environment channel. create_process returns 0 only on failure (status 10).
  • The COFF buffer must not overlap the loader page or the target sections. The loader checks both; the transport must keep its own receive buffer and code out of the target range too (the loader cannot know about them).
  • Heap-list read. Only the global free list is walked (the per-arena lists are unsafe to follow — HEAP.md). If the list is empty or the target is not in it, the loader fails with status 8 instead of corrupting memory.
  • C runtime. .text/.data/.cinit/named initialized sections are copied and .bss is zeroed, but the loader does not run the .cinit table; a C app must still enter through _c_int00 and do its own autoinit as the SDK runtime expects. The loader's entry is whatever the COFF says.

8. Reproduce

# loader (raw image, fixed address)
./dwt/loader/build.sh dwt/loader/loader.asm 01880000

# marker app: three sections at three addresses
./dwt/loader/build.sh dwt/loader/marker.asm 018C0000 018D0000 018D8000

# xhello COFF (entry stays 0; the loader uses textstart)
./dwt/loader/build.sh dwt/gui/xhello.asm 01840000

# relocation / position-dependence demo
./dwt/loader/reldemo.sh

# debugger-assisted run (no network, private NVRAM/cfg)
VT1200_SKIP_LEVEL1=1 LOADER_AT=2500 LOADER_END=2620 \
LOADER_COFF=dwt/loader/build/marker.coff LOADER_OUT=$PWD/dwt/loader/evidence \
/home/arti/code/k-space/mame0289/vt1200 vt1200 \
  -rompath harness/mame/roms -video none -sound none -nothrottle -skip_gameinfo \
  -nvram_directory $PWD/dwt/loader/run/nvram \
  -cfg_directory $PWD/dwt/loader/run/cfg \
  -debug -debugger none \
  -autoboot_script dwt/loader/loader.lua -autoboot_delay 0

LOADER_MODE=1 exercises the callable/return path; LOADER_A8_DESC=1 passes the descriptor in A8; LOADER_SNAP=1 writes a PNG at LOADER_END.

9. Evidence files

path contents
dwt/loader/loader.asm the loader
dwt/loader/marker.asm marker app with .data/.bss at separate addresses
dwt/loader/coff.py COFF reader / dumper (summary, raw, manifest)
dwt/loader/build.sh SDK build wrapper, keeps the COFF
dwt/loader/reldemo.sh, rel_a.asm, rel_b.asm relocation / origin demo
dwt/loader/loader.lua debugger-assisted harness (no network)
dwt/loader/probe_os.lua heap-list + register-write probe used to design the harness
dwt/loader/evidence/coff_layout.txt hexdump + parsed headers of marker/xhello/obj
dwt/loader/evidence/reldemo.txt the full relocation/origin output
dwt/loader/evidence/elf_absent.txt the ELF check
dwt/loader/evidence/marker_run.log mode-0 marker run
dwt/loader/evidence/marker_mode1_run.log callable/return run
dwt/loader/evidence/marker_a8desc_run.log descriptor in A8
dwt/loader/evidence/xhello_run.log xhello run
dwt/loader/evidence/loader_frame2650.png the xhello window
dwt/loader/evidence/reloc_refused.log .obj refused with status 7

10. End-to-end host-side loader (rdebug): malloc -> poke -> load -> run

Date: 2026-10-18. Driver: dwt/loader/hostload.py. Evidence: dwt/loader/evidence/hostload/ (its README.md indexes the logs and PNGs). This is the integration of the two existing halves; the loader and the apps are unchanged.

Exact sequence (one TCP connection to 192.168.99.2:771)

  1. opcode 7 liveness; the computed challenge is answered by dwt/challenge.py (the RDebug default).
  2. jump 0x3F701230 (malloc) with one argument, the COFF byte length. jump returns the callee's A8, i.e. a bit pointer.
  3. Poke the COFF to that bit pointer in 1024-byte chunks (bit + off*8, never bit + off) and peek it back: 510/510 or 774/774 bytes equal, sha256 matches the local file.
  4. Poke the raw loader (build/loader.bin, 1220 B) at bit 0x01880000, then peek it back (610 words, 0 differing). --no-poke-loader skips the write and requires that readback to match.
  5. Build the 16-byte descriptor at bit 0x01882C00 from bit offsets +0/+0x10/+0x20/+0x40/+0x60 (bytes 0/2/4/8/12): 44 4c 31 52 <buf LE32> <len LE32> 01 00 00 00.
  6. jump 0x01880000 with zero arguments. The rdebug jump handler's zero-arg path is a bare CALL after MOVE A9,A8 with A9 = 0, so the loader enters with A8 = 0 and uses the fixed descriptor. (With N args the stubs do not set A8 to the descriptor either; A8 = 0 is the reliable calling convention from rdebug.)
  7. Read the result block at bit 0x01882D00, then app evidence: marker cells by peek, or xhello stage/strlen plus a VRAM screenshot.

What worked (two or three fresh boots each)

  • jump(malloc) returns a usable, byte-aligned bit pointer in the firmware heap: 0x007c7290 (byte 0x0F8E52), 0x007c8030 (byte 0x0F9006), 0x007df450 (byte 0x0FBE8A), 0x00adcd90, 0x009fccd0. These are far below the loader page (0x01880000, byte 0x310000) and below the current apps (.text byte 0x318000), and hostload.py rejects a pointer that overlaps either, the loader page or the marker cells.
  • The loader accepts a malloc'd buffer: status 0 and A8 = 0 on every run. Its free-list check (status 8) passed for .text 0x018C0000 and for xhello .text 0x01840000; the result block reports entry/text/data/bss, pcb != 0, nscns=3, and loader_base=0x01880000.
  • create_process really runs the app: marker.coff leaves start=0x018C0000, data=0xDEADBEEF, data2=0x12345678, bss=0, and a live counter; xhello.coff reaches stage=6 (XSync) and its black 400x200 window is visible at (100,100) in hostload_xhello_boot{3,4}_xhello.png.
  • --tftp also works: the existing dwt/tcpip/tftpc.bin (bit 0x01870000) fetches into its own malloc buffer (stage=4, total=510, done=0x05A5A5A5); peek matches the COFF byte-for-byte, and that buffer is fed to the same descriptor/loader. Server RRQ, boot 3 and 4: ... requested 'dwt/tcpip/probe.bin' (510 bytes).

Two traps found

  1. Result-block offsets are bits. loader.asm's RESULT_BASE+020h is +4 bytes, so the block is 13 adjacent 32-bit cells (52 B), not cells at byte intervals of 0x20. Reading at byte offsets 0x20, 0x40, ... returns 0x55555555 from the free heap beyond the real block; that was a host parsing bug, not a loader failure.
  2. The descriptor is clobbered after a load. A successful create_process leaves a call frame at 0x01882C00 (0d 00 00 00 <pcb> <ptr> 00 60 00 00); ARG_AREA is 0x01882B00 and the frame is 0x100 bits wide. The loader has already consumed the descriptor, and the result block (0x01882D00) is untouched, but a second call must rewrite the descriptor. hostload.py does so immediately before every jump.

Limits

  • No reservation. malloc only finds a buffer; the loader's free-list check is still a check. The apps stay linked at byte 0x318000/0x308000 while allocations grow from the low free blocks; enough allocations on one boot would eventually split the big block up to them. Fresh-boot runs are safe; on a long-lived unit the caller must still choose a free link address.
  • --tftp needs a server that serves the COFF under tftpc's fixed name (dwt/tcpip/probe.bin); tftpc.asm has no filename argument. The server used here was serve_probe.py 192.168.99.1 --bootp --file dwt/loader/build/marker.coff.
  • --frame N cannot trigger a MAME dump. It only waits for vt1200-frame-N.ppm from an externally configured VT1200_DUMP_FRAME(S); frame numbers are boot-relative, so the load-relative visual evidence is the rdebug VRAM screenshot.
  • The loader is still position-dependent and fixed at 0x01880000, and the loaded images are position-dependent; none of this adds relocation.
  • rdebug process only: create_process is used for the app, not for the loader or the TFTP client. A create_process child previously returned -1 from socket() (TCPIP.md), so the fetch stays in the rdebug process.
  • The real unit would need TCP 771 reachable and the same debug-gate state; the emulator run uses the documented VT1200_SKIP_LEVEL1=1 divergence to reach the OS.