20 KiB
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):
-
Two objects with cross references (
rel_a.asmcallsext_funcand loadsext_data;rel_b.asmdefines 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 -
Linking those objects with
-aproduces an output whose sections all havenreloc=0:sec 1 .text paddr=00018c0000 ... nreloc=0 sec 2 .data paddr=00018d0000 ... nreloc=0 -
Linking the same objects at two origin sets (
018C0000/018D0000/018D8000and018E0000/018F0000/018F8000) produces different bytes — the absoluteext_data/ext_funcoperands:== .text: 22 vs 22 bytes differing bytes: 2 0x0004: 8d -> 8f 0x0010: 8c -> 8eThe marker app behaves the same way: with
.textlinked at018C0000it contains the immediate0x018C0000; at018E0000the same byte changes to0x018E0000.movi _startandcallaare absolute. -
Linking without
-astill yieldsnreloc=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
-
Save
A14andSP, set 16/32-bit field sizes, read the descriptor. -
Read the file header: magic
0x0090,nscns,opthdr; check20 + opthdr + nscns*40 <= len. -
Read the optional header:
entry, elsetextstart. -
For each 40-byte section header:
- record the first
.text/.data/.bssaddress and size from flags; - refuse if
nreloc != 0; - require
size % 16 == 0; - check
scnptr + size/8 <= lenwhenscnptr != 0; - check
0x100 <= paddrandpaddr + 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, copysize/16words frombuf + scnptr*8topaddr; - else store
size/16zero words atpaddr(this is.bssand also any uninitialized named section).
- record the first
-
Call
create_process(entry, "LOADER", 0x6000, 1, 0)with the firmware's upward-argument convention (the same shape asdwt/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 -
Store
A8(the PCB) and the status. Ifmode == 1, restoreSP/A14, return the status inA8andrets. Ifmode == 0, restore the interrupted state andjumpto 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=018C0000is the app reading its own_startlabel: it ran at the linked address.data=DEADBEEF,data2=12345678:.datawas copied.bss=00000000after anA5A5pre-fill:.bsswas zeroed by the loader.countgrows every sample: the child is live, not a one-shot jump.pcb=003FFBD0:create_processreturned 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(rawdwt/loader/build/loader.bin); - a descriptor at
0x01882C00withbuf,len,mode=1; - a normal
calla 0x01880000(orA8 = 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
mallocfrom 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 byte0x12191Aupward; the apps are at byte0x318000+). - 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_processgets a name string"LOADER", a0x6000-byte stack andarg = 0; the loader has no argv or environment channel.create_processreturns 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.bssis zeroed, but the loader does not run the.cinittable; a C app must still enter through_c_int00and do its own autoinit as the SDK runtime expects. The loader'sentryis whatever the COFF says.
8. Reproduce
# 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 |