forked from arti/vt1200
dwt/tcpip: map the socket API and fetch a file over TFTP into RAM
The firmware's tftp_open hangs in its resolver from the rdebug process, so the payload speaks RFC 783 over the firmware socket API: socket(2,2,0), sendto(RRQ), recvfrom(DATA)/ACK into a malloc'd buffer, close. Verified live: 600 B fetched from 192.168.99.1 into the malloc buffer, server RRQ logged, and rdebug peek matches probe.bin byte-for-byte. Notes: dwt/notes/TCPIP.md (function table, ABI, limits) and the loader/transport hand-off in dwt/notes/LOADER_CROSS.md.
This commit is contained in:
1 parent
139d89a10c
commit
01923ecb69
47 files changed
+3394
No files matched your search
@@ -373,3 +373,18 @@ Facts for anyone tempted to "just create a window" (details in
|
||||
client is "one call away".
|
||||
* Shared-RAM UI mutation still works (opcode 8 / `0x3F309350`), and that is the
|
||||
realistic route for making the Terminal Manager draw something.
|
||||
|
||||
## 2026-10-18 15:25 EEST — TFTP fetch into a malloc buffer works from rdebug
|
||||
|
||||
For the COFF-loader agent (details: `dwt/notes/LOADER_CROSS.md` and
|
||||
`dwt/notes/TCPIP.md`):
|
||||
|
||||
* The firmware `tftp_open` hangs from the rdebug process (its resolver; no RRQ).
|
||||
Use the socket API instead: `socket` `3F5C9A00`, `sendto` `3F5D3D70`,
|
||||
`recvfrom` `3F5D7D50` (`arg5 = &socklen`, initialise to 16), `close`
|
||||
`3F5E8930`, `malloc` `3F701230`. Stack args, last first.
|
||||
* Verified: `dwt/tcpip/tftpc.asm` (bit `01870000`) fetched 600 B into the
|
||||
malloc buffer `0x00b5bcd0`; server RRQ `'dwt/tcpip/probe.bin'`; peek/cmp
|
||||
byte-identical (sha256 `9662e62b…333359`). Run it in the **rdebug process**.
|
||||
* Pointers are bit addresses: advance a data cursor by `n*8`. Keep the 2048-B
|
||||
receive buffer and the `from`/`fromlen`/ACK cells disjoint.
|
||||
@@ -128,3 +128,28 @@ out of the target sections; the loader cannot see them.
|
||||
offsets match it.
|
||||
* `dwt/notes/LOADER.md` — the full COFF layout, the position-dependence
|
||||
evidence and the loader algorithm.
|
||||
|
||||
## 2026-10-18 15:25 EEST — TFTP fetch works from the rdebug process
|
||||
|
||||
* **Exact fetch call:** the firmware's `tftp_open` is unusable (hangs in its
|
||||
resolver, no RRQ). Use the minimal client in `dwt/tcpip/tftpc.asm`
|
||||
(bit `01870000`, 694 B): `malloc(0x10000)` at `3F701230`, `socket(2,2,0)` at
|
||||
`3F5C9A00`, `sendto(fd,buf,len,0,to,16)` at `3F5D3D70`,
|
||||
`recvfrom(fd,buf,2048,0,&from,&fromlen)` at `3F5D7D50` with `*fromlen=16`,
|
||||
`close(fd)` at `3F5E8930`. Stack arguments are pushed **last first**; the
|
||||
sixth `recvfrom` argument is a **pointer to the socklen**.
|
||||
* **Buffer/process:** run in the **rdebug process** (ordinary `jump`); no
|
||||
`create_process` and no registration. A child returned `-1` from `socket()`
|
||||
in the one attempt. `malloc` returns a **bit** address; advance the
|
||||
destination by `n*8`, not `n`. Keep the receive buffer (2048 B) and the
|
||||
`from`/`fromlen`/ACK cells disjoint — a `0x800` bit offset is only 256 bytes.
|
||||
* **Verified:** 600 B fetched into the malloc buffer at `0x0095bbd0`, server
|
||||
RRQ `15:23:48 ... requested 'dwt/tcpip/probe.bin'`, and `peek`/`cmp` match
|
||||
byte-for-byte (sha256 `9662e62b…333359`). The payload returns 0; rdebug stays
|
||||
live.
|
||||
* **Hand-off:** the fetched buffer's bit address and byte length go straight
|
||||
into the descriptor at `0x01882C00` (`magic 4C44/5231, buf, len, mode=1`),
|
||||
then `calla 01880000h` with `A8=0` or the descriptor address. The transport
|
||||
scratch is below `0x01880000` (recv `01876000`, `from` `0187A000`, `fromlen`
|
||||
`0187A080`, ACK `0187A100`), so it does not collide with the loader page
|
||||
`01880000`–`01883200`.
|
||||
@@ -0,0 +1,251 @@
|
||||
# VT1200 TCP/IP and a working TFTP fetch
|
||||
|
||||
Date: 2026-10-18. Firmware `vt1200_v22.bin` md5
|
||||
`b46a3e442ccb86134d2356bd70d22fd2` (**unmodified**). No driver edits, no ROM
|
||||
patch. The emulator ran the documented `VT1200_SKIP_LEVEL1=1` divergence with a
|
||||
private `-cfg_directory`/`-nvram_directory`, the LANCE on `taptun`, BOOTP from a
|
||||
private server, and `dwt/tcpip/probe.bin` served over TFTP.
|
||||
|
||||
**Result:** a payload (`dwt/tcpip/tftpc.asm`) fetched a 600-byte file from
|
||||
`192.168.99.1:69` into a `malloc`'d buffer, in the **rdebug process** (ordinary
|
||||
`jump`, no `create_process`), and `peek` returned it byte-for-byte:
|
||||
|
||||
```
|
||||
stage=4 fd=1 buf=0x0095bbd0 total=600 last=92 block=0x0200 done=0x05a5a5a5
|
||||
OK: 600 bytes at 0x0095bbd0 match dwt/tcpip/probe.bin
|
||||
sha256=9662e62b45802984e5eef9c7410ad7482645eb5ff77805f0406d248e35333359
|
||||
```
|
||||
|
||||
Server-side RRQ (fresh server log for that run):
|
||||
|
||||
```
|
||||
15:23:48 RRQ 192.168.99.2:1025 requested 'dwt/tcpip/probe.bin' (600 bytes)
|
||||
```
|
||||
|
||||
The firmware's own `tftp_open` is **not usable from the rdebug process**: it
|
||||
never returns and emits no RRQ (details in §3). The fetch is therefore a
|
||||
minimal RFC 783 client built on the firmware's socket API — the allowed
|
||||
fallback, not a mock.
|
||||
|
||||
---
|
||||
|
||||
## 1. Stack architecture
|
||||
|
||||
TCP/IP is an **in-process library with a kernel-lock wrapper**, not a separate
|
||||
service with an IPC protocol. Every socket call is an ordinary ROM function
|
||||
taking stack arguments in the firmware convention:
|
||||
|
||||
```
|
||||
MOVE A14,-*SP,1 ; reserve the slot the callee restores
|
||||
MOVI argN,A0 / MOVE A0,*A14+,1 ; last argument first, upward via A14
|
||||
...
|
||||
MOVI arg0,A0 / MOVE A0,*A14+,1 ; first argument last
|
||||
CALLA target
|
||||
```
|
||||
|
||||
The wrappers take the per-process lock at `[@0x1B3900]+0x1C1` and dispatch
|
||||
through the kernel entry, but from the caller's point of view they are library
|
||||
calls: no registration, no service name, and no X-style attach. The arguments
|
||||
are the normal BSD socket arguments; `struct sockaddr` is a 16-byte
|
||||
`{u16 family; u16 port; u32 addr; u8 zero[8]}` in **network byte order** (the
|
||||
firmware calls `htons(69)` itself in `tftp_open`).
|
||||
|
||||
### Verified entry points
|
||||
|
||||
| function | bit address | Ghidra | ABI (source order) | status |
|
||||
|---|---|---|---|---|
|
||||
| `socket` | `3F5C9A00` | `07EB9340` | `socket(domain, type, protocol)` -> fd | **verified live**, returns 1 |
|
||||
| `bind` | `3F5CAA80` | `07EB9550` | `bind(fd, sockaddr*, len)` | from the BOOTP caller |
|
||||
| `listen` | `3F5CBCD0` | `07EB979A` | `listen(fd, backlog)` | from inetd (`backlog=5`) |
|
||||
| `accept` | `3F5CCB40` | `07EB9968` | `accept(fd, sockaddr*, len*)` | from inetd (`NULL,NULL`) |
|
||||
| `sendto` | `3F5D3D70` | `07EBA7AE` | `sendto(fd, buf, len, flags, to, tolen)` -> n | **verified live** (28-byte RRQ) |
|
||||
| `recvfrom` | `3F5D7D50` | `07EBAFAA` | `recvfrom(fd, buf, len, flags, &from, &fromlen)` -> n | **verified live** (516/92-byte DATA) |
|
||||
| `read` | `3F5D8E70` | `07EBB1CE` | `read(fd, buf, len)` | the rdebug/X read wrapper named in the brief |
|
||||
| `close` | `3F5E8930` | `07EBD126` | `close(fd)` | **verified live** |
|
||||
| `setsockopt` | `3F5DD260` | `07EBBA4C` | `setsockopt(fd, level, name, val*, len)` | from the BOOTP/X callers |
|
||||
| `select` (candidate) | `3F871CE0` | `07F0E39C` | `select(nfds, r, w, e, timeout)` | wrapper used by Xlib; not exercised |
|
||||
| `malloc` | `3F701230` | `07EE0246` | `malloc(bytes)` -> **bit** pointer | **verified live** |
|
||||
| `create_process` | `3F706A40` | `07EE0D48` | `create_process(entry,name,stack,1,arg)` | known from the GUI work |
|
||||
|
||||
`connect` was not needed and is not in this table: the fetch uses UDP
|
||||
`sendto`/`recvfrom` only. The candidates in the same wrapper block
|
||||
(`3F5CF8B0`, `3F5E43F0`) were not exercised, so no ABI is claimed for them.
|
||||
|
||||
`recvfrom`'s sixth argument is a **pointer to a `socklen_t`**, and the caller
|
||||
must initialise it to 16 before the call. Its fifth argument is a pointer to
|
||||
the source `sockaddr`. That is exactly how the firmware's own `tftp_read`
|
||||
passes them (`&0x1CB8A0` / `&0x1CB920`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Per-process requirements
|
||||
|
||||
* **rdebug process: works.** `socket`, `sendto` and `recvfrom` all return
|
||||
normally from the process the `jump` handler runs in. The fd is process-local
|
||||
(`socket` returned 1 in that process).
|
||||
* **No network context or registration is needed.** The terminal already has
|
||||
its address (BOOTP) and the stack is up; the calls are plain library calls.
|
||||
* **`create_process` child:** one attempt (`dwt/tcpip/launcher.asm` +
|
||||
`socktest.asm`) returned `-1` from `socket()` (`sfd=FFFFFFFF`) even though the
|
||||
PCB was valid (`pcb=003FFBD0`) and the child had started (`stage=2`). The
|
||||
machine then went into the boot delay loop. That is **not** a clean child
|
||||
result, but it is enough to say the working context here is the rdebug
|
||||
process, not a child. Do not assume the X result (`create_process` is needed)
|
||||
transfers to sockets; it did not in this test.
|
||||
|
||||
---
|
||||
|
||||
## 3. The firmware TFTP client (why it was not used)
|
||||
|
||||
The client is `dwt_tftp.c` compiled into ROM:
|
||||
|
||||
| function | bit address | Ghidra | ABI |
|
||||
|---|---|---|---|
|
||||
| `tftp_open` | `3F5EBC40` | `07EBD788` | `tftp_open(host, filename)` -> fd, or `-1` |
|
||||
| `tftp_read` | `3F5ED230` | `07EBDA46` | `tftp_read(fd, buf, bufsize)` -> DATA payload length, `0` at EOF, `-1` error |
|
||||
| `tftp_close` | `3F5EC860` | `07EBD90C` | `tftp_close(fd)` |
|
||||
|
||||
`tftp_open` does, in order: `socket(2,2,0)`, `memset` a 16-byte `sockaddr`,
|
||||
`sa_family=2`, resolve the host (`3F5933D0`; on `-1` it retries through
|
||||
`3F5B8580`), `htons(69)` into the port, build the RRQ in its static buffer
|
||||
(`0x1C85A0`) with mode `octet`, and `sendto`. It returns the fd.
|
||||
|
||||
`tftp_read` receives into its own static buffer `0x1CB960` with length `0x204`,
|
||||
checks opcode/block, copies `len-4` bytes to the caller's `buf`, ACKs, and sets
|
||||
`@0x1CB940=1` when the block was shorter than 512. **Read until it returns 0**;
|
||||
if the caller stops after a full 512-byte block, `tftp_close` sends TFTP ERROR 3
|
||||
("Disk full or allocation exceeded") — the behaviour documented in
|
||||
`harness/mame/FONT_STORAGE_REPORT.md`.
|
||||
|
||||
**Why it is unusable:** calling `tftp_open("192.168.99.1","…")` from the rdebug
|
||||
process never returns and produces no RRQ. `socket()` alone works (proved with
|
||||
`socktest.asm`: fd 1), so the hang is downstream; a direct call to the resolver
|
||||
`3F5933D0` also wedged the service and the machine fell into the boot delay
|
||||
loop. The exact resolver failure was not reduced further, because the socket
|
||||
API is sufficient and the fallback is legitimate. **Do not call `tftp_open`
|
||||
from a payload on this image.**
|
||||
|
||||
---
|
||||
|
||||
## 4. The working fetch
|
||||
|
||||
Payload: `dwt/tcpip/tftpc.asm`, built by `dwt/tcpip/build.sh` to
|
||||
`dwt/tcpip/build/tftpc.bin` (694 bytes, linked at bit `01870000`).
|
||||
|
||||
It is a minimal RFC 783 client:
|
||||
|
||||
1. `buf = malloc(0x10000)`; keep the returned **bit** pointer in `a11`.
|
||||
2. `fd = socket(2,2,0)`; keep it in `a9`.
|
||||
3. `sendto(fd, RRQ, 28, 0, &server, 16)`, where the RRQ is
|
||||
`dwt/tcpip/probe.bin` in `octet` mode and `server` is
|
||||
`{AF_INET, htons(69), 192.168.99.1}`.
|
||||
4. `recvfrom(fd, recv, 2048, 0, &from, &fromlen)`; accept only opcode 3
|
||||
(`00 03` on the wire, read as the 16-bit word `0x0300`).
|
||||
5. Copy `len-4` payload bytes to `buf`, send ACK `00 04 <block>` to the
|
||||
`from` address returned by `recvfrom`, advance `buf` by `(len-4)*8` **bits**.
|
||||
6. Repeat while the block was full (512); a short block ends the transfer.
|
||||
`close(fd)`.
|
||||
|
||||
Evidence:
|
||||
|
||||
```
|
||||
# server (dwt/tcpip/serve_probe.py, fresh log)
|
||||
15:23:48 RRQ 192.168.99.2:1025 requested 'dwt/tcpip/probe.bin' (600 bytes)
|
||||
|
||||
# MAME-side markers (probe.lua)
|
||||
TCPC cstage=00000004 cfd=00000001 cbuf=00B5BCD0 ctotal=00000258 \
|
||||
clast=0000005C cblock=00000200 cdone=05A5A5A5
|
||||
|
||||
# rdebug peek + byte comparison (dwt/tcpip/verify.py)
|
||||
stage=4 fd=1 buf=0x0095bbd0 total=600 last=92 block=0x0200 done=0x05a5a5a5
|
||||
OK: 600 bytes at 0x0095bbd0 match dwt/tcpip/probe.bin
|
||||
sha256=9662e62b45802984e5eef9c7410ad7482645eb5ff77805f0406d248e35333359
|
||||
|
||||
$ cmp dwt/tcpip/run/fetched.bin dwt/tcpip/probe.bin
|
||||
(no output = identical)
|
||||
```
|
||||
|
||||
`ctotal` counts the 512-byte block 1 plus the 88-byte block 2; `clast=0x5C`
|
||||
is the 92-byte DATA packet (88 + 4-byte header); `cblock=0x0200` is the wire
|
||||
word for block 2 (bytes `00 02`).
|
||||
|
||||
The payload returns 0, so the same rdebug connection and later connections are
|
||||
still live. No breakpoints, no ROM patch, no `create_process`.
|
||||
|
||||
### The two bugs that cost the debugging cycles
|
||||
|
||||
1. **Pointers are bit addresses.** After copying `n` bytes, the destination
|
||||
cursor must advance by `n*8`. Adding `n` puts block 2 at `dest+64` for a
|
||||
512-byte block, which looks like a copy corruption.
|
||||
2. **The receive buffer and the `from` sockaddr must not overlap.** A bit
|
||||
offset of `0x800` is 256 bytes, not 2048: with the receive buffer at
|
||||
`0187B000`, `from=0187B800` sat 256 bytes into the 2048-byte buffer and the
|
||||
stack's `from` write corrupted the payload at offset 252. The working layout
|
||||
is recv `01876000` (2048 bytes), `from` `0187A000`, `fromlen` `0187A080`,
|
||||
ACK `0187A100`.
|
||||
|
||||
---
|
||||
|
||||
## 5. What a loader must do to fetch a COFF at runtime
|
||||
|
||||
1. Deliver a small bootstrap with rdebug `poke` + `jump` (or `create_process` if
|
||||
a child is needed for the loader itself).
|
||||
2. In the rdebug process: `malloc(size)` for the COFF, then `socket(2,2,0)`,
|
||||
`sendto` an RRQ for the COFF name, and the `recvfrom`/ACK loop from
|
||||
`tftpc.asm`. Use a receive buffer of at least 2048 bytes, separate from the
|
||||
`from`/`fromlen`/ACK cells.
|
||||
3. Append blocks at `dest + total*8` bits, not `dest + total`.
|
||||
4. Stop on a short block; `close(fd)`. No timeout is built in — the server is
|
||||
local and the payload blocks in `recvfrom`. Add `setsockopt(SO_RCVTIMEO)` or
|
||||
a retry if a lossy link matters.
|
||||
5. Hand the buffer to the loader resident at bit `01880000`
|
||||
(`dwt/loader/build/loader.bin`, 1220 B): write the descriptor at
|
||||
`01882C00` — `u16 0x4C44, u16 0x5231, u32 buf, u32 len_bytes, u32 mode=1` —
|
||||
then `move a14,-*sp,1; calla 01880000h` with `A8=0` (fixed descriptor) or
|
||||
the descriptor's bit address. The loader parses the COFF, copies
|
||||
`.text`/`.data`, zeroes `.bss` and calls `create_process` itself. The full
|
||||
contract, including the status block at `01882D00`, is in
|
||||
`dwt/notes/LOADER_CROSS.md`.
|
||||
6. Keep the COFF buffer alive; it is not reused by the fetch after the payload
|
||||
returns. The transport's code (`01870000`), argument area (`01875000`),
|
||||
receive buffer (`01876000`–`0187A000`), `from`/`fromlen`/ACK cells
|
||||
(`0187A000`–`0187A120`) and markers (`0187C000`) are all below the loader
|
||||
page, so they do not collide with `01880000`–`01883200`.
|
||||
|
||||
The fetch call itself is only `malloc`/`socket`/`sendto`/`recvfrom`/`close`;
|
||||
the firmware `tftp_open` must not be used.
|
||||
|
||||
---
|
||||
|
||||
## 6. Limits and files
|
||||
|
||||
* **Buffer:** the test allocates 64 KiB; the proof file is 600 bytes. The
|
||||
receive buffer is 2048 bytes and the server's block size is 512, so the
|
||||
largest single DATA packet is 516 bytes. A COFF over TFTP is limited only by
|
||||
the malloc block and the 16-bit block counter (32 MiB at 512-byte blocks).
|
||||
* **Timeouts:** none in the payload; `recvfrom` blocks until a packet arrives.
|
||||
The server side has 10 retries at 60 s. A lost packet hangs the `jump`.
|
||||
* **Concurrency:** the server is single-threaded. An un-ACKed transfer (the
|
||||
first `udptest` left one) blocks later RRQs until it times out; restart the
|
||||
server or use a per-transfer server.
|
||||
* **Process:** rdebug process. A `create_process` child returned `-1` from
|
||||
`socket()` in the one attempt; if the loader is a child, prove `socket()`
|
||||
first.
|
||||
|
||||
| path | contents |
|
||||
|---|---|
|
||||
| `dwt/tcpip/tftpc.asm` | the working minimal TFTP client (694 B, bit `01870000`) |
|
||||
| `dwt/tcpip/build/tftpc.bin`, `.lst`, `.map` | built payload and assembler/link records |
|
||||
| `dwt/tcpip/tfetch.asm` | firmware-`tftp_open` attempt (hangs; kept as the negative result) |
|
||||
| `dwt/tcpip/socktest.asm`, `restest.asm`, `udptest.asm` | the isolation steps: socket, resolver, sendto |
|
||||
| `dwt/tcpip/launcher.asm` | `create_process` launcher used for the child attempt |
|
||||
| `dwt/tcpip/copytest.asm` | isolated byte-copy loop |
|
||||
| `dwt/tcpip/serve_probe.py` | TFTP server for `probe.bin` + BOOTP (imports `harness/serve.py`) |
|
||||
| `dwt/tcpip/probe.bin` | 600-byte distinctive probe file |
|
||||
| `dwt/tcpip/verify.py` | rdebug peek + byte-for-byte comparison |
|
||||
| `dwt/tcpip/probe.lua` | MAME-side marker/PC sampler and buffer check |
|
||||
| `dwt/tcpip/run/server_final.log` | the final RRQ line |
|
||||
| `dwt/tcpip/run/mame_final.log` | final `TCPC` marker line (`ctotal=0x258`) |
|
||||
| `dwt/tcpip/run/final2_verify.log`, `fetched.bin` | peek comparison and the 600 fetched bytes |
|
||||
| `dwt/tcpip/run/inject_tftpc.log`, `inject_res.log`, `inject_child_sock.log` | negative results: `tftp_open`, resolver, child `socket()` |
|
||||
| `dwt/notes/LOADER_CROSS.md` | the loader/transport contract (loader at `01880000`, descriptor at `01882C00`) |
|
||||
Reference in new issue
Block a user