diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index c53be3987b..fb33a63019 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -526,6 +526,9 @@ if(BUILD_TESTING) add_test(NAME guest_deferred_dependencies COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_deferred_dependencies.py $ $) + add_test(NAME guest_preinit COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_preinit.py + $ $) add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} @@ -539,6 +542,7 @@ if(BUILD_TESTING) add_asan_test_options(guest_module_args verify_asan_link_order=0) add_asan_test_options(guest_deferred_dependencies verify_asan_link_order=0) add_asan_test_options(guest_module_fini verify_asan_link_order=0) + add_asan_test_options(guest_preinit verify_asan_link_order=0) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) set(windows_stdio_host_arguments) diff --git a/core/relinker/domain/include/domain/GuestRuntime.hpp b/core/relinker/domain/include/domain/GuestRuntime.hpp index 5f1e25c734..787c64701a 100644 --- a/core/relinker/domain/include/domain/GuestRuntime.hpp +++ b/core/relinker/domain/include/domain/GuestRuntime.hpp @@ -22,6 +22,7 @@ struct GuestRuntime { std::vector Imports; std::vector InitArrayRvas; std::vector FiniArrayRvas; + std::vector PreInitArrayRvas; std::uint32_t InitRva = 0; std::uint32_t FiniRva = 0; bool UsePlatformTlsResolver = true; diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp index 4f7ec0fee8..597e32dc60 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp @@ -50,6 +50,7 @@ struct MacOsImageInput { std::uint32_t FiniRva = 0; std::vector InitArrayRvas; std::vector FiniArrayRvas; + std::vector PreInitArrayRvas; std::vector TlsModuleSlots; std::vector TlsImports; std::vector TlsExports; diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index d3444f0060..f14ae5031d 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -150,7 +150,31 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma Io::AppendU32(bytes, static_cast(distance)); return start; }; + const auto preinit = [&](std::uint64_t target) { + if (image.PreInitArray.empty()) return std::uint64_t{}; + if (target == 0) throw Domain::RelinkerException("Guest preinit array has no initializer"); + const auto start = address(); + std::vector stub{0x50}; + for (const auto slot : image.PreInitArray) { + const auto load = static_cast(stub.size()); + const auto distance = static_cast(slot) - static_cast(start + static_cast(load) + 7); + if (distance < std::numeric_limits::min() || distance > std::numeric_limits::max()) + throw Domain::RelinkerException("Guest preinit exceeds relative branch range"); + stub.insert(stub.end(), {0x48, 0x8b, 0x05}); + Io::AppendU32(stub, static_cast(static_cast(distance))); + stub.insert(stub.end(), {0x48, 0x85, 0xc0, 0x74, 0x08, 0x31, 0xff, 0x31, 0xf6, 0x31, 0xd2, 0xff, 0xd0}); + } + stub.push_back(0x58); + const auto distance = static_cast(target) - static_cast(start + stub.size() + 5); + if (distance < std::numeric_limits::min() || distance > std::numeric_limits::max()) + throw Domain::RelinkerException("Guest preinit exceeds relative branch range"); + stub.push_back(0xe9); + Io::AppendU32(stub, static_cast(static_cast(distance))); + bytes.insert(bytes.end(), stub.begin(), stub.end()); + return start; + }; const auto init = lifecycle(image.Init); + const auto preinitInit = preinit(init); const auto fini = finalizer(image.Fini); Io::AlignBuffer(bytes, 8); const auto dynamicOffset = bytes.size(); @@ -177,7 +201,8 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma } if (!image.InitArray.empty()) { tag(25, image.InitArray.front()); tag(27, image.InitArray.size() * 8); } if (!image.FiniArray.empty()) { tag(26, image.FiniArray.front()); tag(28, image.FiniArray.size() * 8); } - if (init != 0) tag(12, init); + const auto entry = preinitInit != 0 ? preinitInit : init; + if (entry != 0) tag(12, entry); if (fini != 0) tag(13, fini); tag(30, DF_BIND_NOW | (image.Symbolic ? DF_SYMBOLIC : 0)); tag(0, 0); diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index 74e31d19d3..d251732263 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -109,6 +109,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma input.RunPaths = MachORunPaths(runPath, "@loader_path"); for (const auto slot : guest.InitArray) input.InitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) input.FiniArrayRvas.push_back(image.GetRva(slot, 8)); + for (const auto slot : guest.PreInitArray) input.PreInitArrayRvas.push_back(image.GetRva(slot, 8)); input.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); input.FiniRva = guest.Fini == 0 ? 0 : image.GetRva(guest.Fini); input.Trampolines = &guest.Trampolines; diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 838d0d6c8b..405c3d42fc 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -534,7 +534,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { input.Binds.push_back({"___cxa_atexit", metaRva + static_cast(MetaCxaAtexitSlot)}); } - const bool hasInit = hasTls || !input.TlsImports.empty() || input.InitRva != 0 || !input.InitArrayRvas.empty() || hasFini; + const bool hasInit = hasTls || !input.TlsImports.empty() || input.InitRva != 0 || !input.InitArrayRvas.empty() || !input.PreInitArrayRvas.empty() || hasFini; std::vector
dataSections{{"__meta", 0, metaSize, 0, 3}}; if (hasInit) { const auto initStub = code.GetRva(); @@ -554,6 +554,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { } code.Rip({0x48, 0x89, 0x05}, import.Rva); } + for (const auto slot : input.PreInitArrayRvas) callSlot(slot); if (input.InitRva != 0) { code.Rip({0xff, 0x15}, metaRva + static_cast(moduleArgsSlot)); code.Emit({0x48, 0x8b, 0x38, 0x48, 0x8b, 0x70, 0x08, 0x31, 0xd2}); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index 85fea36fc2..c6ab18ad29 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -155,7 +156,37 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI for (const auto slot : guest.InitArray) Io::AppendU32(lifecycle.Data, image.GetRva(slot, 8)); if (!exports.emplace(Relinker::GuestInitializeExport, nextRva).second) throw Domain::RelinkerException("Duplicate guest export: " + std::string(Relinker::GuestInitializeExport)); nextRva = AlignRva(nextRva + lifecycle.Data.size()); - sections.push_back(std::move(lifecycle)); + if (guest.PreInitArray.empty()) sections.push_back(std::move(lifecycle)); + else { + if (guest.Init == 0) throw Domain::RelinkerException("Guest preinit array has no initializer"); + const auto thunkRva = nextRva; + const auto codeSize = static_cast(40 + 23 * guest.PreInitArray.size()); + const auto slotsRva = static_cast(thunkRva + codeSize + (8 - codeSize % 8) % 8); + WindowsStubEmitter code(thunkRva); + code.Emit({0x41, 0x54, 0x41, 0x55, 0x41, 0x56}); + code.Emit({0x49, 0x89, 0xfc, 0x49, 0x89, 0xf5, 0x49, 0x89, 0xd6}); + for (std::size_t index = 0; index < guest.PreInitArray.size(); ++index) { + code.Rip({0x48, 0x8b, 0x05}, slotsRva + static_cast(index) * 8); + code.Emit({0x48, 0x8b, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x08}); + code.Emit({0x31, 0xff, 0x31, 0xf6, 0x31, 0xd2, 0xff, 0xd0}); + } + code.Emit({0x4c, 0x89, 0xe7, 0x4c, 0x89, 0xee, 0x4c, 0x89, 0xf2}); + code.Rip({0x48, 0x8b, 0x05}, slotsRva + static_cast(guest.PreInitArray.size()) * 8); + code.Emit({0xff, 0xd0}); + code.Emit({0x41, 0x5e, 0x41, 0x5d, 0x41, 0x5c, 0xc3}); + auto thunk = code.TakeBytes(); + while (thunk.size() % 8 != 0) thunk.push_back(0x90); + for (const auto slot : guest.PreInitArray) { + relocations.push_back(CheckedRva(thunkRva + thunk.size())); + Io::AppendU64(thunk, ImageBase + image.GetRva(slot, 8)); + } + relocations.push_back(CheckedRva(thunkRva + thunk.size())); + Io::AppendU64(thunk, ImageBase + image.GetRva(guest.Init, 1)); + Io::WriteU32(lifecycle.Data, 0, thunkRva); + nextRva = AlignRva(nextRva + thunk.size()); + sections.push_back(std::move(lifecycle)); + sections.push_back({".ginitc", thunkRva, SectionRead | SectionExecute | 0x60u, std::move(thunk)}); + } } if (exports.size() > 65535) throw Domain::RelinkerException("Too many guest PE exports"); PeSection exportSection{".edata", nextRva, SectionRead | 0x40u, std::vector(40)}; @@ -202,6 +233,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI const auto entry = nextRva; sections.push_back({".dllmain", entry, SectionRead | SectionExecute | 0x20u, {0xb8, 1, 0, 0, 0, 0xc3}}); for (const auto slot : guest.InitArray) runtime.InitArrayRvas.push_back(image.GetRva(slot, 8)); + for (const auto slot : guest.PreInitArray) runtime.PreInitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) runtime.FiniArrayRvas.push_back(image.GetRva(slot, 8)); runtime.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); runtime.UnmatchedImportModules.assign(guest.Dynamic.UnmatchedImportModules.begin(), guest.Dynamic.UnmatchedImportModules.end()); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp index 8d241798f9..da1513a425 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp @@ -31,6 +31,7 @@ void WindowsGuestStartup::Initialize(WindowsStubEmitter& code, const std::vector for (std::size_t index = 0; index < modules.size(); ++index) { if (modules[index].DeferInitialization) continue; const auto handle = CheckedRva(handles + index * 8); + for (const auto slot : modules[index].PreInitArrayRvas) callLifecycle(code, handle, slot, true); callLifecycle(code, handle, modules[index].InitRva); for (const auto slot : modules[index].InitArrayRvas) callLifecycle(code, handle, slot, true); } diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index da250e22cf..83a5ddece7 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -47,6 +47,7 @@ struct GuestImage { std::vector Symbols; std::vector Dependencies; Domain::SysVDynamicSection Dynamic; + std::vector PreInitArray; std::vector InitArray; std::vector FiniArray; std::uint64_t Init = 0; diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 8d04c05050..304d6362d5 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -358,6 +358,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector entries.push_back(address + offset); } }; + readLifecycleArray(32, 33, image.PreInitArray); readLifecycleArray(25, 27, image.InitArray); readLifecycleArray(26, 28, image.FiniArray); if (tags.contains(33) && tags.at(33) != 0 && image.Init == 0) fail("Guest PREINIT_ARRAY has no module initializer"); diff --git a/core/relinker/relinker/tests/test_guest_preinit.py b/core/relinker/relinker/tests/test_guest_preinit.py new file mode 100644 index 0000000000..c2b2bbc7b5 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_preinit.py @@ -0,0 +1,290 @@ +from pathlib import Path +import os +import platform +import shutil +import struct +import subprocess +import sys +import tempfile + +from test_guest_symbol_names import dynamic_tags, file_offset, imported_symbols, module_symbols +from test_macos_output import MachO +from test_guest_intel_trampolines import main_fixture, pe_sections +from test_windows_import_modules import executable + +ARGS = 0x1122334455667788 +ARGP = 0x99AABBCCDDEEFF00 +RETURNED = 0x5A17 + +GETTER = '__aps5_get_pending_module_args_nid_no_patch' +SETTER = '__aps5_set_module_init_result_nid_no_patch' + +SHARED = 0x1000 +PREINIT0 = 0x1010 +PREINIT1 = 0x1030 +INIT = 0x1050 +RECORD_GOT = 0x2820 +PREINIT_SLOTS = 0x2840 +FLAG = 0x2820 +IMAGE_BASE = 0x140000000 +LOAD_RVA = 0x10000 +CALL_SLOT = b'\x48\x8b\x05' + + +def elf_header(image, program_headers=4): + image[:16] = b'\x7fELF\x02\x01\x01' + bytes(9) + struct.pack_into('= 0: + if data[position + 7:position + 20] == b'\x48\x85\xc0\x74\x08\x31\xff\x31\xf6\x31\xd2\xff\xd0': + displacement, = struct.unpack_from('> 12 == 10: + found.add(page + (entry & 0xfff)) + position += size + assert set(addresses) <= found, (sorted(found), addresses) + + +def check_windows_deferred(data, slots): + sections = {name: (rva, raw_size, raw_offset, flags) for name, rva, raw_size, raw_offset, flags in pe_sections(data)} + ginit_rva, ginit_size, ginit_raw, _ = sections[b'.ginit'] + ginitc_rva, ginitc_size, ginitc_raw, _ = sections[b'.ginitc'] + entry, count = struct.unpack_from('= 0, address + return found + + initializer = position_of(LOAD_RVA + INIT) + for slot in slots: + assert position_of(LOAD_RVA + slot) < initializer, (slot, initializer) + + +def main(): + if sys.argv[1] == '--load': + import ctypes + host = ctypes.CDLL(sys.argv[2], mode=ctypes.RTLD_GLOBAL if sys.platform == 'darwin' else ctypes.DEFAULT_MODE) + host.SetPendingArgs.argtypes = [ctypes.c_size_t, ctypes.c_void_p] + host.SetPendingArgs(ARGS, ARGP) + ctypes.CDLL(sys.argv[3]) + return + relinker = Path(sys.argv[1]).resolve() + host = Path(sys.argv[2]).resolve() + linux = sys.platform == 'linux' and os.uname().machine == 'x86_64' + macos = sys.platform == 'darwin' and platform.machine() == 'x86_64' + with tempfile.TemporaryDirectory(prefix='anyps5-guest-preinit-') as directory: + work = Path(directory) + modules = work / 'sce_module' + modules.mkdir() + name = 'preinit.prx' + (modules / name).write_bytes(recording_module()) + source = work / 'input.elf' + source.write_bytes(executable(name)) + output = work / 'output.elf' + result = subprocess.run([str(relinker), '--skip-syscall-check', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + converted = output.parent / 'app0' / 'sce_module' / (name + '.guest.prx') + image = converted.read_bytes() + assert GETTER in imported_symbols(image), imported_symbols(image) + assert SETTER in imported_symbols(image), imported_symbols(image) + assert 'shared#guest' in module_symbols(image), module_symbols(image) + tags = dynamic_tags(image) + check_linux_stub(image, tags[12], [PREINIT_SLOTS, PREINIT_SLOTS + 8]) + if linux: + libraries = work / 'libs' + libraries.mkdir() + shutil.copyfile(host, libraries / 'libc.prx') + shutil.copyfile(host, libraries / 'libkernel.prx') + load_and_record(work, libraries / 'libc.prx', converted) + mach = work / 'macos' + mach_modules = mach / 'sce_module' + mach_modules.mkdir(parents=True) + (mach_modules / name).write_bytes(recording_module()) + mach_source = mach / 'input.elf' + mach_source.write_bytes(executable(name)) + mach_result = subprocess.run([str(relinker), '--macos', '--skip-syscall-check', str(mach_source), str(mach / 'output')], + capture_output=True, text=True, timeout=30) + assert mach_result.returncode == 0, (mach_result.stdout, mach_result.stderr) + mach_converted = mach / 'app0' / 'sce_module' / (name + '.guest.prx') + check_macos_stub(mach_converted.read_bytes(), [IMAGE_BASE + LOAD_RVA + PREINIT_SLOTS, IMAGE_BASE + LOAD_RVA + PREINIT_SLOTS + 8]) + windows = work / 'windows' + win_modules = windows / 'sce_module' + win_modules.mkdir(parents=True) + (win_modules / name).write_bytes(flag_module()) + win_source = windows / 'input.elf' + win_source.write_bytes(executable(name)) + win_result = subprocess.run([str(relinker), '--windows', '--skip-syscall-check', str(win_source), str(windows / 'output.exe')], + capture_output=True, text=True, timeout=30) + assert win_result.returncode == 0, (win_result.stdout, win_result.stderr) + win_converted = windows / 'app0' / 'sce_module' / (name + '.guest.prx') + check_windows_startup((windows / 'output.exe').read_bytes(), [PREINIT_SLOTS, PREINIT_SLOTS + 8]) + deferred = work / 'deferred' + (deferred / 'sce_module').mkdir(parents=True) + plugins = deferred / 'Media' / 'Plugins' + plugins.mkdir(parents=True) + (plugins / name).write_bytes(flag_module()) + deferred_source = deferred / 'input.elf' + deferred_source.write_bytes(main_fixture()) + deferred_result = subprocess.run([str(relinker), '--windows', '--skip-syscall-check', + '--module-dir', 'Media/Plugins', str(deferred_source), str(deferred / 'output.exe')], + capture_output=True, text=True, timeout=30) + assert deferred_result.returncode == 0, (deferred_result.stdout, deferred_result.stderr) + deferred_converted = deferred / 'app0' / 'Media' / 'Plugins' / (name + '.guest.prx') + check_windows_deferred(deferred_converted.read_bytes(), [PREINIT_SLOTS, PREINIT_SLOTS + 8]) + if os.name == 'nt': + run = subprocess.run([str(windows / 'output.exe')], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (run.returncode, run.stdout, run.stderr) + if linux or macos or os.name == 'nt': + print('Guest preinit tests passed') + else: + print('Guest preinit tests passed: native execution skipped') + + +if __name__ == '__main__': + main() diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 0f0d9ff569..0164e3e37f 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -47,7 +47,7 @@ flowchart LR ## Windows deferred module initialization -Runtime-loaded Windows modules export `__aps5_guest_initialize`: the entry RVA, the initializer count and the RVAs of initializer pointer slots. [DynamicLoader.cpp](../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) consumes the first two words in the mapped image after initialization, so another reference to that image does not run it again. Unloading the image discards this state; a later load reads the original table from the file, including when Windows reuses the same base address. Native references outside the guest handle table retain the same initialized image. +Runtime-loaded Windows modules export `__aps5_guest_initialize`: the entry RVA, the initializer count and the RVAs of initializer pointer slots. [DynamicLoader.cpp](../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) consumes the first two words in the mapped image after initialization, so another reference to that image does not run it again. Unloading the image discards this state; a later load reads the original table from the file, including when Windows reuses the same base address. Native references outside the guest handle table retain the same initialized image. A module with a preinit array exports a thunk as its entry RVA that runs the preinit entries before the module initializer. While initialization runs, the count is `UINT32_MAX` and the entry word holds the initializing Windows thread ID. A different thread waits on the loader's condition variable; the owning thread can recursively open its module. Completion clears both words, restores the table's original page protection and wakes waiters. An exception leaves the count at `UINT32_MAX` with thread ID 0, so another load rejects that image until it is unloaded. The table's file format is unchanged and its initializer slots remain intact. diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 9c5f5bbaa6..370ea5f68e 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -4,6 +4,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. +- Guest `DT_PREINIT_ARRAY` entries are run with `(0, NULL, NULL)` before the module initializer by the [Linux](../../../core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp), [macOS](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp) and [Windows](../../../core/relinker/elfpatcher/src/windows/WindowsGuestStartup.cpp) outputs, since `ld.so` runs a shared object's preinit array only for the main executable; what the console passes to these entries is not measured. A module whose preinit array is non-empty and whose initializer is absent is rejected, because the Linux stub that would run the entries exists only with one. - [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. - 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, so everything it prints is lost, including the entry stub's errors and crash reports (#3282). - [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.