Skip to content

feat(analyzer): expose data-, instruction- and table-referenced function pointers as callable entry candidates - #399

Open
LeandroSSB wants to merge 3 commits into
ran-j:mainfrom
LeandroSSB:pr-04-analyzer-entry-tiers
Open

LeandroSSB wants to merge 3 commits into
ran-j:mainfrom
LeandroSSB:pr-04-analyzer-entry-tiers

Conversation

@LeandroSSB

Copy link
Copy Markdown

Root cause

PS2 games reach real functions through pointers the CFG entry walk never sees:

  1. Data tables — SDK printf output chains, module dispatchers, .ctors: 4-byte words in a loadable section that point at aligned unknown text.
  2. Instruction-formed pointers — lui+addiu pairs formed in code and passed straight to registration syscalls (SetAlarm/handler, thread entry).
  3. Prologue-less table targets — data-word targets that are neither known starts nor decoded direct-branch labels (the recompiler's actual registration oracle), e.g. handlers whose first instruction is an argument load (lw a0, 0(a1)), not a prologue.

With no symbol and no jal the function never becomes a compiled entry — and a JALR through the unregistered pointer kills the calling thread (makeDormant). On MOH:Vanguard this killed two threads at the first post-boot printf and froze every engine sleep; the class has ~6.8k targets in this ELF alone.

Fix

AnalysisPasses::collectDataReferencedEntryCandidates scans all loadable sections for (a) 4-byte words pointing at aligned unknown text with a standard prologue and (b) lui+addiu address-forming pairs; a third tier admits prologue-less data-word targets that are neither known starts nor decoded direct-branch labels. Candidates flow through the existing entry_points channel, so the recompiler synthesizes real entries for them.

Three commits, one per tier (data / instruction-formed / prologue-less tables), each with its own tests.

Related: #286 resolves calls through registers at runtime — complementary (that is a runtime concern; this is the compile-time entry-set concern).

Test

elf_analyzer_tests: data-referenced (known start / interior / outside-text / self-pointer rejections; prologue unit test), instruction-formed (lui+addiu pair), prologue-less table targets, direct branch targets feeding the served set.

Measured (MOH:Vanguard)

+33 data and +22 instruction candidates immediately synthesized two missing entries; tier-3 added 3.203 candidates (55 → 3.258), 18 standalone entries; zero no-function inside text and zero dormant threads in the next run.

🤖 Generated with Claude Code

Suite evidence

Fresh configure+build of ps2x_tests on this branch (VS 2022, Release): 450/450 passed, 0 failed.

LeandroSSB and others added 3 commits October 26, 2026 00:26
…entry candidates

Games reach real functions through data tables of function pointers (SDK
printf output chains, driver callback registries, timer tables). The CFG
walk from the ELF entry never sees those targets, and a symbol-less one
never becomes an entry point, so the generated module has no callable
function at that address. At runtime the indirect call is reported as a
missing function and the calling guest thread is made dormant - Vanguard
lost both of its threads this way (JALR from the printf output-callback
walker at 0x356568 to 0x356998, referenced by the data word at 0x510a54,
killed tid 1 and tid 3 and stalled the whole engine).

AnalysisPasses::collectDataReferencedEntryCandidates scans every loadable
section for 4-byte words that point into executable text at an address
that is not a known function start and whose first instruction is a
standard prologue (addiu/daddiu sp, sp or sd ra, sp). ElfAnalyzer emits
those addresses as address-referenced entry points, the same channel the
recompiler already uses to synthesize standalone guest entries.

Regression test covers the prologue predicate and the candidate scan
(known start, interior non-prologue, out-of-text and self pointers are
rejected). Suite: 454/454.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…candidates

Callback pointers are also formed in code as lui+addiu pairs and handed
straight to registration syscalls - SetAlarm's handler argument never
touches data memory, so the data-word scan cannot see it. Vanguard's
alarm-sleep wake callback (0x355840, referenced only by
'lui s1, 0x35; addiu s1, s1, 0x5840' before SetAlarm) had no compiled
entry: the runtime's setAlarm rejected the handler as unmapped, the game
slept anyway, and no alarm ever woke it - every thread in the engine
parked in alarm sleeps while the boot heartbeat timer froze.

collectDataReferencedEntryCandidates now also walks executable sections
for lui+addiu/daddiu same-register pairs forming a 4-aligned text
address that is not a known function start and whose first instruction
is a standard prologue. The candidate set stays small (22 additions on
MOH5RDVD.ELF vs 33 from the data scan).

Suite: 455/455.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…candidates

Module dispatch tables hand out entries that are bare continuation
fragments whose first instruction is a register load, not a prologue.
Vanguard's module dispatcher died exactly this way: jalr into 0x10d6b8,
pointed to by the .rodata words 0x544420/0x544590, with no dispatch row
at the target, so the scheduler put the thread to sleep for good.

Third tier of collectDataReferencedEntryCandidates: a data-section word
that points into a code section at an address the runtime cannot already
dispatch to - not a known function start, not a decoded direct branch
label - becomes a callable entry candidate regardless of prologue shape.
Function spans are deliberately not the oracle: discovery sweeps them
linearly, so a span also covers holes the compiled dispatch table never
fills. Pointer sources stay restricted to data sections so code words
that merely look like text pointers (every jr ra encodes 0x03e00008,
which lands inside text) are not treated as table registrations.

On MOH5RDVD.ELF the tier emits 3203 new entry candidates beyond the two
earlier tiers, including the 0x10d6b8 dispatcher target.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant