# 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`.