Files
AnyPS5/docs/dev/debt/relinker.md
T

13 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.

  • Windows input and output collision checks use temporary hard links when MinGW reports different permissions for two multiply linked files, as with .elf and .exe names. The comparison fails if either file is read-only or either parent directory refuses the temporary link, even when the files are distinct.

  • 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; the entry stub gives each missing one anyps5.log beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost.

  • 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 reads a 16-bit displacement for 66 E8, 66 E9 and 66 0F 80-8F without REX.W, as AMD processors, the console included, do (binutils objdump -M amd64; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms.

  • --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 Windows output, a guest instruction that addresses memory through GS becomes UD2 and NOP padding of its own length (TLS builder): the host owns GS, and the guest GS base that FreeBSD's sysarch(AMD64_SET_GSBASE) would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine ntdll.

  • 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.

  • The syscall scan of sce_module modules with a PT_SCE_DYNLIBDATA segment stops at the same end, or, when the PLT comes before the code, at the end of the last function of the unwind table; code after it is not scanned.

  • 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, or of the /libexec/ld-elf.so.1 string that begins the segment when no PT_INTERP covers it, as in OpenOrbis modules, to the end of the PLT after the last function of the unwind table, or to the end of that function when the PLT comes before the code, as in the libSceFios2.prx and libc.prx that CUSA02636 bundles (PLT at 0xc8); the pipeline's scans of executables above stop at this same end, also when the PLT comes before the code. A guest module with PLT relocations and no PLT before or after its unwind functions, or with a PLT that overlaps one, fails as the executable does. Code after that end is not analyzed, such as the DT_FINI routine without an unwind entry that follows the last unwind function in both modules and that the Windows and macOS outputs call when the program exits: a TLS access in it is not rewritten, the ELF entry point, DT_INIT, DT_FINI, a branch or a 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).

  • In macOS output, a guest instruction with a GS segment prefix, including one with no memory operand (65 90, 65 48 89 c8), which does not touch GS, becomes UD2 and NOP padding of its own length (macOS image writer): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's sysarch(AMD64_SET_GSBASE) would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine ntdll.

  • 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 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. 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, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. 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.