diff --git a/dwt/loader/evidence/real-unit-tftp/hostload_tftp_xhello.png b/dwt/loader/evidence/real-unit-tftp/hostload_tftp_xhello.png new file mode 100644 index 0000000..05299ff Binary files /dev/null and b/dwt/loader/evidence/real-unit-tftp/hostload_tftp_xhello.png differ diff --git a/dwt/notes/REAL_UNIT.md b/dwt/notes/REAL_UNIT.md index bbb3764..050e30d 100644 --- a/dwt/notes/REAL_UNIT.md +++ b/dwt/notes/REAL_UNIT.md @@ -282,3 +282,25 @@ The LANCE address ROM at bit `0x6800000` reads (low bytes of the words): byte-identical to the MAME model. The `Ethernet Address ROM Test` passing is consistent with this. + +### Self-contained TFTP loader on the real unit + +Retargeted `tftpc.asm` to the LAN (`100.103.255.254:69`) as +`dwt/tcpip/tftpl.asm`; the RRQ name stays `dwt/tcpip/probe.bin` because the +server serves the chosen COFF for any requested name. The DOS 8.3 build name +must be <= 8 characters: `tftpc_lan` silently truncates and GSPLNK produces no +`TFTPC_LAN.OUT`; `tftpl` is safe. + +``` +server: dwt/tcpip/serve_probe.py 100.103.255.254 --file dwt/loader/build/xhello.coff +RRQ: 100.103.100.103:1036 requested 'dwt/tcpip/probe.bin' (774 bytes) +fetch: sha256 a8b3014d27deb97adcebb418a5d14da51fc3c11559e84923ede72e260d3e7271 +loader: status 0, entry 0x01840000, pcb 0x00849a10 +app: xhello stage=6 strlen=4 +``` + +Evidence: `dwt/loader/evidence/real-unit-tftp/hostload_tftp_xhello.png`. + +This is the first fully self-contained delivery on the real unit: the terminal +fetched the COFF over TFTP into `malloc` memory and ran it in a created +process, with only the rdebug jump used to start the fetch. diff --git a/dwt/tcpip/tftpl.asm b/dwt/tcpip/tftpl.asm new file mode 100644 index 0000000..0848313 --- /dev/null +++ b/dwt/tcpip/tftpl.asm @@ -0,0 +1,256 @@ +; tftpc.asm -- minimal TFTP read client built on the firmware's socket API. +; +; The firmware's own tftp_open() calls a resolver that does not return when +; called from the rdebug process (dwt/notes/TCPIP.md), so this payload speaks +; RFC 783 directly: RRQ, DATA/ACK, 512-byte blocks, one file. +; +; socket = 3F5C9A00h socket(2,2,0) -> fd +; sendto = 3F5D3D70h sendto(fd, buf, len, flags, to, tolen) -> n +; recvfrom = 3F5D7D50h recvfrom(fd, buf, len, flags, from, fromlen) -> n +; close = 3F5E8930h close(fd) +; malloc = 3F701230h malloc(bytes) -> bit pointer +; +; The transfer is 600 bytes, so DATA 1 is 512 (full) and DATA 2 is 88 +; (short); the short block ends the transfer. ACKs go to the sockaddr that +; recvfrom reports, not to a hard-coded port. +; +; Evidence cells (32-bit, bit addresses): +; 0187C000 stage (1 malloc, 2 socket, 3 receiving, 4 closed, 0FFh failed) +; 0187C020 fd +; 0187C040 destination buffer (malloc) +; 0187C060 total bytes fetched +; 0187C080 last recvfrom length +; 0187C0A0 last DATA block word as read from the wire +; 0187C0C0 marker 05A5A5A5h when the fetch completed +; 0187C200 entry SP +; 0187C220 entry A14 +; +; Scratch (fixed bit addresses, above this image): +; 01875000 argument area +; 01876000 receive buffer (2048 bytes) +; 0187A000 recvfrom source sockaddr +; 0187A080 recvfrom sockaddr length +; 0187A100 ACK packet (4 bytes) + + .text +_start: + setf 16, 0, 0 + setf 32, 0, 1 + mmtm sp, a0,a1,a2,a3,a4,a5,a6,a7 + move sp, @0187C200h, 1 + move a14, @0187C220h, 1 + + movi 1, a0, L + move a0, @0187C000h, 1 + + ; ---- buf = malloc(010000h) ------------------------------------ + move a14, -*sp, 1 + movi 01875000h, a14, L + movi 010000h, a0, L + move a0, *a14+, 1 + calla 3F701230h + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + move a8, @0187C040h, 1 + move a8, a11 ; dest cursor (callee-saved) + move a8, a8 + jreq fail + + movi 2, a0, L + move a0, @0187C000h, 1 + + ; ---- fd = socket(2,2,0) -------------------------------------- + movi 01875000h, a14, L + movi 0, a0, L + move a0, *a14+, 1 + movi 2, a0, L + move a0, *a14+, 1 + movi 2, a0, L + move a0, *a14+, 1 + calla 3F5C9A00h + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + move a8, @0187C020h, 1 + move a8, a9 ; fd (callee-saved) + move a8, a8 + jrlt fail + + ; ---- sendto(fd, RRQ, 28, 0, SADDR, 16) ------------------------ + movi 01875000h, a14, L + movi 16, a0, L + move a0, *a14+, 1 + movi SADDR, a0, L + move a0, *a14+, 1 + movi 0, a0, L + move a0, *a14+, 1 + movi 28, a0, L + move a0, *a14+, 1 + movi RRQ, a0, L + move a0, *a14+, 1 + move a9, *a14+, 1 + calla 3F5D3D70h + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + move a8, a8 + jrlt fail + + movi 3, a0, L + move a0, @0187C000h, 1 + sub a0, a0 + move a0, @0187C060h, 1 ; total = 0 + +recv_loop: + ; ---- recvfrom(fd, BUF, 2048, 0, &FROM, &FROMLEN) ------------- + ; arg5 is a POINTER to the socklen (as tftp_read passes it); the + ; value is initialised to 16 first. + movi 16, a0, L + move a0, @0187A080h, 1 ; fromlen = 16 + movi 01875000h, a14, L + movi 0187A080h, a0, L + move a0, *a14+, 1 ; arg5 = &fromlen + movi 0187A000h, a0, L + move a0, *a14+, 1 ; arg4 = &from + movi 0, a0, L + move a0, *a14+, 1 ; arg3 = flags + movi 00800h, a0, L + move a0, *a14+, 1 ; arg2 = 2048 + movi 01876000h, a0, L + move a0, *a14+, 1 ; arg1 = receive buffer + move a9, *a14+, 1 ; arg0 = fd + calla 3F5D7D50h + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + + move a8, @0187C080h, 1 ; last length + move a8, a8 + jrle fail + + ; opcode: wire 00 03 -> little-endian word 0300h + move @01876000h, a0, 0 + cmpi 0300h, a0 + jrne fail ; only DATA is accepted + + move @01876010h, a4, 0 ; block number, wire order + move a4, @0187C0A0h, 1 + + move a8, a3 + subk 4, a3 ; payload length + + ; ---- total += payload, dest += payload ----------------------- + move @0187C060h, a1, 1 + add a3, a1 + move a1, @0187C060h, 1 ; total += payload (bytes) + move a11, a6 ; copy destination + move a3, a2 + sll 3, a2 ; bytes -> bits: pointers are bit addresses + add a2, a11 ; dest cursor advances now (do NOT add bytes) + + ; ---- copy payload from BUF+4 to a6, one byte at a time ------ + ; Interrupts are disabled for the copy so a 512-byte copy is atomic + ; with respect to the firmware's ISRs. + dint + movi 01876020h, a5, L + move a3, a2 + move a3, @0187C0E0h, 1 ; payload length + move a2, @0187C140h, 1 ; copy count + move a6, @0187C120h, 1 ; write start + move a11, @0187C100h, 1 ; cursor after advance + move a5, @0187C160h, 1 ; source + jreq copy_done +copy_bytes: + movb *a5, a0 + movb a0, *a6 + addk 8, a5 ; one byte + addk 8, a6 + dsjs a2, copy_bytes +copy_done: + eint + + ; ---- ACK: 00 04 -------------------------- + movi 00400h, a0, L + move a0, @0187A100h, 0 + move a4, @0187A110h, 0 + + movi 01875000h, a14, L + movi 16, a0, L + move a0, *a14+, 1 + movi 0187A000h, a0, L + move a0, *a14+, 1 + movi 0, a0, L + move a0, *a14+, 1 + movi 4, a0, L + move a0, *a14+, 1 + movi 0187A100h, a0, L + move a0, *a14+, 1 + move a9, *a14+, 1 + calla 3F5D3D70h + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + move a8, a8 + jrlt fail + + ; short block ends the transfer; full block means read again + move @0187C080h, a2, 1 + subk 4, a2 + cmpi 512, a2 + jrlt close_it + jruc recv_loop + +close_it: + movi 4, a0, L + move a0, @0187C000h, 1 + movi 01875000h, a14, L + move a9, *a14+, 1 + calla 3F5E8930h ; close(fd) + move @0187C200h, sp, 1 + move @0187C220h, a14, 1 + setf 16, 0, 0 + setf 32, 0, 1 + movi 05A5A5A5h, a0, L + move a0, @0187C0C0h, 1 + sub a8, a8 + jruc epilogue + +fail: + movi 0FFh, a0, L + move a0, @0187C000h, 1 + movi 0FFFFFFFFh, a8, L +epilogue: + mmfm sp, a0,a1,a2,a3,a4,a5,a6,a7 + rets + +RRQ: + .word 00100h ; opcode 1 (00 01) + .word 07764h ; "dw" + .word 02F74h ; "t/" + .word 06374h ; "tc" + .word 06970h ; "pi" + .word 02F70h ; "p/" + .word 07270h ; "pr" + .word 0626Fh ; "ob" + .word 02E65h ; "e." + .word 06962h ; "bi" + .word 0006Eh ; "n\0" + .word 0636Fh ; "oc" + .word 06574h ; "te" + .word 00074h ; "t\0" +SADDR: + .word 00002h ; sin_family = AF_INET + .word 04500h ; sin_port = htons(69) + .word 06764h ; 100.103 + .word 0FEFFh ; .255.254 + .word 0 + .word 0 + .word 0 + .word 0 + .end