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]>
The raw-byte check that runs before a syscall pattern between PS4 unwind
functions is skipped now also accepts mov rax, imm64, merges a following
mov al, imm8 or mov ax, imm16 into the earlier constant, and walks back
over any number of register moves or zeroings that leave rax alone
instead of at most seven. Visited states keep the walk linear in the
bytes it covers.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A relocation or lea target outside the unwind functions of a PS4 image
is now decoded up to its first jump, return or trap, or to the start of
the next unwind function, which also bounds the transfer check of the
tail rule. A target whose run holds an fs:[disp32] access, the form the
linear TLS sweep roots, is followed as code. The other targets taken as
data are counted in one message and kept as entry points that the
Windows TLS rewrite may not overwrite. The analysis merges nested
unwind ranges like the scans.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The skip of syscall patterns outside the unwind functions let some real
syscalls through. The scan now merges nested and overlapping unwind
ranges with the instruction scanner's MergeCodeRanges, so a pattern
inside an outer function is refused. A constant syscall number is also
looked for in the bytes in front of the pattern, whatever alignment the
decoding took: a mov eax, mov rax or xor eax, eax that ends at the
pattern or before register moves and zeroings that leave rax alone. The
decoded lookback requires each instruction to end where the next one
starts. Guest modules without PT_SCE_DYNLIBDATA get no unwind ranges,
so their scan stays strict even when their unwind table is read.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The code analysis of the Windows and macOS outputs followed every
relocation and lea target inside the code range as code. PS4 images keep
read-only data between unwind functions, and strings and tables there
decoded into garbage, failing the conversion. Outside the unwind functions
such a target is now code only when it is 16-byte aligned, starts less
than 256 bytes after an unwind function and decodes to a jump, return or
trap; lea targets go through the same rule instead of the graph's own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A --module-dir module that only the executable needs was initialized by
the Windows entry stub with no start arguments, so a later
sceKernelLoadStartModule with arguments found it started and its
module_start never saw them. Unity's mono-ps4.sprx keeps those
arguments for the plugins it loads. Such a module is now deferred like
any other runtime module: mapped at startup and started, with its
arguments, when the title loads it. Modules another guest module needs
are still started at startup, and Linux and macOS are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rosetta ends the process instead of raising SIGSEGV or SIGBUS when a
VMASKMOVPS, VMASKMOVPD, VPMASKMOVD or VPMASKMOVQ store faults on any
element other than an active first element ("rosetta error: unexpectedly
need to EmulateForward on a synchronous exception"). Measured on an M5
with macOS 27.0.1 for all eight 128- and 256-bit store forms. A masked
store into a page the macOS write watch keeps read-only therefore ends
the title.
--to-rosetta now moves each masked store to a stub that saves RAX, RCX
and RFLAGS below the red zone, computes the operand address with LEA,
reads the mask sign bits with VMOVMSKPS or VMOVMSKPD, and writes to the
first and last byte of every active element with LOCK OR of 0. Those
writes fault normally, so the write watch sees the page and unprotects
it, and they leave memory unchanged. The stub then restores the
registers and runs the original store. Segment and address-size
prefixes are kept on the probes, RSP-based operands skip the spill
frame, and RIP-relative operands are relocated. Masked loads, the
Intel target and a plain relink are unchanged.
masked_store_lowering decodes every form, register and extension,
checks the stub bytes, and on x86-64 Linux and macOS runs 1728 stubs
across all forms, six addressing modes, six offsets around a read-only
page and six mask patterns. It checks the stored bytes, that exactly one
fault reaches the handler when an active element touches the page,
that no register, flag or red-zone byte changes, and that RSP is
restored. Without the probes the x86-64 build aborts under Rosetta.
test_to_rosetta.py checks that only --to-rosetta lowers a VMASKMOVPS.
MCOMMIT becomes STC plus NOP padding and RDPRU goes through a
runtime-checked stub returning the host TSC for MPERF/APERF while
other selectors yield zero, with converter and execution tests.
docs/dev/debt/relinker.md records the new lowering and its unverified
assumptions.
The Linux entry stub now passes each traced import through libc.prx's
TraceImport_nid_no_patch before it calls the guest entry, and stores the
returned thunk in the import's slot. The same imports are traced as on
Windows: functions bound through a PLT slot or a GOT slot, without an
addend, and not weak. TraceImport_nid_no_patch is bound eagerly through
an added GLOB_DAT relocation, so a libc.prx without it fails at load.
Each slot's page is made writable with mprotect first, so slots inside a
PT_GNU_RELRO range can be replaced.
--trace-imports now requires libc.prx among the libraries on Linux too,
and is refused with --macos.
MinGW's std::filesystem::equivalent returns false for hard links whose
names give different permissions, such as .elf and .exe. The temporary
hard-link probe that worked around it failed on read-only files and on
directories that refuse the link. Compare the volume serial number and
file index from GetFileInformationByHandle instead.
Co-Authored-By: Claude Opus 5.5 <[email protected]>