# harness/c-hello — the first C program on the VT1200 A C hello world built with the **1987 TI TMS34010 SDK** (K&R compiler, `GSPCC`/`GSPCG` 2.01, `GSPA`/`GSPLNK` 2.00, `RTS.LIB`) and run on **real hardware** — not a debugger poke of assembly, and no X11: it writes the word `HELLO` straight into video RAM. It is also the project's smallest end-to-end proof of the C path: ``` gspcpp -> gspcc -> gspcg -> gspa -> gsplnk (DOSBox, 1987 tools) -> hello.coff (GSPLNK COFF, sections at bit addresses) -> dwt/loader/hostload.py (rdebug -> malloc -> poke COFF -> COFF loader) -> loader.asm (free-list check, copy .text, zero .bss, create_process) -> _start (entry.asm) -> main -> VRAM at bit 0x04000000 ``` ## Files | file | what it is | |---|---| | `hello.c` | the payload: 5x7 font, 8x-scaled `HELLO` in a black 256x72 box, markers at `0x018A8000` | | `entry.asm` | the process entry: sets `STK`/`FP`, `calla _main`, `rets` (replaces `boot.obj`) | | `build.sh` | DOSBox wrapper; `build.sh` = real unit (256-byte row pitch), `build.sh emu` = MAME (128) | | `build/hello.coff` | linked image for the real unit, entry `_start` | | `build/hello-emu.coff` | same source with `ROW_BYTES 128`, for MAME's canvas | | `build/hello.map`, `hello.lst`, `entry.lst`, `hello.s` | link map and listings | | `evidence/hello_frame.png` | real unit, **pre-fix**: the pointer-arithmetic bug, one-byte bars spread over a whole row | | `evidence/hello_real_frame.png` | real unit, working: the current box at x=40 and, to its right, the previous run's box at x=320 (nothing erases VRAM between runs) | | `work/` | extracted SDK + DOS scratch (git-ignored) | ## Build ```sh ./harness/c-hello/build.sh # real unit: .text 0x018A0000, .bss 0x01A00000 ./harness/c-hello/build.sh emu # MAME: same addresses, 128-byte row pitch ``` Both land inside the firmware's large free block (byte `0x0C03AA..0x400000` on a fresh boot). The link map must show `.cinit` empty and entry `_start`: ``` ENTRY POINT SYMBOL: "_start" address: 018A17E0 .text 018A0000 .cinit 018A1870 (size 0) .bss 01A00000 ``` ## Run Real unit (rdebug on TCP 771; the terminal needs the OS up): ```sh python3 dwt/heapmap.py --host 100.103.100.103 --at 0x018A0000 # read-only preflight python3 dwt/heapmap.py --host 100.103.100.103 --at 0x01A00000 python3 dwt/loader/hostload.py --host 100.103.100.103 \ --coff harness/c-hello/build/hello.coff \ --out harness/c-hello/evidence --real --settle 3 python3 dwt/solve.py --host 100.103.100.103 --peek 0x018A8000 --len 8 # HELL DONE python3 dwt/screenshot.py --host 100.103.100.103 --real --out /tmp/hello.png ``` MAME (debugger-assisted, no network): ```sh VT1200_SKIP_LEVEL1=1 LOADER_AT=2500 LOADER_END=2800 LOADER_SNAP=1 \ LOADER_COFF=$PWD/harness/c-hello/build/hello-emu.coff LOADER_OUT=/tmp/chello \ /home/arti/code/k-space/mame0289/vt1200 vt1200 \ -rompath harness/mame/roms -video none -sound none -nothrottle -skip_gameinfo \ -debug -debugger none -autoboot_script dwt/loader/loader.lua -autoboot_delay 0 ``` Success = loader status 0, `entry=0x018A17E0`, markers `48454C4C`/`444F4E45` (`HELL`/`DONE`), and the box on screen. On the real unit the box is at **x=40, y=800**, 256x72 pixels. ## Three traps this image exists to record 1. **`boot.obj`'s `exit()` is `for(;;)` and `_c_int00` ends in `reti`.** That is a standalone-ROM runtime, not a process. Linked with `-c`, the child finishes `main`, spins forever (the linked `jruc self` at `0x018A1520`), and because the VT1200 OS is cooperative it starves the whole machine; on real hardware the terminal fell over with an illegal opcode. `-cr` is no better — it still pulls in `boot.obj` and still defines `_c_int00`. Hence `entry.asm`: no `boot.obj`, entry `_start`, plain `rets` back to the `create_process` trampoline (the same return `xhello` uses). `SP` (the OS's interrupt stack) is left alone; only `STK`/`FP` are set. 2. **Pointers are bit addresses and pointer arithmetic scales by object size.** A `char *` step is 8 bits, so `p + px*8` advances 8 *bytes*, not one. Address bytes as `addr + px*8` bits and cast. The pre-fix frame (`evidence/hello_frame.png`) is that bug: 32 one-byte bars spread over a whole row. 3. **No `.cinit`.** With `entry.asm` the runtime's autoinit pass never runs, so the image must not need static initializers. The font and order tables are filled in `build_font()` at runtime. ## Geometry is not portable between the real unit and MAME The two framebuffers differ, and the payload carries the difference as `ROW_BYTES`: | | real unit (measured) | MAME | |---|---|---| | base | bit `0x04000000` | bit `0x04000000` | | row pitch | **256 bytes** (`ROW_BYTES 256`) | 128 bytes (`VMEM_PITCH_BITS`) | | pixels per row | 1280 (160 bytes) + 96 B `0x55` mask | 1024 | | screen | 1280x1024 | 1024x864 snapshot in MAME's 1280x901 | The real pitch is measured, not assumed: a payload writing at `row*2048` bits lands on scanline `row` and a 256-pixel box at pixel 40 appears at x=40 — and the pre-fix frame's bars, written 8 bytes apart from byte 40, run to the end of a 1280-pixel line. Under a 128-byte pitch the same writes would wrap into the next memory row. `dwt/screenshot.py --real` (1280 wide, stride 256, LSB-first) also decodes live VRAM correctly, which pins the bit order. Do not use a MAME frame as proof of pixel placement: same code, different framebuffer.