Merge pull request #13291 from PolkadotDom/fix/eip-import-call-sites

fix(relinker): resolve EIP-relative import call sites
This commit is contained in:
Dean authored and GitHub committed 2026-11-03 08:45:32 +13:00
commit b2627ce36d
4 files changed
+54 -10

No files matched your search

@@ -19,10 +19,16 @@ std::int32_t readDisp32(const std::vector<std::uint8_t>& text, std::size_t offse
return static_cast<std::int32_t>(v);
}
std::optional<std::size_t> ripRelativeDispOffset(const std::vector<std::uint8_t>& text, const Codegen::InstructionMatch& m) {
struct RelativeOperand {
std::size_t DisplacementOffset;
bool AddressSize32;
};
std::optional<RelativeOperand> relativeOperand(const std::vector<std::uint8_t>& text, const Codegen::InstructionMatch& m) {
std::size_t cur = static_cast<std::size_t>(m.Offset);
std::size_t end = cur + m.Length;
bool hasRexW = false;
bool addressSize32 = false;
while (cur < end) {
std::uint8_t b = text[cur];
if (b >= RexMin && b <= RexMax) {
@@ -30,8 +36,9 @@ std::optional<std::size_t> ripRelativeDispOffset(const std::vector<std::uint8_t>
++cur;
continue;
}
if (b == PrefixRepne || b == PrefixSegCs || b == PrefixSegDs || b == PrefixSegEs || b == PrefixSegSs) {
if (b == PrefixRepne || b == PrefixSegCs || b == PrefixSegDs || b == PrefixSegEs || b == PrefixSegSs || b == PrefixAddressSize) {
hasRexW = false;
if (b == PrefixAddressSize) addressSize32 = true;
++cur;
continue;
}
@@ -47,9 +54,9 @@ std::optional<std::size_t> ripRelativeDispOffset(const std::vector<std::uint8_t>
if (mod != ModRmModIndirect || rm != ModRmRmRipRelative) return std::nullopt;
const auto operation = (modrm >> ModRmRegShift) & ModRmRegMask;
if (opcode == OneByteGrp5Rm && operation >= 2 && operation <= 6)
return cur + 2;
return RelativeOperand{cur + 2, addressSize32};
if (opcode == OneByteModRmRangeJMax && hasRexW)
return cur + 2;
return RelativeOperand{cur + 2, addressSize32};
return std::nullopt;
}
}
@@ -79,11 +86,12 @@ std::vector<FileByteOffset> CallSiteResolver::ResolveCallSites(const std::vector
constexpr std::size_t MinRipRelativeInstructionBytes = 6;
_cachedTargetToInstr.reserve(std::min(static_cast<std::size_t>(candidateCount), textSection.size() / MinRipRelativeInstructionBytes));
scanner->VisitCodeSection(textSection, 0, textSection.size(), [&](const Codegen::InstructionMatch& m) {
const auto dispOff = ripRelativeDispOffset(textSection, m);
if (!dispOff || *dispOff + 4 > textSection.size()) return;
std::int32_t disp = readDisp32(textSection, *dispOff);
const auto operand = relativeOperand(textSection, m);
if (!operand || operand->DisplacementOffset + 4 > textSection.size()) return;
std::int32_t disp = readDisp32(textSection, operand->DisplacementOffset);
VirtualAddress instrEnd = textSectionVAddr + m.Offset + static_cast<VirtualAddress>(m.Length);
VirtualAddress target = instrEnd + static_cast<VirtualAddress>(static_cast<std::int64_t>(disp));
if (operand->AddressSize32) target = static_cast<std::uint32_t>(target);
_cachedTargetToInstr.emplace_back(target, m.Offset);
});
std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end());
@@ -1,5 +1,7 @@
#include <relinker/analysis/CallSiteResolver.hpp>
#include <cstdint>
#include <cstring>
#include <initializer_list>
#include <iostream>
#include <stdexcept>
#include <string>
@@ -18,6 +20,32 @@ void check(const std::string& name, const std::vector<std::uint8_t>& text, bool
require(resolver->ResolveCallSites(text, 0x1000, 0x1200, 8).empty(), name + " unexpected address");
}
void checkAddressSize() {
for (const auto prefix : {std::vector<std::uint8_t>{0x67}, {0xf2, 0x67}, {0x3e, 0x67}, {0x67, 0x67}, {0x2e, 0x67}, {0x67, 0x26}, {0x36, 0x67}}) {
for (const auto opcode : {std::vector<std::uint8_t>{0xff, 0x15}, {0xff, 0x25}, {0xff, 0x35}, {0x48, 0x8b, 0x05}}) {
for (const std::uint64_t address : {0x1000ULL, 0xfffff000ULL, 0x100001000ULL}) {
auto resolver = Relinker::MakeCallSiteResolver();
auto text = prefix;
text.insert(text.end(), opcode.begin(), opcode.end());
text.resize(text.size() + 4);
constexpr std::uint64_t slot = 0x2800;
const auto displacement = static_cast<std::uint32_t>(slot - address - text.size());
std::memcpy(text.data() + text.size() - 4, &displacement, 4);
const std::vector<Relinker::FileByteOffset> expected{address};
require(resolver->ResolveCallSites(text, address, slot, 8) == expected, "EIP-relative import access missing");
require(resolver->ResolveCallSites(text, address, slot + 0x100000000ULL, 8).empty(), "EIP-relative access used the unwrapped slot");
require(resolver->ResolveCallSites(text, address, slot, 0).empty(), "Empty EIP-relative target range matched");
require(resolver->ResolveCallSites(text, address, slot - 1, 1).empty(), "EIP-relative target matched range end");
require(resolver->ResolveCallSites(text, address, slot - 1, 2) == expected, "EIP-relative target missing inside range");
}
}
}
const std::vector<std::uint8_t> rip{0x48, 0x8b, 0x05, 0xf9, 0x17, 0, 0};
auto resolver = Relinker::MakeCallSiteResolver();
require(resolver->ResolveCallSites(rip, 0x100001000ULL, 0x100002800ULL, 8) == std::vector<Relinker::FileByteOffset>{0x100001000ULL}, "RIP-relative access was truncated");
require(resolver->ResolveCallSites(rip, 0x100001000ULL, 0x2800, 8).empty(), "RIP-relative access matched a wrapped slot");
}
std::vector<std::uint8_t> ripInstruction(std::vector<std::uint8_t> prefixes, std::uint8_t opcode, std::uint8_t modrm) {
prefixes.push_back(opcode);
prefixes.push_back(modrm);
@@ -57,7 +85,11 @@ int main() {
check("NOTRACK preserves final narrow REX", {0x3e, 0x48, 0x40, 0x8b, 0x05, 0xf7, 0, 0, 0}, false);
check("FS call is not a flat GOT reference", {0x64, 0xf2, 0xff, 0x15, 0xf8, 0, 0, 0}, false);
check("GS jump is not a flat GOT reference", {0x3e, 0x65, 0xff, 0x25, 0xf8, 0, 0, 0}, false);
check("address-size override is not a RIP64 reference", {0x3e, 0x67, 0xff, 0x15, 0xf8, 0, 0, 0}, false);
check("address-size override is an EIP-relative reference", {0x3e, 0x67, 0xff, 0x15, 0xf8, 0, 0, 0}, true);
check("address-size prefix after REX.W leaves a narrow load", {0x48, 0x67, 0x8b, 0x05, 0xf8, 0, 0, 0}, false);
check("REX.W after address-size prefix widens a load", {0x67, 0x48, 0x8b, 0x05, 0xf8, 0, 0, 0}, true);
check("FS EIP-relative call is not a flat GOT reference", {0x64, 0x67, 0xff, 0x15, 0xf8, 0, 0, 0}, false);
check("GS EIP-relative load is not a flat GOT reference", {0x67, 0x65, 0x48, 0x8b, 0x05, 0xf7, 0, 0, 0}, false);
check("BND register call has no RIP operand", {0xf2, 0xff, 0xd0}, false);
check("NOTRACK register jump has no RIP operand", {0x3e, 0xff, 0xe0}, false);
check("BND-prefixed increment is not an import access", {0xf2, 0xff, 0x05, 0xf9, 0, 0, 0}, false);
@@ -67,6 +99,7 @@ int main() {
check("increment memory", {0xff, 0x05, 0xfa, 0, 0, 0}, false);
check("decrement memory", {0xff, 0x0d, 0xfa, 0, 0, 0}, false);
check("push memory", {0xff, 0x35, 0xfa, 0, 0, 0}, true);
checkAddressSize();
for (const std::uint8_t segment : {0x2e, 0x26, 0x36, 0x3e}) {
const auto name = "segment " + std::to_string(segment);
for (const std::uint8_t modrm : {0x15, 0x1d, 0x25, 0x2d, 0x35})
@@ -80,7 +113,7 @@ int main() {
check(name + " increment", ripInstruction({segment}, 0xff, 0x05), false);
check(name + " decrement", ripInstruction({segment}, 0xff, 0x0d), false);
check(name + " narrow load", ripInstruction({segment}, 0x8b, 0x05), false);
for (const std::uint8_t unsupported : {0x64, 0x65, 0x67, 0x66, 0xf0, 0xf3}) {
for (const std::uint8_t unsupported : {0x64, 0x65, 0x66, 0xf0, 0xf3}) {
const auto suffix = " unsupported prefix " + std::to_string(unsupported);
check(name + " before" + suffix, ripInstruction({segment, unsupported}, 0xff, 0x15), false);
check(name + " after" + suffix, ripInstruction({unsupported, segment}, 0xff, 0x15), false);
@@ -49,6 +49,9 @@ def main():
for segment in (0x2e, 0x26, 0x36, 0x3e):
instructions = [
(bytes([segment, 0xff, 0x15]), True),
(bytes([segment, 0x67, 0xff, 0x15]), True),
(bytes([0x67, segment, 0xff, 0x25]), True),
(bytes([0x67, segment, 0x48, 0x8b, 0x05]), True),
(bytes([segment, 0xff, 0x25]), True),
(bytes([segment, 0xff, 0x35]), True),
(bytes([segment, 0x48, 0x8b, 0x05]), True),
+1 -1
View File
@@ -15,7 +15,7 @@
- [Runtime-loaded modules the executable needs](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) are mapped at startup on Windows and started when the title loads them with `sceKernelLoadStartModule`, with their start arguments, as a Unity player loads `mono-ps4.sprx`. A title that calls such a module before loading it finds its initializers not run; whether the console starts it earlier is not measured. A module the executable only imports from and the title never loads itself gets no `module_start` and no initializers at all under this deferral. The [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) prints `Deferred module <path>` at startup for each such module that an import of the executable names, whether or not the title loads it later; an import without a module name is not reported. macOS still starts it at startup.
- The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) 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](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) 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](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - 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.
- Registry [call-site analysis](../../../core/relinker/relinker/src/analysis/CallSiteResolver.cpp) does not classify instructions with `F3`, `66`, `67`, `F0`, FS/GS prefixes, or `8B` loads without REX.W, so missing call sites do not establish that an import is unused.
- Registry [call-site analysis](../../../core/relinker/relinker/src/analysis/CallSiteResolver.cpp) does not classify instructions with `F3`, `66`, `F0`, FS/GS prefixes, or `8B` loads without REX.W, so missing call sites do not establish that an import is unused.
- Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) and [prefer `0x400000`](../../../core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp) for their lowest ELF load page 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). Where the console loads an executable that does not require an address is not measured; `0x400000` is the base that executables reading their GOT through `app0/imports.txt` assume (#4013). Relocations are kept, so if that range is taken Windows can relocate the image, possibly above that window; no run has observed this, and from this base an image cannot reach the shared user data page at `0x7ffe0000`, since Windows refuses a `SizeOfImage` above `0x77000000` (#9332). Verified on Windows 11 only.
- [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact sequences from [SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), seen in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument setup and carry-flag error handling. A match needs a decoded instruction boundary and the whole sequence inside the scanned range; other SDK/compiler output is rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults. Three more cover the inlined `__syscall6` wrappers `mount`, `nmount` and `unmount` ([SDK libc/mount.c](https://github.com/ps5-payload-dev/sdk/blob/7116a650a0/libc/mount.c)) in ftpsrv v0.21.1 and websrv v0.35, with numbers 21, 378 and 22 pinned, so a stub carrying another number does not match. None is implemented; a trapped call faults.
- The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) 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.