Files
AnyPS5/docs/dev/debt/relinker.md
T
Get-fix 8c34587598 fix(relinker): search libc.prx for libkernel-only Windows executables
AnyPS5 exports some libkernel functions, such as __error, from
libc.prx, which libkernel.prx links. On Linux they resolve through that
dependency. On Windows, the import stubs search only the listed
modules, and libc.prx was listed only when libSceLibcInternal.prx was
needed. A PS4 homebrew that needs libkernel.prx but no C library (the
OpenOrbis graphics sample) failed with "unresolved ELF import
9BcDykPmo1I". libc.prx is now also listed when libkernel.prx is needed.
PS5 executables need libSceLibcInternal.prx and convert unchanged.
2026-10-15 21:02:31 +03:00

9.5 KiB

relinker technical debt

Functional

  • Executable guest imports - on Linux and macOS, a relocated executable import resolved to one guest provider requires that module and its dependencies at startup, even when it was discovered through --module-dir without a DT_NEEDED entry. Unreferenced symbols and no-op relocations do not require an otherwise deferred module. Console module-start ordering is unmeasured.

  • Runtime-loaded modules (--module-dir) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded.

  • The executable file that relinker generates opens a console when launched, unless it is relinked with --windows-gui, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle, so everything it prints is lost, including the entry stub's errors and crash reports (#3282).

  • Guest import registries - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability.

  • Converted Windows main executables omit DYNAMIC_BASE by default, not only with --windows-fixed-base: on the console the executable lies inside the 40-bit window that the title's own sceLibcMspaceCreate checks (#3043). Relocations are kept, so if 0x140000000 is taken Windows relocates the image, possibly above that window. Verified only on Windows 10.

  • SDK memory-management syscall traps under --trap-sdk-syscall-stubs recognize three exact instruction sequences from ps5-payload-dev SDK libc/mman.c, observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes UD2; this does not implement mmap or mprotect and a call still faults.

  • The instruction decoder decodes a near CALL, JMP or Jcc with an operand-size prefix (66) as Intel does, with a 32-bit or 8-bit displacement and a 64-bit target. AMD processors, the console included, read a 16-bit displacement for 66 E8, 66 E9 and 66 0F 80-8F and truncate the target of every near branch with that prefix, rel8 forms included, to 16 bits. Compilers do not emit these forms; the relinker's length and target for them do not match the console.

  • --to-intel does not lower RDPRU/MCOMMIT; the matcher fails the relink instead.

  • The length-changing path of the instruction rewriter is not used by the converter: it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length.

  • A PT_SCE_RELRO segment that no PT_LOAD covers, the PS4 layout, is loaded as a read-write PT_LOAD and stays writable after relocation; FreeBSD rtld makes PT_GNU_RELRO read-only once relocation is done, and what the console does with PT_SCE_RELRO is not checked.

  • In executables with a PT_SCE_DYNLIBDATA segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The relinker pipeline therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. --to-intel and the unused-filter=1 and unused-filter=2 analyses are not limited this way.

  • In executables and guest modules with a PT_SCE_DYNLIBDATA segment, the code analysis of the Windows and macOS outputs follows code only from the end of PT_INTERP to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion.

  • A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for STN_UNDEF (pipeline). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot (relocation builder). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 (#4437).

  • The Linux placement of the --to-intel stubs in the ELF patcher is covered only by a synthetic test.

  • --to-rosetta lowers what Rosetta lacks on top of --to-intel (matcher). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI.

  • --to-intel replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with correctly rounded 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted).

  • --trace-imports (entry stub) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with sceKernelDlsym, is not, and neither are weak imports or the imports of modules loaded at run time

  • ANDN lowering uses scalar NOT and AND for 32- and 64-bit register and memory operands under --to-rosetta, following the ANDN entry in Intel SDM volume 2A. SF and ZF follow the result, CF and OF clear, and AF and PF are undefined. The scratch register is saved below the 128-byte red zone. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws; other BMI instructions are not lowered. The M1 test covers register overlap, RSP operands, memory addressing, RIP relocation, flags and red-zone preservation; precise fault delivery inside a spill sequence has not been verified.

  • On macOS, libc.prx is a dependency only when libSceLibcInternal.prx is needed, so a program that needs only libkernel.prx cannot resolve the libkernel functions that AnyPS5 exports from libc.prx, such as __error. Windows also lists libc.prx for libkernel.prx.