feat(relinker): log deferred Windows modules the executable imports from

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]>
This commit is contained in:
Savvanis SpyrosandClaude Opus 5.5 committed 2026-10-21 18:38:23 +03:00
1 parent cecb540a63
commit 960863f0e2
3 files changed
+25 -1

No files matched your search

@@ -157,6 +157,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con
std::vector<std::uint32_t> searchCounts;
std::vector<bool> platformTlsResolvers;
std::vector<bool> imported(libraries.size());
std::vector<bool> executableImports(guestModules.size());
const bool unscopedImports = std::any_of(imports.begin(), imports.end(), [](const auto& import) { return import.Library.empty(); });
for (const auto& import : imports) {
const auto library = Domain::NeededLibraryName(import.Library);
@@ -167,6 +168,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con
const bool guestName = index < guestModules.size() &&
std::find(guestModules[index].Names.begin(), guestModules[index].Names.end(), import.Library) != guestModules[index].Names.end();
if (import.Library.empty() || name == library || guestName) order.push_back(CheckedRva(index));
if (import.TargetModule < 0 && !import.Library.empty() && index < guestModules.size() && (name == library || guestName)) executableImports[index] = true;
}
if (order.empty()) throw Domain::RelinkerException("Import module is not a dependency: " + import.Library);
const bool hostModule = !import.Library.empty() &&
@@ -244,6 +246,11 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con
const auto searched = addString("Searched libraries:\n");
const auto indent = addString(" ");
const auto enteringElf = addString("Transferring control to ELF entry point\n");
std::vector<std::uint32_t> deferredImports;
for (std::size_t index = 0; index < guestModules.size(); ++index) {
if (guestModules[index].DeferInitialization && executableImports[index])
deferredImports.push_back(addString("Deferred module " + guestModules[index].Path + ": the executable imports from it, but it starts only when the title loads it\n"));
}
std::vector<std::uint32_t> resolvedPaths;
for (std::size_t index = 0; index < libraries.size(); ++index)
resolvedPaths.push_back(reserve(PathCapacity));
@@ -712,6 +719,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con
code.Emit({0x85, 0xc0});
unresolvedBranches.push_back(code.Branch({0x0f, 0x85}));
}
for (const auto message : deferredImports) writeString(message);
guestStartup.Initialize(code, guestModules, handles);
writeString(enteringElf);
code.Rip({0x48, 0x8d, 0x0d}, shellLibrary);
@@ -10,6 +10,7 @@ from test_guest_intel_trampolines import main_fixture, pe_sections
from test_guest_module_directories import module_with_symbol, needed_libraries
from test_guest_needed_modules import executable_with_needed, module_with_needed
from test_macos_output import MachO
from test_windows_import_modules import executable, provider
def pe_export_names(data):
@@ -199,6 +200,21 @@ def main():
else:
assert "$ORIGIN/app0/Media/Modules/needed.prx.guest.prx" in needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes())
for directory, deferred in (("Media/Modules", True), ("sce_module", False)):
case = work / f"{windows}-imported-{deferred}"
(case / directory).mkdir(parents=True)
(case / directory / "needed.prx").write_bytes(provider(42))
result, output = convert(case, windows, ["--module-dir", directory] if deferred else [], executable("needed.prx"))
assert result.returncode == 0, (result.stdout, result.stderr)
line = f"Deferred module app0/{directory}/needed.prx.guest.prx: the executable imports from it, but it starts only when the title loads it\n"
if windows:
assert (line.encode() in output.read_bytes()) == deferred, directory
if os.name == "nt":
run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30)
assert run.returncode == 42 and (line in run.stdout) == deferred, (directory, run.returncode, run.stdout, run.stderr)
else:
assert f"$ORIGIN/app0/{directory}/needed.prx.guest.prx" in needed_libraries(output.read_bytes()), needed_libraries(output.read_bytes())
case = work / f"{windows}-root"
case.mkdir()
(case / "root.prx").write_bytes(module_with_symbol(True))
+1 -1
View File
@@ -5,7 +5,7 @@
- [Executable guest imports](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - 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](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--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.
- [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, and nothing reports it. Linux and macOS still start it at startup.
- [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. Linux and macOS still start 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.
- Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) 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.