# 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 `) 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 ```sh # 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 |