docs(loader): the per-process XG block must exist before XOpenDisplay #10
No files matched your search
@@ -15,6 +15,7 @@ Read `dwt/notes/LOADER.md` for the full report and
|
||||
| `loader.asm` | the loader; linked at bit `0x01880000`, raw `.bin` 1220 B |
|
||||
| `marker.asm` | acceptance app with `.text`/`.data`/`.bss` at three addresses |
|
||||
| `coff.py` | TI COFF reader/dumper (`summary`, `raw`, `manifest`) |
|
||||
| `xg.c` | app-side `vt_install_xg()`: installs the per-process Xlib context before `XOpenDisplay` (see below); not part of the loader payload |
|
||||
| `build.sh` | GSPA/GSPLNK wrapper; keeps the linked COFF |
|
||||
| `reldemo.sh`, `rel_a.asm`, `rel_b.asm` | relocation and position-dependence demo |
|
||||
| `loader.lua` | debugger-assisted harness (no network, no rdebug) |
|
||||
@@ -25,6 +26,84 @@ Read `dwt/notes/LOADER.md` for the full report and
|
||||
`work/` (the DOS tools extracted from the SDK floppy) and `run/` (private
|
||||
NVRAM/cfg) are local scratch and ignored.
|
||||
|
||||
## The per-process XG block (needed before `XOpenDisplay`)
|
||||
|
||||
A process this loader starts with `create_process` begins with **no Xlib
|
||||
context**. The ROM Xlib finds it through the current-process word:
|
||||
|
||||
```
|
||||
proc = *(u32 *)0x001B3900 ; current PCB (bit address)
|
||||
pp = *(u32 *)(proc + 0x100) ; per-process user block (bit +0x100 = byte +0x20)
|
||||
XG = *(u32 *)pp ; per-process Xlib context; 0 in a fresh child
|
||||
free_list = *(u32 *)(XG + 0xCC0) ; bits; byte +0x198 of the block
|
||||
```
|
||||
|
||||
Verified in the ROM: the current-process helper `0x3F708700` is just
|
||||
`move @0x1b3900,a8,1; rets`, and both the dequeue path inside `XNextEvent`
|
||||
(`0x3F9C1CF0`) and its enqueue helper (`0x3F9B89A0`, Xlib's `_XEnq`, called at
|
||||
`0x3F9C2180`) do `calla 0x3F708700` → `move *a8(0x100),a7` → `move *a7,a7` →
|
||||
access `*a7(0xcc0)`.
|
||||
|
||||
With `XG == 0` the whole XG structure — queue head, queue length, and the free
|
||||
list at `+0xCC0` — is read from low RAM. Nothing crashes and nothing reports an
|
||||
error: `XOpenDisplay`, `XCreateSimpleWindow`, `XMapWindow`, `XSync` and all
|
||||
drawing still work, and the window appears on screen. **Only events fail** —
|
||||
`XNextEvent` returns zeroed records (`type=0`, `keycode=0`) and never a real
|
||||
event. That is the symptom to recognise: a client that looks alive and never
|
||||
reacts.
|
||||
|
||||
Install a zeroed ≥1 KiB block before the first Xlib call:
|
||||
|
||||
```c
|
||||
extern int vt_install_xg(void);
|
||||
|
||||
if(!vt_install_xg())
|
||||
return 1; /* no plausible current process / user block */
|
||||
dpy = XOpenDisplay("0::0"); /* only after the block exists */
|
||||
```
|
||||
|
||||
`xg.c` is that helper. Build it with the app (the C toolchain path used by
|
||||
`apps/termtris`):
|
||||
|
||||
```sh
|
||||
apps/termtris/build.sh myapp.c dwt/loader/xg.c
|
||||
```
|
||||
|
||||
or copy the file into the app's tree; there is no header — declare
|
||||
`extern int vt_install_xg(void);`. The termtris port makes the same install
|
||||
(`apps/termtris/src/vt1200/xgfix.c`) with a `static vt_u32 vt_xg[256]` block
|
||||
(1 KiB); `xg.c` zeroes its block on each call rather than trusting startup to
|
||||
clear `.bss`. The helper stores its block's address at `*(u32 *)pp` — it
|
||||
overwrites XG, not the pointer cell at `proc + 0x100` — and changes RAM only,
|
||||
never the ROM.
|
||||
|
||||
An assembly client does the same two loads and one store:
|
||||
|
||||
```
|
||||
move @1B3900h, a8, 1 ; current PCB
|
||||
move *a8(100h), a7, 1 ; per-process user block (bit offset 0x100)
|
||||
movi xg_block, a9 ; a 1 KiB block, zeroed at startup
|
||||
move a9, *a7, 1 ; XG = block
|
||||
```
|
||||
|
||||
**Ordering:** before `XOpenDisplay`, every time. A draw-only window can get
|
||||
away without it; the moment the client calls `XNextEvent`, the missing block
|
||||
turns into "no events, ever".
|
||||
|
||||
Measured (2026-11-13): the unmodified termtris smoke client
|
||||
(`apps/termtris/tests/smoke.c`) linked with `dwt/loader/xg.c` and loaded by
|
||||
this harness received a real `KeyPress`:
|
||||
|
||||
```
|
||||
SMOKE: magic=534D4F4B dpy=00942290 win=00400001 n=9 type=2 keycode=194 lookup_n=1 ch=61 ks=0061 keymark=4B455950
|
||||
```
|
||||
|
||||
The same client with a stub `vt_install_xg()` that installs nothing runs its
|
||||
event loop but only sees zeroed records:
|
||||
`type=0 keycode=0 lookup_n=0 ch=00 ks=0000 keymark=00000000` after 10
|
||||
iterations. The transcript, including the control, is
|
||||
`evidence/xg_helper_run.log`.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# vt_install_xg() acceptance run — 2026-11-13
|
||||
|
||||
Host: Linux, MAME 0.289 single-driver binary
|
||||
`/home/arti/code/k-space/mame0289/vt1200`.
|
||||
Worktree: `fix/issue-2-xg-block` (docs + `dwt/loader/xg.c`).
|
||||
|
||||
The ROM is unchanged before and after the runs:
|
||||
|
||||
```
|
||||
$ md5sum vt1200_v22.bin
|
||||
b46a3e442ccb86134d2356bd70d22fd2 vt1200_v22.bin
|
||||
```
|
||||
|
||||
The client is the **unmodified** termtris smoke test
|
||||
(`apps/termtris/tests/smoke.c`), which calls `vt_install_xg()`, then
|
||||
`XOpenDisplay("0::0")`, creates and maps a window, selects input, and waits for
|
||||
a `KeyPress`. Only the helper source differs between the two runs: the first
|
||||
links `dwt/loader/xg.c`, the second a stub that installs nothing.
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
$ apps/termtris/build.sh apps/termtris/tests/smoke.c dwt/loader/xg.c
|
||||
```
|
||||
|
||||
Link map excerpt (`apps/termtris/build/smoke.map`):
|
||||
|
||||
```
|
||||
.text 0 018a0000 00000da0
|
||||
018a0b30 00000270 XG.obj (.text)
|
||||
.bss 0 01a00000 0000a700 UNINITIALIZED
|
||||
01a08700 00002000 XG.obj (.bss)
|
||||
```
|
||||
|
||||
`MARK` is the first `_mark` symbol in the map, `0x01a08000`.
|
||||
|
||||
## Run — helper installed
|
||||
|
||||
```
|
||||
$ HOME=/tmp/opencode/vt1200-task2/home VT1200_SKIP_LEVEL1=1 \
|
||||
SMOKE_MARK=0x01a08000 LOADER_AT=2500 LOADER_END=3400 \
|
||||
LOADER_BIN=$PWD/dwt/loader/build/loader.bin \
|
||||
LOADER_COFF=$PWD/apps/termtris/build/smoke.coff \
|
||||
LOADER_OUT=/tmp/opencode/vt1200-task2 \
|
||||
/home/arti/code/k-space/mame0289/vt1200 vt1200 \
|
||||
-rompath harness/mame/roms -video none -sound none -nothrottle \
|
||||
-skip_gameinfo -cfg_directory /tmp/opencode/vt1200-task2/cfg \
|
||||
-nvram_directory /tmp/opencode/vt1200-task2/nvram \
|
||||
-debug -debugger none -autoboot_script apps/termtris/tests/smoke.lua \
|
||||
-autoboot_delay 0
|
||||
```
|
||||
|
||||
Relevant output:
|
||||
|
||||
```
|
||||
LOADER: result status=0 (OK) entry=018A0000 text=018A0000/3488 bits data=018A0DA0/16 bits bss=01A00000/42752 bits relocs=0 pcb=003FEE50 nscns=4 len=1349
|
||||
LOADER: placed .text at 018A0000: 436 bytes, 0 differing
|
||||
LOADER: placed .cinit at 018A0DA0: 2 bytes, 0 differing
|
||||
SMOKE: magic=534D4F4B dpy=00942290 win=00400001 n=9 type=2 keycode=194 lookup_n=1 ch=61 ks=0061 keymark=4B455950
|
||||
LOADER: PC at inspection = 3F579870
|
||||
```
|
||||
|
||||
`type=2` is `KeyPress`, `keycode=194` is the LK401 `A`, and `keymark=4B455950`
|
||||
("KEYP") is the client's own marker: the real event reached the client.
|
||||
|
||||
## Control — helper replaced by a stub
|
||||
|
||||
The stub (`/tmp/opencode/vt1200-task2/xgstub.c`) is:
|
||||
|
||||
```c
|
||||
int
|
||||
vt_install_xg(void)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
```
|
||||
|
||||
Built as `OUTNAME=smokectl apps/termtris/build.sh apps/termtris/tests/smoke.c
|
||||
/tmp/opencode/vt1200-task2/xgstub.c`, then run with the same command and private
|
||||
directories (`...-ctl-home`, `...-ctl-cfg`, `...-ctl-nvram`) and
|
||||
`LOADER_COFF=apps/termtris/build/smokectl.coff`:
|
||||
|
||||
```
|
||||
LOADER: result status=0 (OK) entry=018A0000 text=018A0000/2896 bits data=018A0B50/16 bits bss=01A00000/34560 bits relocs=0 pcb=003FEE50 nscns=4 len=1275
|
||||
SMOKE: magic=534D4F4B dpy=00942290 win=00400001 n=10 type=0 keycode=0 lookup_n=0 ch=00 ks=0000 keymark=00000000
|
||||
LOADER: PC at inspection = 3F579870
|
||||
```
|
||||
|
||||
The display opens (`dpy=00942290`) and the window is created and mapped
|
||||
(`win=00400001`), exactly as in the passing run. The event loop runs 10
|
||||
iterations, but every record is `type=0 keycode=0` and `keymark` stays zero: no
|
||||
real event ever arrives. This is the symptom the helper exists to prevent.
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* xg.c -- install a per-process Xlib context before XOpenDisplay.
|
||||
*
|
||||
* A process created by `create_process` inherits a per-process user block: the
|
||||
* current PCB is *(u32 *)0x001B3900 and its cell at bit +0x100 points at that
|
||||
* block. The first word of the block is the per-process Xlib context (XG),
|
||||
* and it is zero in a fresh child. The ROM Xlib's event code (XNextEvent,
|
||||
* 0x3F9C1CF0, and its enqueue helper, 0x3F9B89A0) does
|
||||
*
|
||||
* XG = *(*(proc + 0x100)); ; three loads, see xg_install below
|
||||
* free_list = *(XG + 0xCC0); ; bits = byte 0x198 of the block
|
||||
*
|
||||
* so a child with XG == 0 treats low RAM as its event queue: windows, mapping
|
||||
* and drawing still work, but XNextEvent never delivers an event. This
|
||||
* helper gives the child a zeroed 1 KiB block. It is a RAM write only; the
|
||||
* ROM is untouched.
|
||||
*
|
||||
* Addresses are TMS34010 bit addresses, as everywhere on this CPU: +0x100 is
|
||||
* 32 bytes into the PCB and the free-list field is at byte 0x198 of the block.
|
||||
*
|
||||
* This is the same code as apps/termtris/src/vt1200/xgfix.c. Build it with
|
||||
* the C client (apps/termtris/build.sh <main>.c dwt/loader/xg.c) or copy it
|
||||
* into the app's source tree; declare `extern int vt_install_xg(void);` and
|
||||
* call it once, before XOpenDisplay.
|
||||
*/
|
||||
typedef unsigned long vt_u32;
|
||||
|
||||
static vt_u32 vt_xg[256]; /* 1 KiB: covers the 0x198-byte free-list field */
|
||||
|
||||
int
|
||||
vt_install_xg(void)
|
||||
{
|
||||
vt_u32 proc, pp;
|
||||
int i;
|
||||
|
||||
proc = *(volatile vt_u32 *)0x001B3900UL;
|
||||
if(proc < 0x100UL || proc > 0x01FFFFFFUL)
|
||||
return 0;
|
||||
pp = *(volatile vt_u32 *)(proc + 0x100UL);
|
||||
if(pp < 0x100UL || pp > 0x01FFFFFFUL)
|
||||
return 0;
|
||||
|
||||
for(i = 0; i < 256; i++)
|
||||
vt_xg[i] = 0;
|
||||
*(volatile vt_u32 *)pp = (vt_u32)vt_xg;
|
||||
return 1;
|
||||
}
|
||||
+87
-8
@@ -44,7 +44,7 @@ WM `@0x27AA80`).
|
||||
| what | Ghidra | bits | notes |
|
||||
|---|---|---|---|
|
||||
| `create_process(entry,name,stack,1,arg)` | `0x07EE0D48` | `0x3F706A40` | mallocs a 0x39-byte PCB, copies the name, allocates the stack, links it at `@0x1B3860` |
|
||||
| current-process pointer | — | `0x1B3900` | `+0x100` = user area (fd table) |
|
||||
| current-process pointer | — | `0x1B3900` | `+0x100` = user area (fd table); its first word is the per-process Xlib context (§10.5) |
|
||||
| process list head | — | `0x1B3860` | |
|
||||
| `Main Program` (creates SERVER + SESSIONMGR) | `0x07E69B6A`…`0x07E69C1A` | `0x3F34DB50` | itself a created process (name `Main Program`) |
|
||||
| X server entry **SERVER** | `0x07E6C522` | `0x3F362910` | dix main; prints `Hello, this is the X server` |
|
||||
@@ -186,8 +186,12 @@ MOVE A11,*A14+,1 ; display
|
||||
CALLA 0x07FA8372 ; -> window id
|
||||
```
|
||||
|
||||
The full minimum is **4 Xlib calls**:
|
||||
The full minimum is **4 Xlib calls after one setup step**:
|
||||
|
||||
0. **Install the per-process Xlib context first** — see §10.5. It is not an
|
||||
Xlib call: `pp = *(u32 *)(*(u32 *)0x1B3900 + 0x100); *(u32 *)pp = block;`
|
||||
with `block` a zeroed ≥1 KiB buffer. Skip it and window creation and
|
||||
drawing still work, but `XNextEvent` silently never delivers a real event.
|
||||
1. `XOpenDisplay("0::0")` — `0x3F9C2570`
|
||||
2. `XCreateSimpleWindow(d, root, x, y, w, h, bw, border, background)` —
|
||||
`0x3FD41B90`
|
||||
@@ -207,9 +211,10 @@ To draw text add `XLoadFont(d,"fixed")` (`0x3FD4AE80`), `XCreateGC`
|
||||
`XDrawString` (`0x3FDA0000`). No event loop is needed for a static window; the
|
||||
server keeps it after the client goes idle.
|
||||
|
||||
Globals: only the `Display *` is per-app. There is **no global "current
|
||||
display" to initialise**; `XOpenDisplay` allocates and returns it. Window ids,
|
||||
GCs and fonts are server-side resources and are valid for any client.
|
||||
Globals: the `Display *` and the per-process XG block (§10.5) are per-app.
|
||||
There is **no global "current display" to initialise**; `XOpenDisplay`
|
||||
allocates and returns it. Window ids, GCs and fonts are server-side resources
|
||||
and are valid for any client.
|
||||
|
||||
## 5. The injection experiment (run) and the verdict
|
||||
|
||||
@@ -447,7 +452,81 @@ Raw excerpt: `dwt/gui/evidence/transport_evidence.txt`; full trace:
|
||||
`dwt/gui/run12/debug.log` (instrumented) and `dwt/gui/run13/mame.log`
|
||||
(breakpoint-free).
|
||||
|
||||
### 10.5 The second bug: do not reload `Display *` from the marker cell
|
||||
### 10.5 The per-process XG block: events need it before `XOpenDisplay`
|
||||
|
||||
A child made by `create_process` starts with **no per-process Xlib context**.
|
||||
The ROM Xlib reaches it through the current-process word and two cells:
|
||||
|
||||
```
|
||||
proc = *(u32 *)0x001B3900 ; current PCB; bit 0x1B3900
|
||||
pp = *(u32 *)(proc + 0x100) ; per-process user block (bit +0x100 = byte +0x20)
|
||||
XG = *(u32 *)pp ; per-process Xlib context; 0 in a fresh child
|
||||
```
|
||||
|
||||
This is confirmed by disassembly, not by the field names. `0x3F708700` is the
|
||||
ROM's "current process" helper, and the event code calls it before each use:
|
||||
|
||||
```
|
||||
3f708700: move @0x1b3900, a8, 1 ; a8 = current PCB
|
||||
3f708730: rets
|
||||
...
|
||||
3f9c23c0: calla 0x3f708700
|
||||
3f9c23f0: move *a8(0x100), a7, 1 ; a7 = pp
|
||||
3f9c2410: move *a7, a7, 1 ; a7 = XG
|
||||
3f9c2420: move *a7(0xcc0), *a11(0), 1 ; free-list head = *(XG + 0xCC0 bits)
|
||||
3f9c24b0: move a11, *a7(0xcc0), 1 ; used node goes back on the free list
|
||||
```
|
||||
|
||||
That block is inside `XNextEvent` (`0x3F9C1CF0`, which returns at
|
||||
`0x3F9C2560`). The enqueue helper it calls at `0x3F9C2180` (`0x3F9B89A0`;
|
||||
Xlib's `_XEnq`) repeats the same three loads and takes a node from
|
||||
`*(XG + 0xCC0)`:
|
||||
|
||||
```
|
||||
3f9b8a80: calla 0x3f708700
|
||||
3f9b8ab0: move *a8(0x100), a7, 1
|
||||
3f9b8ad0: move *a7, a7, 1
|
||||
3f9b8ae0: move *a7(0xcc0), a11, 1
|
||||
3f9b8b70: move *a11(0), *a7(0xcc0), 1
|
||||
```
|
||||
|
||||
The ROM is **bit-addressed**: `XG + 0xCC0` bits is byte `+0x198` of the block,
|
||||
which is where the low-level note `XG + 0x198` comes from.
|
||||
|
||||
In a fresh child `XG` is 0, so the whole XG structure — queue head, queue
|
||||
length, and the free list at `+0xCC0` — is read from low RAM. Whatever happens
|
||||
to be there becomes the event queue. The result is not a crash: `XOpenDisplay`,
|
||||
`XCreateSimpleWindow`, `XMapWindow`, `XSync` and every drawing call still work,
|
||||
because they only build requests — the window really appears. **Events silently
|
||||
never arrive**: `XNextEvent` returns zeroed records (`type=0`, `keycode=0`) and
|
||||
never a real `KeyPress`, and nothing reports an error. It reads like a keyboard
|
||||
or focus problem.
|
||||
|
||||
The fix is a RAM write before the first Xlib call:
|
||||
|
||||
1. Allocate a **zeroed block of at least 1 KiB** (the termtris port uses
|
||||
`static vt_u32 vt_xg[256]`, exactly 1 KiB; the `+0x198` field and the queue
|
||||
nodes live inside it).
|
||||
2. Read `proc = *(u32 *)0x001B3900` and `pp = *(u32 *)(proc + 0x100)`.
|
||||
3. Store the block's address into `*(u32 *)pp`.
|
||||
|
||||
`dwt/loader/xg.c` implements this as `int vt_install_xg(void)` (returns 1 on
|
||||
success, 0 if `proc` or `pp` is not a plausible RAM pointer); it zeroes the
|
||||
block itself rather than trusting startup to clear `.bss`. The termtris port
|
||||
uses the same three loads and store (`apps/termtris/src/vt1200/xgfix.c`) with
|
||||
the same 1 KiB block. Both are RAM-only: the ROM is untouched. The ordering
|
||||
rule is **install it before `XOpenDisplay`**. The ROM re-reads the cell on each
|
||||
event call, but installing first means no Xlib entry point can ever run with
|
||||
`XG == 0`, and that is the order the working client uses.
|
||||
|
||||
Measured: with the block installed, a `create_process` child receives real
|
||||
events — the termtris smoke test gets `type=2 keycode=194` and `XLookupString`
|
||||
returns `a` (`apps/termtris/VT1200.md`, and the loader-tree helper's own run in
|
||||
`dwt/loader/README.md`). With the install removed, the same client opens and
|
||||
maps its window, loops in `XNextEvent` and only ever sees zeroed records
|
||||
(`type=0`, `keycode=0`).
|
||||
|
||||
### 10.6 The second bug: do not reload `Display *` from the marker cell
|
||||
|
||||
With H3 alone the child still crashed in `XCreateSimpleWindow`. The cause is in
|
||||
`xhello`, not in the transport: `XOpenDisplay` really returns `0x7C7290`
|
||||
@@ -461,7 +540,7 @@ window in **a10** — both are callee-saved by every Xlib entry point
|
||||
re-run `setf 16,0,0; setf 32,0,1` after every Xlib call because Xlib changes the
|
||||
CPU's field state. The markers are diagnostic only now.
|
||||
|
||||
### 10.6 Reproduce
|
||||
### 10.7 Reproduce
|
||||
|
||||
```sh
|
||||
./dwt/gui/build.sh dwt/gui/xhello.asm 01840000 # 390 B
|
||||
@@ -486,7 +565,7 @@ python3 dwt/visual/inject.py --host 192.168.99.2 \
|
||||
`probe_win.lua` contains no `bpset`/`wpset`; the final frame is produced by the
|
||||
payload alone. Stage markers: `01890140` reaches 6, `01890160` = 4.
|
||||
|
||||
### 10.7 Limits
|
||||
### 10.8 Limits
|
||||
|
||||
* The demonstration depends on the `VT1200_SKIP_LEVEL1=1` divergence (as did the
|
||||
original experiment); it is evidence about the OS/X server, not a real POST.
|
||||
|
||||
Reference in new issue
Block a user