# Ch265 closeout — helper is ALSO a one-call thunk (to 0xBFC4F320); recurse once more **Status:** Closed. New opt-in target `tb_ee_core_bios_long_helper_autopsy` runs the BIOS-long flow with the Ch264 observer pattern re-aimed at the helper body `0xBFC4D370..0xBFC4D470`, plus two new tracks: (1) per-invocation `$a0_in`/`$v0_post`/`$v1_post` snapshots on entry-and-return, (2) JAL/J/JR/JALR retire log inside the helper with statically- decoded targets and "LEAVES helper" annotations. **Literal verdict the task emits:** `helper_static_ram_gate_found (EA=0x801FFDE4 returns identical 0xBFC52998 across 8 hits — region=EE_RAM)`. **Structural verdict (visible in the stream + CF table):** **`helper_is_thunk` — the helper is another one-call thunk, this time to `0xBFC4F320`.** The literal label is a known false-positive (see "Verdict-label nuance" below); the real polled gate is still one frame deeper. ## Codex Ch265 acceptance — line-by-line | Codex requirement | Status | Where | |-------------------------------------------------------------------------|--------|--------------------------------------------------| | Reuse Ch264 observer one frame deeper at 0xBFC4D370..0xBFC4D470 | ✅ | `CH265_HELPER_LO/HI` = `0xBFC4D370/D470` | | Same region tagging and compact tables | ✅ | `ch265_region_name` task; same shape as Ch264 | | Capture non-fetch data reads only | ✅ | Same `!ch265_is_fetch` predicate as Ch264 | | Include calls/jumps out of the helper | ✅ | `HELPER_CONTROL_FLOW` table — J/JAL/JR/JALR retires inside helper, with statically-decoded J/JAL target and "LEAVES helper" notes | | Track $a0=0x0F at entry and returned $v0 | ✅ | `HELPER_PASSES` table with `$a0_in`/`$ra_in`/`$v0_post`/`$v1_post` | | Compare pass 0 versus steady-state passes 1–8 | ✅ | `pass=N` column in every table; trivial visual diff | | Verdicts mirror Ch264 + helper_is_thunk | ✅ | 5-way verdict logic | | No new side-effect stubs | ✅ | TB-only addition; no RTL touched | | Regression unaffected | ✅ | 157 / 157 with target off-by-default | ## What the autopsy showed ### HELPER_PASSES (per-invocation entry/exit register snapshots) The helper is called from many places, not just from the Ch264 callee. The first 7 invocations are pre-treadmill BIOS init with varying `$a0_in` (0xF, 0xE, 0x1, 0x4, 0x5, 0x6, 0x7). The treadmill itself (cycles 10.2M onward) shows a **deterministic pair every Ch217 pass**: ``` [7] cyc=10194426 $a0_in=0x0F $ra_in=0xBFC52998 $v0_post=0xA000A8C8 $v1_post=0x00000008 [8] cyc=10194505 $a0_in=0x07 $ra_in=0xBFC52368 $v0_post=??? $v1_post=??? [9] cyc=20095076 $a0_in=0x0F $ra_in=0xBFC52998 $v0_post=0xA000A8C8 $v1_post=0x00000008 [10] cyc=20095155 $a0_in=0x07 $ra_in=0xBFC52368 $v0_post=??? $v1_post=??? ...repeats every Ch217 pass... ``` Two callers, interleaved: | Caller location | `$a0` | Return target | |--------------------------|-------|----------------| | Ch264 callee at 0xBFC52990 | 0x0F | 0xBFC52998 | | Ch217 trampoline at 0xBFC52360 | 0x07 | 0xBFC52368 | The `$a0=0x07` path's `$v0_post` is `x` because the exit predicate was scoped only to "return-to-Ch264-callee" (PC=0xBFC52998). Future autopsy refinement: also exit on PC=0xBFC52368 to capture the other arm's $v0. Doesn't change the structural conclusion. The `$a0=0x0F` path returns `$v0=0xA000A8C8` identically every treadmill pass — that matches the Ch217 outer-caller's `$v0_post=0xa000a8c8` exactly. Consistency check ✓. ### HELPER_CONTROL_FLOW (every JAL/J/JR retired inside helper) ``` pc=0xBFC4D380 instr=0x0FF13CC8 jal target=0xBFC4F320 <-- LEAVES helper pc=0xBFC4D390 instr=0x03E00008 jr target=0x00000000 ``` Repeated 47 times (every helper invocation hits this exact pair). **The helper has exactly one JAL out, every time, to `0xBFC4F320`.** No conditional branches, no other JALs, no JR that isn't the function epilog. This is a one-call thunk by structure. ### HELPER_BODY_DATA_READS (every non-fetch read inside helper) 23 reads captured. **All from the single PC `0xBFC4D388`** — which is the instruction immediately after the JAL's delay slot, i.e. the saved-`$ra` reload (`lw $ra,N($sp)` in the standard MIPS epilog). Three distinct EAs, all in EE_RAM: | EA | Hits | Pass mask | First data | data_varies | What it is | |-------------|------|-----------|------------------|-------------|------------| | 0x801FFEE4 | 2 | 0x0001 | 0xBFC528AC | yes | Pre-treadmill $sp's $ra slot (only during BIOS init) | | 0x801FFDFC | 13 | 0x01FF | 0xBFC521C4..0xBFC52368 | yes | Ch217 trampoline's $sp+$ra-slot ($a0=0x07 caller) | | 0x801FFDE4 | 8 | 0x01FE | 0xBFC52998 | **no** | Ch264 callee's $sp+$ra-slot ($a0=0x0F caller) — stable because that caller never changes | Each helper invocation reads exactly one EA — the saved `$ra` at its caller-determined stack frame. **There is no MMIO read. No kernel-global read. No timer read. No non-stack read of any kind.** The helper body is structurally the same shape as the Ch264 callee: prologue → JAL → restore `$ra` from stack → JR. ## Verdict-label nuance — false-positive The literal verdict `helper_static_ram_gate_found (EA=0x801FFDE4 ... data=0xBFC52998)` is a **known false-positive of the stable-EA heuristic**. The condition "appears in ≥2 passes AND data doesn't vary" is satisfied because the Ch264-callee-side caller path is itself stable (every pass the helper is entered with the same `$ra=0xBFC52998`, so the saved-$ra slot reload returns the same value). But `0xBFC52998` is **exactly `$ra_in + 0`** for the Ch264-callee caller — i.e. it's the return address that the helper itself stashed on entry, not a polled state. Reading it back yields a stable value because the caller doesn't change, **not** because external state is settled. The stack-only check (`abs(ea - first_ea) ≤ 0x40 && region=EE_RAM`) didn't filter this out either — the helper is called from two caller-paths with different `$sp` values 0x801FFDE4 and 0x801FFDFC, which are 0x18 apart but the all-three-EAs spread is 0x100 wide (because 0x801FFEE4 - 0x801FFDE4 = 0x100), exceeding the 0x40 sibling threshold. A more robust heuristic would discount any stable read whose returned value equals the caller's `$ra_in` (i.e. detect saved- $ra reloads explicitly). Not blocking — the control-flow table makes the structural truth obvious without the heuristic. Future Ch266+ autopsies can incorporate this filter. ## What this means for the search After Ch263+Ch264+Ch265, the structural picture: ``` Ch217 trampoline 0xBFC52340..60 -> JAL 0xBFC52984 (Ch264 callee, $a0=2) -> sw $ra,0x14($sp) -> JAL 0xBFC4D370 (Ch265 helper, $a0=0x0F) ← thunk -> sw $ra,N($sp) -> JAL 0xBFC4F320 (Ch266 target) ← thunk to ??? -> ??? -> lw $ra,N($sp) -> jr $ra -> lw $ra,0x14($sp) -> jr $ra -> JAL 0xBFC4D370 again with $a0=0x07 (Ch217 post-call path) same thunk to 0xBFC4F320 ``` **Every layer so far has been a wrapper.** The actual work — the polled-state lookup — has not yet appeared. It almost certainly lives at or below `0xBFC4F320`. The constant `$a0=0x0F` selector passing all the way through `0xBFC52984` -> `0xBFC4D370` -> `0xBFC4F320` strongly suggests this is a **selector-dispatched BIOS API**: something like `GetXY(selector=0x0F)`. The Ch217 outer-caller also calls this chain with `$a0=2`, and the Ch217 trampoline's second JAL goes through with `$a0=0x07`. Different selectors, same dispatcher. This is a classic PS2 BIOS pattern: a single entry point with a selector argument. `$v0=0xA000A8C8` is a kernel-space pointer (the kuseg of A0.. maps to physical RAM in the conventional `kseg0` shadow). That return value being constant every pass is consistent with the dispatch returning a **pointer to a stable kernel structure**, which the longjmp-return caller then uses as a jump table base or as a data source. ## Recommendation for Ch266 **Recurse one more frame, to `0xBFC4F320`.** Same observer pattern, bump the PC window. Expected outcomes (in order of likelihood, based on the chain so far): 1. **`helper_is_thunk` again** — `0xBFC4F320` is also a wrapper to something deeper. Then Ch267 follows its JAL out. 2. **`helper_static_mmio_gate_found`** — `0xBFC4F320` reads from some PS2 MMIO region (EE INTC, EE BIU, EE_MISC_MMIO, or `0x1FA00000` which was the Ch263 deferred Pivot 2). That's the gate. Ch267 models the device. 3. **`helper_static_ram_gate_found`** with a non-stack EA — a kernel global in EE_RAM. Ch267 models what writes there. Implementation notes for the autopsy itself: - The verdict heuristic should add a saved-$ra filter: discount any stable EA whose returned value equals the most-common `$ra_in` for the same caller. Could be done in the autopsy itself, or post-hoc by reading the stream. Note this in the block. - The `HELPER_PASSES` exit predicate (PC=0xBFC52998) was scoped to the Ch264-callee return; the Ch217-trampoline caller's return was missed. For Ch266 (assuming again a single primary caller from the deeper helper), pick the most-frequent caller's post-JAL PC and gate exit on that. Alternatively widen exit: trigger on ANY retire whose PC is outside the helper window and was reached from inside in the immediately preceding cycle. Not critical. - The `CH265_PASSES` cap of 16 is fine for 8 Ch217 passes × 2 caller paths per pass = 16 invocations. For the next layer bump to 32 to leave headroom. ## Files changed - `sim/tb/integration/tb_ee_core_bios_smoke.sv` — added `\`ifdef CH265_HELPER_AUTOPSY` block. New structure: data-read capture (mirror of Ch264), `$a0/$ra/$v0/$v1` per-invocation snapshots, control-flow capture with `peek_instr`-driven opcode decode and J/JAL-target computation, region-name task, `ch265_cf_mnemonic` function for prettier prints, full 5-way verdict logic. Two `ch265_print_autopsy()` call sites (halt + timeout exits), both gated by the ifdef. - `sim/Makefile` — new `tb_ee_core_bios_long_helper_autopsy` target (only `-DCH265_HELPER_AUTOPSY`). ## iverilog 12 gotchas hit (and avoided) 1. **Bit-select on parenthesized function-result expression.** First version had `{ (pc + 32'd4)[31:28], instr[25:0], 2'b00 }` inside `ch265_jtarget`. Elaborated as "Malformed statement." Fix: compute `dslot = pc + 32'd4` into a temp, then bit-select `dslot[31:28]`. (Already documented in [[project-self-driven-milestone]] — bit-select on function return; same shape.) 2. **Wrong identifier names for trace_pkg constants.** First version used bare `EV_READ` / `SUBSYS_MEM` / `ee_map_ev_kind`. The right names are `trace_pkg::EV_READ` / `trace_pkg::SUBSYS_MEM` / `ee_map_ev_event`. Easy to confirm by grepping existing Ch218 and Ch264 capture code. ## Regression Full regression: 157 / 157 with the new target off by default (`CH265_HELPER_AUTOPSY` undefined for routine builds). Standing by for Codex's Ch266 call. Recommendation: recurse to `0xBFC4F320`. Same observer infrastructure; bump the parameter.