Files
vt1200/dwt/notes/LOADER.md
T
SDD 8588340527 dwt/loader: end-to-end host loader over rdebug
hostload.py drives one rdebug connection: jump(malloc) for the COFF buffer,
poke the COFF, poke the loader at 0x01880000, write the descriptor, call the
loader, and verify status 0 plus app evidence (marker cells or xhello stage 6
and a VRAM screenshot). --tftp uses the existing tftpc.bin fetch instead of a
host poke. Adds evidence for two/three fresh boots and appends the integrate
section to dwt/notes/LOADER.md, including the bit-offset result-block layout
and the create_process clobber of the descriptor.
2026-10-18 15:34:25 +03:00

574 lines
25 KiB
Markdown

# 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 <orig_pc>`) 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 |
---
## 10. End-to-end host-side loader (rdebug): malloc -> poke -> load -> run
Date: 2026-10-18. Driver: `dwt/loader/hostload.py`. Evidence:
`dwt/loader/evidence/hostload/` (its `README.md` indexes the logs and PNGs).
This is the integration of the two existing halves; the loader and the apps are
unchanged.
### Exact sequence (one TCP connection to `192.168.99.2:771`)
1. `opcode 7` liveness; the computed challenge is answered by
`dwt/challenge.py` (the `RDebug` default).
2. `jump 0x3F701230` (`malloc`) with **one** argument, the COFF byte length.
`jump` returns the callee's `A8`, i.e. a **bit** pointer.
3. Poke the COFF to that bit pointer in 1024-byte chunks
(`bit + off*8`, never `bit + off`) and `peek` it back: 510/510 or 774/774
bytes equal, sha256 matches the local file.
4. Poke the raw loader (`build/loader.bin`, 1220 B) at bit `0x01880000`, then
`peek` it back (610 words, 0 differing). `--no-poke-loader` skips the write
and requires that readback to match.
5. Build the 16-byte descriptor at bit `0x01882C00` from **bit** offsets
+0/+0x10/+0x20/+0x40/+0x60 (bytes 0/2/4/8/12):
`44 4c 31 52 <buf LE32> <len LE32> 01 00 00 00`.
6. `jump 0x01880000` with **zero** arguments. The rdebug jump handler's
zero-arg path is a bare `CALL` after `MOVE A9,A8` with `A9 = 0`, so the
loader enters with `A8 = 0` and uses the fixed descriptor. (With N args the
stubs do not set `A8` to the descriptor either; `A8 = 0` is the reliable
calling convention from rdebug.)
7. Read the result block at bit `0x01882D00`, then app evidence: marker cells
by `peek`, or xhello `stage`/`strlen` plus a VRAM screenshot.
### What worked (two or three fresh boots each)
* `jump(malloc)` returns a usable, byte-aligned bit pointer in the firmware
heap: `0x007c7290` (byte `0x0F8E52`), `0x007c8030` (byte `0x0F9006`),
`0x007df450` (byte `0x0FBE8A`), `0x00adcd90`, `0x009fccd0`. These are far
below the loader page (`0x01880000`, byte `0x310000`) and below the current
apps (`.text` byte `0x318000`), and `hostload.py` rejects a pointer that
overlaps either, the loader page or the marker cells.
* The loader accepts a malloc'd buffer: status `0` and `A8 = 0` on every run.
Its free-list check (status 8) passed for `.text 0x018C0000` and for
`xhello` `.text 0x01840000`; the result block reports entry/text/data/bss,
`pcb != 0`, `nscns=3`, and `loader_base=0x01880000`.
* `create_process` really runs the app: `marker.coff` leaves
`start=0x018C0000`, `data=0xDEADBEEF`, `data2=0x12345678`, `bss=0`, and a
live counter; `xhello.coff` reaches `stage=6` (`XSync`) and its black
400x200 window is visible at `(100,100)` in
`hostload_xhello_boot{3,4}_xhello.png`.
* `--tftp` also works: the existing `dwt/tcpip/tftpc.bin` (bit `0x01870000`)
fetches into its own `malloc` buffer (`stage=4`, `total=510`,
`done=0x05A5A5A5`); `peek` matches the COFF byte-for-byte, and that buffer is
fed to the same descriptor/loader. Server RRQ, boot 3 and 4:
`... requested 'dwt/tcpip/probe.bin' (510 bytes)`.
### Two traps found
1. **Result-block offsets are bits.** `loader.asm`'s `RESULT_BASE+020h` is
+4 **bytes**, so the block is 13 adjacent 32-bit cells (52 B), not cells at
byte intervals of 0x20. Reading at byte offsets `0x20, 0x40, ...` returns
`0x55555555` from the free heap beyond the real block; that was a host
parsing bug, not a loader failure.
2. **The descriptor is clobbered after a load.** A successful
`create_process` leaves a call frame at `0x01882C00`
(`0d 00 00 00 <pcb> <ptr> 00 60 00 00`); `ARG_AREA` is `0x01882B00` and the
frame is 0x100 bits wide. The loader has already consumed the descriptor,
and the result block (`0x01882D00`) is untouched, but a second call must
rewrite the descriptor. `hostload.py` does so immediately before every
jump.
### Limits
* **No reservation.** `malloc` only finds a buffer; the loader's free-list
check is still a check. The apps stay linked at byte `0x318000`/`0x308000`
while allocations grow from the low free blocks; enough allocations on one
boot would eventually split the big block up to them. Fresh-boot runs are
safe; on a long-lived unit the caller must still choose a free link address.
* **`--tftp` needs a server that serves the COFF under tftpc's fixed name**
(`dwt/tcpip/probe.bin`); `tftpc.asm` has no filename argument. The server
used here was
`serve_probe.py 192.168.99.1 --bootp --file dwt/loader/build/marker.coff`.
* **`--frame N` cannot trigger a MAME dump.** It only waits for
`vt1200-frame-N.ppm` from an externally configured
`VT1200_DUMP_FRAME(S)`; frame numbers are boot-relative, so the load-relative
visual evidence is the rdebug VRAM screenshot.
* The loader is still position-dependent and fixed at `0x01880000`, and the
loaded images are position-dependent; none of this adds relocation.
* rdebug process only: `create_process` is used for the *app*, not for the
loader or the TFTP client. A `create_process` child previously returned `-1`
from `socket()` (TCPIP.md), so the fetch stays in the rdebug process.
* The real unit would need TCP 771 reachable and the same debug-gate state;
the emulator run uses the documented `VT1200_SKIP_LEVEL1=1` divergence to
reach the OS.