Skip to content

Resolve register calls and hoisted jump tables without indirect fallback entries - #286

Open
Fiztba wants to merge 1 commit into
ran-j:mainfrom
Fiztba:cfa-register-calls-hoisted-tables
Open

Fiztba wants to merge 1 commit into
ran-j:mainfrom
Fiztba:cfa-register-calls-hoisted-tables

Conversation

@Fiztba

@Fiztba Fiztba commented Oct 13, 2026

Copy link
Copy Markdown

Two changes to ControlFlowAnalyzer that stop functions being promoted to indirect fallback entries when the analyzer already has what it needs.

1. A register call no longer promotes its caller to fallback entries

An unresolved JALR used to set needsIndirectFallback. That turned every instruction of the calling function into an entry point and an indirect fallback entry. A register call returns to its link address the same way JAL does:

  • The analyzer already queues pc + 8 as a resume entry, and that is the only place control comes back to.
  • The callee is resolved through the function table at run time.

So an unknown call target is no reason to make the rest of the caller re-enterable.

Behaviour that is unchanged:

  • JALR still goes through the jump-table search, so a register call that does resolve to local targets keeps them.
  • A JALR whose link register is $zero doesn't link, so it is still treated like JR and falls back as before.
  • Fallback reporting (recordIndirectFallbackPromotion) no longer lists register calls among the jumps that caused a promotion.

This reverses the expectation in the existing test "unresolved JALR marks internal labels as indirect fallback resume entries" (added in #128). I've rewritten that test to assert the new contract:

  • no fallback entries;
  • the link address is still a resume entry and still a label;
  • nothing past the link address becomes a label.

A new test pins the $zero-link case.

2. Hoisted table LUI resolves against a configured [jump_tables] entry

GCC sometimes hoists a switch table's lui into a saved register far above the dispatch. It then forms the low half into a different register:

lui   $s0, %hi(table)        # many instructions earlier
...
sll   $v0, $a0, 2
addiu $v1, $s0, %lo(table)   # rt != rs
addu  $v0, $v1, $v0
lw    $v0, 0($v0)
jr    $v0

The table search only matched a lui into the addu operand itself, so this shape always fell back. The analyzer now also follows addiu rt, rs, %lo (with rs != rt) back to the lui of rs. It accepts the result only when the address names a table listed under [jump_tables], so code without that config behaves exactly as before. Two new tests cover the shape:

  • with the table configured, it resolves with no fallback entries;
  • without the config, it still falls back.

Results (King's Field IV, SLUS-20318)

Function map Fallback functions (entries), before After
Ghidra map, JALR change alone 1,352 (119,442) 150 (6,046)
Exact function boundaries + configured jump tables 634 (103,858) 0

I regenerated KFIV from this branch with exact function boundaries and configured jump tables:

  • 0 fallback promotions and 17,787 resume entries;
  • the generated sources match the build that was verified frame-identical on native Windows. The only difference is SQRT.S emission, which comes from an unrelated local patch.

Overlap with #279

#279 (also mine, "Fix continuation ownership for overlapping entry wrappers") touches entry handling too, but in different files:

Both add tests to code_generator_tests.cpp, in separate places: this PR's tests sit with the other control-flow analysis tests, and #279's hunk starts later in the file. They should merge in either order without conflicts.

Testing

  • MSVC 19.44 (VS 2022 Build Tools), Release: ps2_recomp and ps2x_tests build.
  • ps2x_tests (run from the repo root): 440/440 pass.

🤖 Generated with Claude Code

An unresolved JALR promoted every instruction of its function to an
indirect fallback entry, although a register call returns to its link
address like JAL. The resume entry it already queues is all the caller
needs; the callee is found through the function table at run time. A
JALR that links into $zero does not return and still falls back like JR,
and register calls no longer appear in the fallback promotion report.

A switch whose table LUI the compiler hoisted into a saved register,
with the low half formed into another register by ADDIU rt, rs, %lo,
never matched the table search. That shape is now accepted when it
names a table listed under [jump_tables], so unconfigured code behaves
as before.

KFIV with exact function boundaries and jump tables: 634 fallback
functions (103,858 entries) -> 0. With the Ghidra map: 1,352 (119,442)
-> 150 (6,046).

Co-Authored-By: Claude Opus 5.5 <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