Files
vt1200/dwt/notes/LOADER_CROSS.md
T
SDD 01923ecb69 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.
2026-10-18 15:24:34 +03:00

7.1 KiB
Raw Blame History

LOADER_CROSS — the boundary between the TFTP transport and the COFF loader

Append-only. Each entry starts with a timestamp header.


2026-10-18 — COFF loader interface (loader half complete)

The loader half is implemented and tested with debugger-poked images. This entry is the contract the transport half must satisfy; it is written by the loader owner for the transport owner.

What the transport hands over

  1. The complete linked COFF image in RAM. Not the .obj; the GSPLNK -a output (*.out), byte-for-byte, 510–774 bytes for the current test apps. The transport owns the receive buffer and passes its bit address and byte length. The loader does not allocate or receive the image itself.

  2. The loader resident at bit 0x01880000. The raw dwt/loader/build/loader.bin (1220 bytes, .text size 0x2620 bits) is position-dependent; it must be placed exactly there. A transport that includes the loader in its own payload must copy it to that address first (and must not overwrite it).

  3. A descriptor at bit 0x01882C00 (or at an address passed in A8). Offsets are bytes; in the loader's assembly they are bits (+0, +0x10, +0x20, +0x40, +0x60):

    DESC + 0x00  u16  0x4C44   "LD"    ) both magic words 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
    

    mode = 1 is the transport mode. mode = 0 is the debugger mode: the loader restores the interrupted context and jumps to the PC the harness saved; a transport must not use it.

  4. A call, using the firmware convention the loader itself uses for create_process:

    move  a14, -*sp, 1
    movi  DESC_ADDR, a14, L       ; no descriptor args to push, but keep A14
    calla 01880000h               ; A8 = 0 (fixed descriptor) or DESC_ADDR
    

    A8 may be 0 (use the fixed descriptor at 0x01882C00) or the descriptor's bit address. Any other value falls back to the fixed descriptor if it does not carry the magic.

What the loader returns

  • A8 = RESULT_STATUS: 0 = success, otherwise an error code (table below).

  • The result block at bit 0x01882D00 (32-bit cells at +0x20 intervals):

    offset meaning
    +0x000 status (same value as A8)
    +0x020 entry passed to create_process (bits)
    +0x040 .text paddr / +0x060 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 0x01880000
  • On success the app is already a first-class process (a PCB exists and the child is scheduled when the caller yields). The loader does not wait for the app and does not return its exit status.

Address policy: the loader picks, the transport does not

The transport passes buffer and length only. The load address of every section comes from the COFF section headers (bit addresses); the entry comes from the optional header (entry, else textstart). There is no "load-address" argument. The linked image is position-dependent (GSPLNK resolves all references and emits nreloc = 0), so the transport must not move the sections after the fact and must not send a .obj.

The loader checks, before copying:

  • section size is a whole number of 16-bit words;
  • the section bytes are inside [buf, buf+len);
  • 0x100 <= paddr and paddr + size <= 0x2000000 bits (4 MiB RAM);
  • no overlap with [0x01880000,0x01883200) (loader page) or the COFF buffer;
  • the range lies inside one block of the global free list (head bit 0x1E8AC0; block {u32 size_bits; …; u32 next@+0x20}, user at +0x40).

The free-list check is a check, not a reservation. The transport should choose app link addresses away from the allocator's growth path (the large free block starts at byte 0x12191A and grows upward; the current apps live at byte 0x318000+). The transport's own code, stack and receive buffer must also stay out of the target sections; the loader cannot see them.

Status codes

code meaning
0 success, child created
1 bad descriptor (magic)
2 truncated COFF / section headers past len
3 bad COFF magic (not 0x0090)
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 (unsupported; the input is probably a .obj)
8 target not inside a global free-list block
9 no entry point
10 create_process returned 0

Currently proven

  • Debugger-poked marker.coff (buf=0x01900000, len=510, mode 0 and mode 1, A8=0 and A8=DESC): status 0, PCB 0x003FFBD0, markers start=018C0000 data=DEADBEEF bss=0, counter live.
  • Debugger-poked xhello.coff (len=774, mode 0): status 0, entry 0x01840000, stage=6, window visible in loader_frame2650.png.
  • rela.obj (len=342): status 7, relocs=2, nothing copied.

Files the transport needs

  • dwt/loader/build/loader.bin — raw loader to place at 0x01880000.
  • dwt/loader/coff.py — the host-side reference parser; the loader's field 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.