Read the unwind functions of an image that only has PT_GNU_EH_FRAME so
inter-FDE data tables are still found and masked, but leave the syscall
and call-site scan ranges empty without PT_SCE_DYNLIBDATA as main does.
The fixture now emits standard dynamic tags for its SysV block, and the
debt entry stays within the 1000 character limit.
In PS5 Prospero PRX binaries, compilers place embedded constant data tables (such as the OpenSSL Whirlpool lookup table in ubiservices.prx) in inter-FDE gaps after function code, referenced via RIP-relative LEA instructions.
- In RelativeJumpTables, detect constant data tables in inter-FDE gaps that begin with padding, contain no direct branch targets, and contain undecodable non-code bytes, distinguishing them from valid unannotated code routines and multi-hop gap branches.
- In GuestModuleBuilder and RelinkerPipeline, extract unwind functions from PT_GNU_EH_FRAME as well as PT_SCE_DYNLIBDATA, and mask embedded data tables with 0x90 in scan buffers before SyscallScanner.
- In CodeInstructionCollector, populate table boundaries whenever unwind functions are present to prevent addAddressTakenRoot from adding data table addresses to CFG roots on Windows and macOS.
- Update relinker technical debt documentation and add regression tests.
Fixes#7547
Unblocks: PPSA28155 #7547
The debt entry said that shrink-wrapped leaves, which push later and pop in
balance, could be framed. They cannot as the framer works now: lowering RSP on
entry moves their later pushes onto the spills, which keep their addresses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Walk each macOS function import before the guest entry and pass it through TraceImport_nid_no_patch, as Linux and Windows already do. An x86-64 libc build records the call and prints it on SIGABRT.
The Linux output copied a nonzero r_addend of GLOB_DAT and JUMP_SLOT
relocations unchanged into DT_RELA and DT_JMPREL, while the Windows,
macOS and guest module paths refuse it. The x86-64 psABI computes both
types as S, but glibc before f8587a61892c applies S+A when binding
eagerly, so the result depended on the host glibc and, for JUMP_SLOT,
on the binding mode. The Linux patcher now refuses such an entry in all
three Linux modes with the Windows message and the unrebased r_offset.
Entries with a zero addend are emitted as before.
The check that refuses leaves without pushes that push or pop also refused
`push rbp; mov rbp, rsp` leaves without other pushes, whose `pop rbp` it
counted, since rbp is not in their list of pushes. A unit test and an
execution test now cover such a leaf with a 4-byte prologue and a 2-byte
exit. The debt entry also notes that the check refuses shrink-wrapped leaves
whose later pushes and pops balance.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
--frame-red-zone replaces a leaf's prologue and each exit with a 5-byte jump
to a stub, so it skipped leaves whose pushes or pop-and-ret exit were shorter
than that, and leaves with neither pushes nor an allocation. A short prologue
now takes in the straight-line instructions after it and a short exit those
before it, re-encoded in the stub with the moved displacement. Widening stops
at branches, rets, RIP-relative instructions, pushes and pops, and the leaf is
skipped when a branch targets the instructions taken in or the first
instruction of a widened prologue, or when a leaf without pushes pushes or
pops.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
XSAVEOPT raises an illegal instruction under Rosetta on an M5 running
macOS 27.0.1, as XSAVEC does, while XSAVE and XRSTOR save and restore
x87, SSE and AVX state in the standard format. The PS5's Zen 2 has
XSAVEOPT, so code built for it can use the instruction without a CPUID
check.
XSAVEOPT differs from XSAVE only in the init and modified optimizations:
it may skip writing state that is in its initial configuration or
unchanged since the last XRSTOR from the same area. XRSTOR restores the
same state from the area XSAVE writes, so --to-rosetta rewrites
XSAVEOPT (NP 0F AE /6, memory operand) to XSAVE (/4) in place, keeping
the prefixes and the operand, as RDSEED becomes RDRAND. CLWB (66 0F AE
/6), MFENCE, UMONITOR, UMWAIT and the Intel target are left alone.
XSAVEC is not converted: it writes the compacted format.
The converter tests check the rewrite for the plain, REX.W,
RSP-relative, extended-base, RIP-relative and FS-relative forms, check
that the neighbouring 0F AE encodings are not taken, and execute the
rewritten XSAVEOPT64 followed by XRSTOR on YMM3 in the x86-64 build.
The --to-rosetta integration test now covers XSAVEOPT as well.
Function starts were direct call targets plus 16-byte-aligned code after
int3 padding. A function reached only through a pointer and not preceded
by int3 then ended up inside the previous region, and its pop rbp; ret
exit could get the epilogue lea without a matching prologue.
The bounds now come from the executable's .eh_frame FDEs (the existing
EhFrameReader), each function is decoded from its FDE start to its FDE
end, and code outside every FDE is not framed. Conversion stops with an
error when the executable has no FDE. Sites added in one pass also count
as taken for the next function.
The debt entry no longer says that a leaf with an rsp operand that
cannot move in place is skipped. A new entry says that such an operand
moves to a stub, when the leaf is still skipped, and that a game patch
over a framed site or a moved displacement is refused. The USAGE row
says the same in short.
A bundled module that names a console file dependency (.sprx, .suprx,
.debug_prx) imports by symbol name, as homebrew built with the
ps5-payload-dev SDK does. Bind those imports to the NID of the name,
except names another bundled module or the executable exports, and name
the module's host dependencies with the .prx library of the same stem.
Homebrew built with the ps5-payload-dev SDK names its system
libraries with .sprx and imports by symbol name, while the libraries
in libs/ are .prx files that export NIDs. On Linux such an executable
converted but stopped at load time on the first .sprx dependency and,
once that was found, on the first symbol name.
The Linux patcher now loads a dependency named with .sprx, .suprx or
.debug_prx as the .prx library of the same stem. In an executable that
names such a dependency, an import without a module that is named by
symbol gets the NID of that name. A name that a bundled module exports
still binds to that module. There is no lookup by the name as written,
because the host loader also searches the host C library. Other
executables keep their unqualified imports as written.
Since #8162 a --module-dir module that the executable needs is deferred
on Windows: the entry stub maps it and the title starts it with
sceKernelLoadStartModule. A module the executable imports from but the
title never loads gets no module_start and no initializers, silently.
The entry stub now prints one line at startup for each deferred guest
module that an import of the executable names. Guest module bytes are
unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Guest module discovery skipped the input executable and the
--executable check rejected an executable converted twice through
std::filesystem::equivalent, which MinGW reports as false for hard links
whose names give different permissions, such as .bin and .exe. Move the
GetFileInformationByHandle comparison to Io::SameFile and use it for
these checks and for SameOutputFile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>