mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 17:55:23 +02:00
CrossCraft Classic jumps to 0xae870000 a few seconds in, after a couple of
button presses, and dies. It runs fine on a real PSP.
The jump is a Zig interface call - std.mem.Allocator is {ptr, vtable} and the
crash is `lw t9, 0(vtable); jalr t9`. The vtable pointer had come out as
0x089191E0 instead of 0x089291E0, exactly 64KB low, so t9 was loaded from the
middle of compiler_rt.udivmod and the "function pointer" was really the
instruction word `sw a3,0(s4)`.
That pointer is built by a lui/addiu pair, and which LO16 completes a given
HI16 decides one thing: whether the high half carries. We were pairing each
HI16 with the next non-HI16 relocation in the table, which can't work in
general. A PRX relocation carries no symbol index - r_info holds segment
numbers - so the table cannot express the pairing. prxgen does order it so a
HI16 is followed by a LO16 for the same symbol, but not necessarily by *its*
LO16: LLVM schedules several luis together and their addius come back
permuted, here even earlier in the table than the lui they belong to. Two
LO16s for one symbol usually yield the same high half, which is why this has
gone unnoticed for so long - it only bites when their low halves land on
opposite sides of 0x8000. This game has two .rodata references in one
function that do exactly that, and the loader swapped them.
Pair the way the compiler generated it instead: the lui loads a register, and
the instruction completing it is the next one using that register as a base -
unless another lui reloads that register first, in which case the candidate
belongs to that one. Falls back to the old scan when the register trail goes
cold (the value gets copied before use), so nothing that worked before stops.
Checked exhaustively against an oracle: the pre-link ELF still has a symbol,
and thus a section, for every relocation, so any pairing whose result lands
outside that section is wrong. Over this module's 8589 HI16 relocations the
old rule puts 22 outside their section; the new one puts 0, and never picks a
LO16 belonging to a different symbol. Both the register match and the reload
guard are needed - without the guard it scores worse than the old rule.
Impact is narrow. On GCC-built PRXs (pspautotests) the chosen LO16 differs for
2-4 relocations per module and the emitted instruction changes in none of
them. On this LLVM-built module 449 pairings differ and 46 emitted addresses
change, i.e. 46 pointers that were quietly 64KB off.
pspautotests 314/314, unit tests 51/51.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9