From 3292cd39fb8c5d4215fa45d07f6a2acbe9a793d7 Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Mon, 12 Oct 2026 22:51:58 +0100 Subject: [PATCH 1/2] feat(libSceSystemService): relaunch the title for sceSystemServiceLoadExec of its own eboot.bin Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceSystemService/CMakeLists.txt | 5 +- core/libs/prx/libSceSystemService/Export.cpp | 58 ++++++++++++- core/libs/tests/GuestSystemExit.cpp | 87 +++++++++++++++++++ docs/dev/debt/libSceSystemService.md | 1 + 4 files changed, 148 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index b99e85275..9b90ef588 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -12,12 +12,15 @@ if(BUILD_TESTING) add_test(NAME guest_quick_exit COMMAND guest_system_exit_tests --quick-exit) add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) + add_test(NAME guest_system_relaunch COMMAND guest_system_exit_tests --relaunch-driver) set_tests_properties(guest_system_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest shutdown and atexit completed") set_tests_properties(guest_quick_exit PROPERTIES PASS_REGULAR_EXPRESSION "Guest quick_exit handlers completed") + set_tests_properties(guest_system_relaunch PROPERTIES + PASS_REGULAR_EXPRESSION "Guest relaunch completed" TIMEOUT 30) if(WIN32) - set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index fc2b46ac9..c6bba7607 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -1,12 +1,22 @@ +#include #include #include +#include #include #include #include +#include +#include #include #include #include #include +#include +#if defined(_WIN32) +#include +#elif defined(__linux__) +#include +#endif #include "prx/libc/include/Shutdown.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -29,17 +39,61 @@ int ConsoleLanguage() { return language; } +constexpr char OwnExecutable[] = "/app0/eboot.bin"; + +void RelaunchHostExecutable() { + std::fflush(nullptr); +#if defined(_WIN32) + std::wstring executable(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size())); + if (length == 0 || length >= executable.size()) { + throw std::runtime_error("sceSystemServiceLoadExec: GetModuleFileNameW failed with error " + std::to_string(GetLastError())); + } + executable.resize(length); + std::wstring commandLine = GetCommandLineW(); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + if (!CreateProcessW(executable.c_str(), commandLine.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process)) { + throw std::runtime_error("sceSystemServiceLoadExec: CreateProcessW failed with error " + std::to_string(GetLastError())); + } + CloseHandle(process.hThread); + CloseHandle(process.hProcess); +#elif defined(__linux__) + std::ifstream file("/proc/self/cmdline", std::ios::binary); + const std::string commandLine((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + std::vector argv; + std::size_t start = 0; + while (start < commandLine.size()) { + argv.push_back(const_cast(commandLine.c_str() + start)); + const std::size_t end = commandLine.find('\0', start); + if (end == std::string::npos) break; + start = end + 1; + } + if (argv.empty()) throw std::runtime_error("sceSystemServiceLoadExec: empty /proc/self/cmdline"); + argv.push_back(nullptr); + ::execv("/proc/self/exe", argv.data()); + throw std::runtime_error("sceSystemServiceLoadExec: execv failed: " + std::string(std::strerror(errno))); +#else + NotImplemented_nid_no_patch("sceSystemServiceLoadExec: relaunching on this host"); +#endif +} + } extern "C" { int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* arguments) { if (!path || !*path) return SYSTEM_SERVICE_ERROR_PARAMETER; - if (std::strcmp(path, "exit") != 0) { + const bool exit = std::strcmp(path, "exit") == 0; + if (!exit && std::strcmp(path, OwnExecutable) != 0) { NotImplemented_nid_no_patch("sceSystemServiceLoadExec: executable replacement"); } - (void)arguments; + if (!exit && arguments != nullptr && arguments[0] != nullptr) { + NotImplemented_nid_no_patch("sceSystemServiceLoadExec: arguments for the relaunched executable"); + } LibcRunShutdown_nid_postfix(); + if (!exit) RelaunchHostExecutable(); std::exit(0); } diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 6dc172ac0..400691877 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -1,12 +1,21 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/Shutdown.hpp" #include "prx/libSceSystemService/SystemService.hpp" +#include #include #include +#include +#include +#include #include #include +#include +#include #ifdef _WIN32 #include +#else +#include +#include #endif extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*); extern "C" void APS5_VABI _Exit_nid_postfix(int); @@ -33,6 +42,66 @@ void UnexpectedCleanup() { std::_Exit(3); } #ifdef _WIN32 void WINAPI UnexpectedDetach(PVOID) { TerminateProcess(GetCurrentProcess(), 4); } #endif +constexpr char RelaunchMarker[] = "ANYPS5_TEST_RELAUNCH_MARKER"; +constexpr char Relaunched[] = "ANYPS5_TEST_RELAUNCHED"; +void SetVariable(const char* name, const char* value) { +#ifdef _WIN32 + Require(SetEnvironmentVariableA(name, value) != 0); + Require(_putenv_s(name, value) == 0); +#else + Require(setenv(name, value, 1) == 0); +#endif +} +void AppendMarker(const char* line) { + const char* marker = std::getenv(RelaunchMarker); + Require(marker != nullptr); + std::ofstream(marker, std::ios::app) << line << '\n'; +} +void RelaunchedShutdown() { AppendMarker("shutdown"); } +int RunRelaunchingChild(const char* self) { +#ifdef _WIN32 + (void)self; + std::wstring executable(32768, L'\0'); + executable.resize(GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size()))); + std::wstring commandLine = L"\"" + executable + L"\" --relaunch"; + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + Require(CreateProcessW(executable.c_str(), commandLine.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process) != 0); + WaitForSingleObject(process.hProcess, INFINITE); + DWORD code = 1; + GetExitCodeProcess(process.hProcess, &code); + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return static_cast(code); +#else + const pid_t child = fork(); + Require(child >= 0); + if (child == 0) { + execl(self, self, "--relaunch", static_cast(nullptr)); + std::_Exit(127); + } + int status = 0; + Require(waitpid(child, &status, 0) == child); + return WIFEXITED(status) ? WEXITSTATUS(status) : 1; +#endif +} +int DriveRelaunch(const char* self) { + const auto marker = std::filesystem::temp_directory_path() / ("anyps5-relaunch-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + SetVariable(RelaunchMarker, marker.string().c_str()); + Require(RunRelaunchingChild(self) == 0); + std::string text; + for (int attempt = 0; attempt < 200 && text.find("relaunched") == std::string::npos; ++attempt) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + std::ifstream file(marker); + text.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + std::error_code ignored; + std::filesystem::remove(marker, ignored); + Require(text == "shutdown\nrelaunched\n"); + std::puts("Guest relaunch completed"); + return 0; +} } int main(int argc, char** argv) { if (argc > 1 && std::strcmp(argv[1], "--immediate") == 0) { @@ -55,6 +124,19 @@ int main(int argc, char** argv) { quick_exit_nid_postfix(0); return 2; } + if (argc > 1 && std::strcmp(argv[1], "--relaunch-driver") == 0) { + return DriveRelaunch(argv[0]); + } + if (argc > 1 && std::strcmp(argv[1], "--relaunch") == 0) { + if (std::getenv(Relaunched) != nullptr) { + AppendMarker("relaunched"); + return 0; + } + LibcRegisterShutdown_nid_postfix(RelaunchedShutdown); + SetVariable(Relaunched, "1"); + sceSystemServiceLoadExec("/app0/eboot.bin", nullptr); + return 2; + } if (argc > 1) { LibcRegisterShutdown_nid_postfix(Cleanup); Require(std::atexit(VerifyExit) == 0); @@ -67,4 +149,9 @@ int main(int argc, char** argv) { try { sceSystemServiceLoadExec("/app0/another.bin", nullptr); } catch (const std::runtime_error&) { rejected = true; } Require(rejected); + rejected = false; + const char* const arguments[] = {"--mode", nullptr}; + try { sceSystemServiceLoadExec("/app0/eboot.bin", arguments); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected); } diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 9ff34f9ba..052211ca0 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -14,4 +14,5 @@ ### Functional +- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, only the title's own `/app0/eboot.bin` without arguments is loaded: after the guest shutdown the converted executable is started again with the host command line it was started with (`CreateProcessW` on Windows, `execv` of `/proc/self/exe` on Linux), and the old process exits with 0 without waiting, so its direct memory is released before the new one allocates. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself this way to see newly mounted drives and expects no return. On Windows the new process is a separate process, so a shell or script that started the title sees it end. Other executables, non-empty argument lists and macOS hosts throw - [Running application ID](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. From 99efb32c404f6ce17afb8800b101b6ff0acb1488 Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Tue, 13 Oct 2026 08:51:50 +0100 Subject: [PATCH 2/2] feat: convert a title's other executables and start them with sceSystemServiceLoadExec The relinker's repeatable --executable converts another executable of the title beside the output, sharing its libs, app0 and guest modules. sceSystemServiceLoadExec maps /app0 paths to those executables, passes the argument list on the new command line and finds the main executable through APS5_EBOOT. RetroArch (PPSA99169) starts its frontend picker this way. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceSystemService/Export.cpp | 182 ++++++++++--- core/libs/tests/GuestSystemExit.cpp | 85 ++++--- core/relinker/CMakeLists.txt | 2 + core/relinker/cli/include/Cli.hpp | 1 + core/relinker/cli/src/CliArgs.cpp | 8 +- core/relinker/cli/tests/CliArgsTests.cpp | 10 + core/relinker/main.cpp | 240 ++++++++++++------ .../relinker/tests/test_executables.py | 76 ++++++ docs/dev/debt/libSceSystemService.md | 2 +- docs/user/USAGE.md | 1 + 10 files changed, 455 insertions(+), 152 deletions(-) create mode 100644 core/relinker/relinker/tests/test_executables.py diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index c6bba7607..136c434f1 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -5,8 +5,7 @@ #include #include #include -#include -#include +#include #include #include #include @@ -39,45 +38,153 @@ int ConsoleLanguage() { return language; } -constexpr char OwnExecutable[] = "/app0/eboot.bin"; - -void RelaunchHostExecutable() { - std::fflush(nullptr); +constexpr char MainExecutable[] = "/app0/eboot.bin"; +constexpr char ApplicationRoot[] = "/app0/"; +constexpr char MainExecutableVariable[] = "APS5_EBOOT"; #if defined(_WIN32) - std::wstring executable(32768, L'\0'); - const DWORD length = GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size())); - if (length == 0 || length >= executable.size()) { +constexpr wchar_t MainExecutableVariableWide[] = L"APS5_EBOOT"; +#endif + +[[noreturn]] void UnconvertedExecutable(const std::string& message) { + NotImplemented_nid_no_patch(("sceSystemServiceLoadExec: " + message).c_str()); + std::abort(); +} + +std::string ConvertedExecutableName(const char* path) { + std::string_view relative(path); + if (!relative.starts_with(ApplicationRoot)) UnconvertedExecutable(std::string("executables outside /app0 (") + path + ")"); + relative.remove_prefix(std::size(ApplicationRoot) - 1); + std::string name; + while (true) { + const std::size_t separator = relative.find('/'); + const std::string_view component = relative.substr(0, separator); + if (component.empty() || component == "." || component == "..") UnconvertedExecutable(std::string("a path with an empty, . or .. component (") + path + ")"); + name += (name.empty() ? "" : ".") + std::string(component); + if (separator == std::string_view::npos) return name; + relative.remove_prefix(separator + 1); + } +} + +#if defined(_WIN32) +std::wstring Utf16(const std::string& text) { + if (text.empty()) return {}; + const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), nullptr, 0); + if (size <= 0) throw std::runtime_error("sceSystemServiceLoadExec: argument is not valid UTF-8"); + std::wstring wide(static_cast(size), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), wide.data(), size); + return wide; +} + +std::wstring QuotedArgument(const std::wstring& argument) { + if (!argument.empty() && argument.find_first_of(L" \t\n\v\"") == std::wstring::npos) return argument; + std::wstring quoted = L"\""; + for (std::size_t index = 0;; ++index) { + std::size_t backslashes = 0; + while (index < argument.size() && argument[index] == L'\\') { + ++backslashes; + ++index; + } + if (index == argument.size()) { + quoted.append(backslashes * 2, L'\\'); + break; + } + quoted.append(argument[index] == L'"' ? backslashes * 2 + 1 : backslashes, L'\\'); + quoted.push_back(argument[index]); + } + quoted.push_back(L'"'); + return quoted; +} + +struct HostLaunch { + std::wstring executable; + std::wstring commandLine; +}; + +HostLaunch PrepareLaunch(const char* path, const char* const* arguments) { + std::wstring own(32768, L'\0'); + const DWORD length = GetModuleFileNameW(nullptr, own.data(), static_cast(own.size())); + if (length == 0 || length >= own.size()) { throw std::runtime_error("sceSystemServiceLoadExec: GetModuleFileNameW failed with error " + std::to_string(GetLastError())); } - executable.resize(length); - std::wstring commandLine = GetCommandLineW(); + own.resize(length); + HostLaunch launch; + const std::filesystem::path ownPath(own); + if (std::strcmp(path, MainExecutable) == 0) { + std::wstring main(32768, L'\0'); + const DWORD mainLength = GetEnvironmentVariableW(MainExecutableVariableWide, main.data(), static_cast(main.size())); + launch.executable = mainLength > 0 && mainLength < main.size() ? main.substr(0, mainLength) : own; + } else { + const auto target = ownPath.parent_path() / (Utf16(ConvertedExecutableName(path)) + ownPath.extension().wstring()); + if (!std::filesystem::is_regular_file(target)) { + UnconvertedExecutable(std::string(path) + " without its converted executable; relink with --executable " + (path + std::size(ApplicationRoot) - 1)); + } + launch.executable = target.wstring(); + } + if (GetEnvironmentVariableW(MainExecutableVariableWide, nullptr, 0) == 0 && !SetEnvironmentVariableW(MainExecutableVariableWide, own.c_str())) { + throw std::runtime_error(std::string("sceSystemServiceLoadExec: cannot set ") + MainExecutableVariable); + } + launch.commandLine = L"\"" + launch.executable + L"\""; + for (std::size_t index = 0; arguments != nullptr && arguments[index] != nullptr; ++index) { + launch.commandLine += L" " + QuotedArgument(Utf16(arguments[index])); + } + return launch; +} + +void StartHostExecutable(HostLaunch& launch) { STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; - if (!CreateProcessW(executable.c_str(), commandLine.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process)) { + if (!CreateProcessW(launch.executable.c_str(), launch.commandLine.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process)) { throw std::runtime_error("sceSystemServiceLoadExec: CreateProcessW failed with error " + std::to_string(GetLastError())); } CloseHandle(process.hThread); CloseHandle(process.hProcess); -#elif defined(__linux__) - std::ifstream file("/proc/self/cmdline", std::ios::binary); - const std::string commandLine((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - std::vector argv; - std::size_t start = 0; - while (start < commandLine.size()) { - argv.push_back(const_cast(commandLine.c_str() + start)); - const std::size_t end = commandLine.find('\0', start); - if (end == std::string::npos) break; - start = end + 1; - } - if (argv.empty()) throw std::runtime_error("sceSystemServiceLoadExec: empty /proc/self/cmdline"); - argv.push_back(nullptr); - ::execv("/proc/self/exe", argv.data()); - throw std::runtime_error("sceSystemServiceLoadExec: execv failed: " + std::string(std::strerror(errno))); -#else - NotImplemented_nid_no_patch("sceSystemServiceLoadExec: relaunching on this host"); -#endif } +#elif defined(__linux__) +struct HostLaunch { + std::string executable; + std::vector arguments; +}; + +HostLaunch PrepareLaunch(const char* path, const char* const* arguments) { + std::error_code error; + const auto own = std::filesystem::read_symlink("/proc/self/exe", error); + if (error) throw std::runtime_error("sceSystemServiceLoadExec: cannot read /proc/self/exe: " + error.message()); + HostLaunch launch; + if (std::strcmp(path, MainExecutable) == 0) { + const char* main = std::getenv(MainExecutableVariable); + launch.executable = main != nullptr && *main != '\0' ? main : own.string(); + } else { + const auto target = own.parent_path() / (ConvertedExecutableName(path) + own.extension().string()); + if (!std::filesystem::is_regular_file(target)) { + UnconvertedExecutable(std::string(path) + " without its converted executable; relink with --executable " + (path + std::size(ApplicationRoot) - 1)); + } + launch.executable = target.string(); + } + if (setenv(MainExecutableVariable, own.c_str(), 0) != 0) { + throw std::runtime_error(std::string("sceSystemServiceLoadExec: cannot set ") + MainExecutableVariable); + } + launch.arguments.push_back(launch.executable); + for (std::size_t index = 0; arguments != nullptr && arguments[index] != nullptr; ++index) launch.arguments.emplace_back(arguments[index]); + return launch; +} + +void StartHostExecutable(HostLaunch& launch) { + std::vector argv; + for (auto& argument : launch.arguments) argv.push_back(argument.data()); + argv.push_back(nullptr); + ::execv(launch.executable.c_str(), argv.data()); + throw std::runtime_error("sceSystemServiceLoadExec: execv failed: " + std::string(std::strerror(errno))); +} +#else +struct HostLaunch {}; + +HostLaunch PrepareLaunch(const char*, const char* const*) { + UnconvertedExecutable("starting an executable on this host"); +} + +void StartHostExecutable(HostLaunch&) {} +#endif } @@ -85,16 +192,15 @@ extern "C" { int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* arguments) { if (!path || !*path) return SYSTEM_SERVICE_ERROR_PARAMETER; - const bool exit = std::strcmp(path, "exit") == 0; - if (!exit && std::strcmp(path, OwnExecutable) != 0) { - NotImplemented_nid_no_patch("sceSystemServiceLoadExec: executable replacement"); - } - if (!exit && arguments != nullptr && arguments[0] != nullptr) { - NotImplemented_nid_no_patch("sceSystemServiceLoadExec: arguments for the relaunched executable"); + if (std::strcmp(path, "exit") == 0) { + LibcRunShutdown_nid_postfix(); + std::exit(0); } + auto launch = PrepareLaunch(path, arguments); LibcRunShutdown_nid_postfix(); - if (!exit) RelaunchHostExecutable(); - std::exit(0); + std::fflush(nullptr); + StartHostExecutable(launch); + LibcTerminate_nid_no_patch(0); } int APS5_VABI sceSystemServiceDisableNoticeScreenSkipFlagAutoSet(void) { diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 400691877..f0d40600b 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -43,7 +43,7 @@ void UnexpectedCleanup() { std::_Exit(3); } void WINAPI UnexpectedDetach(PVOID) { TerminateProcess(GetCurrentProcess(), 4); } #endif constexpr char RelaunchMarker[] = "ANYPS5_TEST_RELAUNCH_MARKER"; -constexpr char Relaunched[] = "ANYPS5_TEST_RELAUNCHED"; +constexpr char MainExecutable[] = "ANYPS5_TEST_MAIN_EXECUTABLE"; void SetVariable(const char* name, const char* value) { #ifdef _WIN32 Require(SetEnvironmentVariableA(name, value) != 0); @@ -52,22 +52,29 @@ void SetVariable(const char* name, const char* value) { Require(setenv(name, value, 1) == 0); #endif } -void AppendMarker(const char* line) { +std::filesystem::path OwnExecutable() { +#ifdef _WIN32 + std::wstring executable(32768, L'\0'); + executable.resize(GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size()))); + return executable; +#else + return std::filesystem::read_symlink("/proc/self/exe"); +#endif +} +void AppendMarker(const std::string& line) { const char* marker = std::getenv(RelaunchMarker); Require(marker != nullptr); std::ofstream(marker, std::ios::app) << line << '\n'; } -void RelaunchedShutdown() { AppendMarker("shutdown"); } -int RunRelaunchingChild(const char* self) { +void MainShutdown() { AppendMarker("shutdown"); } +void SecondaryShutdown() { AppendMarker("secondary shutdown"); } +int RunRelaunchingChild(const std::filesystem::path& self) { #ifdef _WIN32 - (void)self; - std::wstring executable(32768, L'\0'); - executable.resize(GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size()))); - std::wstring commandLine = L"\"" + executable + L"\" --relaunch"; + std::wstring commandLine = L"\"" + self.wstring() + L"\" --relaunch"; STARTUPINFOW startup{}; startup.cb = sizeof(startup); PROCESS_INFORMATION process{}; - Require(CreateProcessW(executable.c_str(), commandLine.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process) != 0); + Require(CreateProcessW(self.c_str(), commandLine.data(), nullptr, nullptr, TRUE, 0, nullptr, nullptr, &startup, &process) != 0); WaitForSingleObject(process.hProcess, INFINITE); DWORD code = 1; GetExitCodeProcess(process.hProcess, &code); @@ -78,7 +85,7 @@ int RunRelaunchingChild(const char* self) { const pid_t child = fork(); Require(child >= 0); if (child == 0) { - execl(self, self, "--relaunch", static_cast(nullptr)); + execl(self.c_str(), self.c_str(), "--relaunch", static_cast(nullptr)); std::_Exit(127); } int status = 0; @@ -86,9 +93,13 @@ int RunRelaunchingChild(const char* self) { return WIFEXITED(status) ? WEXITSTATUS(status) : 1; #endif } -int DriveRelaunch(const char* self) { +int DriveRelaunch() { + const auto self = OwnExecutable(); + const auto secondary = self.parent_path() / ("picker.picker.bin" + self.extension().string()); + std::filesystem::copy_file(self, secondary, std::filesystem::copy_options::overwrite_existing); const auto marker = std::filesystem::temp_directory_path() / ("anyps5-relaunch-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); SetVariable(RelaunchMarker, marker.string().c_str()); + SetVariable(MainExecutable, self.string().c_str()); Require(RunRelaunchingChild(self) == 0); std::string text; for (int attempt = 0; attempt < 200 && text.find("relaunched") == std::string::npos; ++attempt) { @@ -98,7 +109,9 @@ int DriveRelaunch(const char* self) { } std::error_code ignored; std::filesystem::remove(marker, ignored); - Require(text == "shutdown\nrelaunched\n"); + for (int attempt = 0; attempt < 100 && std::filesystem::exists(secondary) && !std::filesystem::remove(secondary, ignored); ++attempt) + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(text == "shutdown\narguments: --secondary|two words|quote\"d\\|\nsecondary shutdown\nrelaunched\n"); std::puts("Guest relaunch completed"); return 0; } @@ -125,18 +138,30 @@ int main(int argc, char** argv) { return 2; } if (argc > 1 && std::strcmp(argv[1], "--relaunch-driver") == 0) { - return DriveRelaunch(argv[0]); + return DriveRelaunch(); } if (argc > 1 && std::strcmp(argv[1], "--relaunch") == 0) { - if (std::getenv(Relaunched) != nullptr) { - AppendMarker("relaunched"); - return 0; - } - LibcRegisterShutdown_nid_postfix(RelaunchedShutdown); - SetVariable(Relaunched, "1"); - sceSystemServiceLoadExec("/app0/eboot.bin", nullptr); + LibcRegisterShutdown_nid_postfix(MainShutdown); + const char* const arguments[] = {"--secondary", "two words", "quote\"d\\", "", nullptr}; + sceSystemServiceLoadExec("/app0/picker/picker.bin", arguments); return 2; } + if (argc > 1 && std::strcmp(argv[1], "--secondary") == 0) { + Require(OwnExecutable().filename().string().starts_with("picker.picker.bin")); + std::string received = "arguments:"; + for (int index = 1; index < argc; ++index) received += std::string(index == 1 ? " " : "|") + argv[index]; + AppendMarker(received); + LibcRegisterShutdown_nid_postfix(SecondaryShutdown); + const char* const arguments[] = {"--relaunched", nullptr}; + sceSystemServiceLoadExec("/app0/eboot.bin", arguments); + return 2; + } + if (argc > 1 && std::strcmp(argv[1], "--relaunched") == 0) { + const char* main = std::getenv(MainExecutable); + Require(main != nullptr && std::filesystem::equivalent(OwnExecutable(), main)); + AppendMarker("relaunched"); + return 0; + } if (argc > 1) { LibcRegisterShutdown_nid_postfix(Cleanup); Require(std::atexit(VerifyExit) == 0); @@ -145,13 +170,15 @@ int main(int argc, char** argv) { } Require(sceSystemServiceLoadExec(nullptr, nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); Require(sceSystemServiceLoadExec("", nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); - bool rejected = false; - try { sceSystemServiceLoadExec("/app0/another.bin", nullptr); } - catch (const std::runtime_error&) { rejected = true; } - Require(rejected); - rejected = false; - const char* const arguments[] = {"--mode", nullptr}; - try { sceSystemServiceLoadExec("/app0/eboot.bin", arguments); } - catch (const std::runtime_error&) { rejected = true; } - Require(rejected); + for (const char* path : {"/app0/another.bin", "/data/eboot.bin", "/app0/../eboot.bin", "/app0//eboot.bin"}) { + bool rejected = false; + try { sceSystemServiceLoadExec(path, nullptr); } + catch (const std::runtime_error&) { rejected = true; } + Require(rejected); + } +#ifdef _WIN32 + Require(GetEnvironmentVariableA("APS5_EBOOT", nullptr, 0) == 0); +#else + Require(std::getenv("APS5_EBOOT") == nullptr); +#endif } diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 08cb9c0c9..5c6d5c793 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -440,6 +440,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_dir.py $) add_test(NAME guest_module_directories COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_directories.py $) + add_test(NAME relinker_executables COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executables.py $) add_test(NAME sce_module_path COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_path.py $) add_test(NAME linux_guest_stack COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 7faa5353b..96df4f521 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -29,6 +29,7 @@ struct Args { std::string runPath = "$ORIGIN/libs"; std::set excludedSceModules; std::vector moduleDirectories; + std::vector executables; }; Args ParseArgs(int argc, char* argv[]); diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 21d055d6b..fd896e80e 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -8,7 +8,7 @@ namespace Cli { const char* Usage() { - return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" + return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" "Example: relinker input.elf output.elf"; } @@ -40,6 +40,9 @@ Args ParseArgs(int argc, char* argv[]) { if (i + 1 >= argc) throw std::runtime_error("--module-dir requires a directory"); args.moduleDirectories.push_back(argv[++i]); + } else if (arg == "--executable") { + if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--")) throw std::runtime_error("--executable requires a nonempty path"); + args.executables.push_back(argv[++i]); } else if (arg == "--to-intel") { args.toIntel = true; } else if (arg == "--to-rosetta") { @@ -92,6 +95,9 @@ Args ParseArgs(int argc, char* argv[]) { if (args.skipSceModule && !args.moduleDirectories.empty()) throw std::runtime_error("--module-dir conflicts with --skip-sce-module"); + if (args.skipSceModule && !args.executables.empty()) + throw std::runtime_error("--executable conflicts with --skip-sce-module"); + if (args.toWindows && args.toMacos) throw std::runtime_error("--windows conflicts with --macos"); if (args.toRosetta && args.toWindows) diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 3cc339e55..5265776a3 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -133,6 +133,15 @@ void excludeConflictsWithSkip() { expectThrows({"--exclude-sce-module", "m", "--skip-sce-module", "input.elf", "output.elf"}, message); } +void executables() { + const auto args = parse({"--executable", "picker/picker.bin", "input.elf", "--executable", "es-de/es-de.bin", "output.elf"}); + require(args.executables == std::vector{"picker/picker.bin", "es-de/es-de.bin"}, "--executable paths were not recorded in order"); + expectThrows({"--executable"}, "--executable requires a nonempty path"); + expectThrows({"--executable", "", "input.elf", "output.elf"}, "--executable requires a nonempty path"); + expectThrows({"--executable", "--registry", "input.elf", "output.elf"}, "--executable requires a nonempty path"); + expectThrows({"--skip-sce-module", "--executable", "picker.bin", "input.elf", "output.elf"}, "--executable conflicts with --skip-sce-module"); +} + void unusedFilter() { for (const auto& value : {std::string("0"), std::string("1"), std::string("2")}) { const auto args = parse({"unused-filter=" + value, "input.elf", "output.elf"}); @@ -213,6 +222,7 @@ int main() { excludeSceModuleConsumesNextTokenVerbatim(); excludeSceModuleWithoutValue(); excludeConflictsWithSkip(); + executables(); unusedFilter(); unusedFilterRejectsBadValues(); unusedFilterRejectsDuplicates(); diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index f67db4aa0..35b283b63 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -38,6 +38,102 @@ namespace { +struct ConvertedExecutable { + std::string InputPath; + std::string OutputPath; + std::vector Source; + std::vector Trampolines; + Relinker::RelinkResult Result; + std::vector GuestArtifacts; + std::vector Bytes; +}; + +ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable) { + Io::FileReader fileReader; + auto sourceBytes = fileReader.Read(inputPath); + + std::vector trampolines; + if (args.toIntel) { + const auto segments = Relinker::ElfReader(sourceBytes).ReadProgramHeaders(); + auto converted = Codegen::MakeAmd64OnlyConverter(args.toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(sourceBytes), segments); + sourceBytes = std::move(converted.Bytes); + trampolines = std::move(converted.Trampolines); + std::map stubsByName; + const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; + for (const auto& report : converted.Reports) { + if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) + std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; + else if (counted(report.InstructionName)) + ++stubsByName[report.InstructionName]; + else + std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n"; + } + for (const auto& [name, count] : stubsByName) + std::cout << "Intel substitution: " << name << " -> stub at " << count << " sites\n"; + std::cout << "Intel conversion: " << converted.ReplacedCount << " in place, " << trampolines.size() << " stubs, " << converted.KeptCount << " kept\n"; + } + + auto elfReader = std::make_shared(sourceBytes); + const std::shared_ptr syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(args.trapSdkSyscallStubs); + + const auto pipeline = std::make_shared( + elfReader, + syscallScanner, + Relinker::MakeCallSiteResolver(), + std::make_shared(), + std::make_shared(), + args.unusedFilterLevel == 2 ? Relinker::MakeStrictUnusedNidFilter() : Relinker::MakeUnusedNidFilter(), + args.unusedFilterLevel + ); + + if (mainExecutable) { + std::cout << "System: " << (args.toWindows ? "Windows" : args.toMacos ? "macOS" : "Linux") << "; unused-filter=" << args.unusedFilterLevel << "\n"; + std::cout << Relinker::GuestModulePattern << " processing: " << (args.skipSceModule ? "disabled (--skip-sce-module)" : "enabled") << '\n'; + for (const auto& name : args.excludedSceModules) std::cout << "Guest module excluded: " << name << '\n'; + } + auto result = pipeline->Relink(sourceBytes); + for (const auto& patch : result.Patches) { + if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset) + throw Domain::RelinkerException("Relinker patch exceeds source image", patch.Offset); + for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index]; + } + + std::vector guestArtifacts; + if (!args.skipSceModule) { + guestArtifacts = Relinker::GuestModuleBuilder().Build(Io::NativePath(inputPath), Io::NativePath(absPath), result.DynamicSection, args.toWindows, args.toMacos, args.toIntel, args.toRosetta, *syscallScanner, args.lazyBinding, args.runPath, args.excludedSceModules, Io::NativePath(args.sceModulePath), args.moduleDirectories); + } + + return {inputPath, absPath, std::move(sourceBytes), std::move(trampolines), std::move(result), std::move(guestArtifacts), {}}; +} + +void Patch(const Cli::Args& args, ConvertedExecutable& converted, const bool mainExecutable) { + auto byteWriter = std::make_shared(); + + std::shared_ptr patcher; + if (args.toMacos) { + patcher = std::make_shared(); + } else if (args.toWindows) { + patcher = std::make_shared(args.windowsGui, Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png"); + } else { + patcher = std::make_shared( + std::make_shared(), + std::make_shared( + std::make_shared(), + byteWriter + ), + std::make_shared(byteWriter), + byteWriter + ); + } + + try { + converted.Bytes = patcher->Patch(converted.Source, converted.Result.OriginalHeaders, converted.Result.DynamicSection, converted.Result.OriginalPltGotVaddr, args.runPath, args.lazyBinding, args.windowsDiagnostics && mainExecutable, converted.Trampolines); + } catch (Domain::RelinkerException& error) { + error.InputPath = converted.InputPath; + throw; + } +} + int Run(const int argc, char* argv[]) { Cli::Args args; try { @@ -56,74 +152,72 @@ int Run(const int argc, char* argv[]) { auto extension = Io::Utf8Path(Io::NativePath(args.outputPath).extension()); for (auto& character : extension) if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); if (!args.toWindows && !args.toMacos && extension == ".exe") std::cerr << "WARNING: Output filename ends with .exe, but --windows was not specified. The output will be a Linux ELF executable.\n"; - Io::FileReader fileReader; Io::FileWriter fileWriter; - auto sourceBytes = fileReader.Read(args.inputPath); - if (std::filesystem::exists(Io::NativePath(args.outputPath)) && std::filesystem::equivalent(Io::NativePath(args.inputPath), Io::NativePath(args.outputPath))) + if (std::filesystem::exists(Io::NativePath(args.inputPath)) && std::filesystem::exists(Io::NativePath(args.outputPath)) && std::filesystem::equivalent(Io::NativePath(args.inputPath), Io::NativePath(args.outputPath))) throw Domain::RelinkerException("Executable output would overwrite the input executable"); const std::string absPath = Io::Utf8Path(std::filesystem::absolute(Io::NativePath(args.outputPath))); - std::vector trampolines; - if (args.toIntel) { - const auto segments = Relinker::ElfReader(sourceBytes).ReadProgramHeaders(); - auto converted = Codegen::MakeAmd64OnlyConverter(args.toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(sourceBytes), segments); - sourceBytes = std::move(converted.Bytes); - trampolines = std::move(converted.Trampolines); - std::map stubsByName; - const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; - for (const auto& report : converted.Reports) { - if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) - std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; - else if (counted(report.InstructionName)) - ++stubsByName[report.InstructionName]; - else - std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n"; + struct ExtraExecutable { + std::string Argument; + std::filesystem::path Relative; + std::filesystem::path Input; + std::filesystem::path Output; + }; + std::vector extraExecutables; + const auto outputDirectory = Io::NativePath(absPath).parent_path(); + const auto moduleRoot = args.executables.empty() ? std::filesystem::path() : std::filesystem::absolute(Io::NativePath(args.sceModulePath)).lexically_normal(); + std::vector moduleDirectories; + for (const auto* name : {"sce_module", "sce_modules", "prx"}) moduleDirectories.push_back(moduleRoot / name); + for (const auto& name : args.moduleDirectories) moduleDirectories.push_back((moduleRoot / Io::NativePath(name)).lexically_normal()); + for (auto& directory : moduleDirectories) if (!directory.has_filename()) directory = directory.parent_path(); + for (const auto& executable : args.executables) { + const auto relative = Io::NativePath(executable).lexically_normal(); + if (relative.empty() || relative.has_root_path() || *relative.begin() == ".." || !relative.has_filename()) + throw Domain::RelinkerException("--executable must be a file inside the guest module parent directory: " + executable); + const auto input = moduleRoot / relative; + if (!std::filesystem::is_regular_file(input)) throw Domain::RelinkerException("--executable is not a file: " + Io::Utf8Path(input)); + if (std::find(moduleDirectories.begin(), moduleDirectories.end(), input.parent_path()) != moduleDirectories.end()) + throw Domain::RelinkerException("--executable is in a guest module directory: " + executable); + std::string name; + for (const auto& part : relative) name += (name.empty() ? "" : ".") + Io::Utf8Path(part); + const auto output = outputDirectory / Io::NativePath(name + Io::Utf8Path(Io::NativePath(absPath).extension())); + if (std::filesystem::equivalent(Io::NativePath(args.inputPath), input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + if (output.lexically_normal() == Io::NativePath(absPath).lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); + for (const auto& extra : extraExecutables) { + if (std::filesystem::equivalent(extra.Input, input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + if (extra.Output.lexically_normal() == output.lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); } - for (const auto& [name, count] : stubsByName) - std::cout << "Intel substitution: " << name << " -> stub at " << count << " sites\n"; - std::cout << "Intel conversion: " << converted.ReplacedCount << " in place, " << trampolines.size() << " stubs, " << converted.KeptCount << " kept\n"; + extraExecutables.push_back({executable, relative, input, output}); } - auto elfReader = std::make_shared(sourceBytes); - const std::shared_ptr syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(args.trapSdkSyscallStubs); - - const auto pipeline = std::make_shared( - elfReader, - syscallScanner, - Relinker::MakeCallSiteResolver(), - std::make_shared(), - std::make_shared(), - args.unusedFilterLevel == 2 ? Relinker::MakeStrictUnusedNidFilter() : Relinker::MakeUnusedNidFilter(), - args.unusedFilterLevel - ); - - std::cout << "System: " << (args.toWindows ? "Windows" : args.toMacos ? "macOS" : "Linux") << "; unused-filter=" << args.unusedFilterLevel << "\n"; - std::cout << Relinker::GuestModulePattern << " processing: " << (args.skipSceModule ? "disabled (--skip-sce-module)" : "enabled") << '\n'; - for (const auto& name : args.excludedSceModules) std::cout << "Guest module excluded: " << name << '\n'; - auto result = pipeline->Relink(sourceBytes); - for (const auto& patch : result.Patches) { - if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset) - throw Domain::RelinkerException("Relinker patch exceeds source image", patch.Offset); - for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index]; - } - - std::vector guestArtifacts; - if (!args.skipSceModule) { - guestArtifacts = Relinker::GuestModuleBuilder().Build(Io::NativePath(args.inputPath), Io::NativePath(absPath), result.DynamicSection, args.toWindows, args.toMacos, args.toIntel, args.toRosetta, *syscallScanner, args.lazyBinding, args.runPath, args.excludedSceModules, Io::NativePath(args.sceModulePath), args.moduleDirectories); + std::vector executables; + executables.push_back(Convert(args, args.inputPath, absPath, true)); + for (const auto& [executable, relative, input, output] : extraExecutables) { + std::cout << "Executable: " << relative.generic_string() << '\n'; + auto converted = Convert(args, Io::Utf8Path(input), Io::Utf8Path(output), false); + const auto& mainArtifacts = executables.front().GuestArtifacts; + const bool shared = converted.GuestArtifacts.size() == mainArtifacts.size() && std::equal(mainArtifacts.begin(), mainArtifacts.end(), converted.GuestArtifacts.begin(), [](const auto& left, const auto& right) { + return left.Path == right.Path && left.Bytes == right.Bytes; + }); + if (!shared) throw Domain::RelinkerException("Guest modules convert differently for " + executable + ", so it cannot share them with the input executable"); + executables.push_back(std::move(converted)); } + const auto& guestArtifacts = executables.front().GuestArtifacts; const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); const auto checkOutputInputs = [&](const std::filesystem::path& path, const char* kind) { if (!std::filesystem::exists(path)) return; - if (std::filesystem::equivalent(Io::NativePath(args.inputPath), path)) - throw Domain::RelinkerException(std::string(kind) + " output would overwrite the input executable: " + Io::Utf8Path(path)); + for (const auto& executable : executables) + if (std::filesystem::equivalent(Io::NativePath(executable.InputPath), path)) + throw Domain::RelinkerException(std::string(kind) + " output would overwrite the input executable: " + Io::Utf8Path(path)); for (const auto& artifact : guestArtifacts) if (std::filesystem::equivalent(artifact.SourcePath, path)) throw Domain::RelinkerException(std::string(kind) + " output would overwrite an input module: " + Io::Utf8Path(path)); }; + for (std::size_t index = 1; index < executables.size(); ++index) checkOutputInputs(Io::NativePath(executables[index].OutputPath), "Executable"); if (desktopEntry) { checkOutputInputs(desktopEntry->IconPath, "Icon"); checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); @@ -133,56 +227,36 @@ int Run(const int argc, char* argv[]) { std::vector registryPaths; if (args.writeRegistry) { + for (const auto& executable : executables) { + const std::filesystem::path outFsPath = Io::NativePath(executable.OutputPath); + registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); + } const std::filesystem::path outFsPath = Io::NativePath(absPath); - registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); for (const auto& artifact : guestArtifacts) registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json")); for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); } - auto byteWriter = std::make_shared(); - - std::shared_ptr patcher; - if (args.toMacos) { - patcher = std::make_shared(); - } else if (args.toWindows) { - patcher = std::make_shared(args.windowsGui, Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png"); - } else { - patcher = std::make_shared( - std::make_shared(), - std::make_shared( - std::make_shared(), - byteWriter - ), - std::make_shared(byteWriter), - byteWriter - ); - } - - std::vector executableBytes; - try { - executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, args.runPath, args.lazyBinding, args.windowsDiagnostics, trampolines); - } catch (Domain::RelinkerException& error) { - error.InputPath = args.inputPath; - throw; - } + for (std::size_t index = 0; index < executables.size(); ++index) Patch(args, executables[index], index == 0); for (const auto& artifact : guestArtifacts) { std::filesystem::create_directories(artifact.Path.parent_path()); fileWriter.Write(Io::Utf8Path(artifact.Path), artifact.Bytes); std::cout << "Guest module: " << Io::Utf8Path(artifact.Path) << '\n'; } - fileWriter.Write(absPath, executableBytes); + for (const auto& executable : executables) fileWriter.Write(executable.OutputPath, executable.Bytes); if (args.writeRegistry) { const auto registryWriter = std::make_shared(); - fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); + for (std::size_t index = 0; index < executables.size(); ++index) + fileWriter.Write(Io::Utf8Path(registryPaths[index]), registryWriter->WriteCallRegistry(executables[index].Result.RegistryEntries)); for (std::size_t index = 0; index < guestArtifacts.size(); ++index) - fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); + fileWriter.Write(Io::Utf8Path(registryPaths[executables.size() + index]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); } if (desktopEntry) std::cout << "Desktop entry: " << Io::Utf8Path(desktopEntryWriter.Write(*desktopEntry)) << '\n'; - std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; - std::cout << "Expected runtime layout (relative to the output executable):\n" - << Io::Utf8Path(Io::NativePath(absPath).filename()) << "\n" - << "libs/\n *.prx\napp0/\n \n"; + std::cout << "External prx references: " << executables.front().Result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; + for (std::size_t index = 1; index < executables.size(); ++index) std::cout << "Executable file: " << executables[index].OutputPath << '\n'; + std::cout << "Expected runtime layout (relative to the output executable):\n"; + for (const auto& executable : executables) std::cout << Io::Utf8Path(Io::NativePath(executable.OutputPath).filename()) << "\n"; + std::cout << "libs/\n *.prx\napp0/\n \n"; for (const auto& artifact : guestArtifacts) { std::string relative = Io::Utf8Path(artifact.Path.lexically_relative(Io::NativePath(absPath).parent_path() / "app0")); std::replace(relative.begin(), relative.end(), '\\', '/'); diff --git a/core/relinker/relinker/tests/test_executables.py b/core/relinker/relinker/tests/test_executables.py new file mode 100644 index 000000000..bc8fcb260 --- /dev/null +++ b/core/relinker/relinker/tests/test_executables.py @@ -0,0 +1,76 @@ +from pathlib import Path +import os +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import module_with_symbol, needed_libraries +from test_guest_needed_modules import executable_with_needed + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-executables-") as directory: + work = Path(directory) + + def convert(case, windows, options, picker=None): + (case / "sce_module").mkdir(parents=True, exist_ok=True) + (case / "sce_module" / "bundled.prx").write_bytes(module_with_symbol(True)) + (case / "picker").mkdir(exist_ok=True) + (case / "picker" / "picker.bin").write_bytes(main_fixture() if picker is None else picker) + source = case / "eboot.bin" + source.write_bytes(main_fixture()) + output = case / "out" / ("app.exe" if windows else "app.elf") + output.parent.mkdir(exist_ok=True) + result = subprocess.run([str(relinker), *(["--windows"] if windows else []), "--registry", *options, str(source), str(output)], + capture_output=True, text=True, timeout=60) + return result, output + + for windows in (False, True): + suffix = ".exe" if windows else ".elf" + case = work / f"{windows}-picker" + result, output = convert(case, windows, ["--executable", "picker/picker.bin"]) + assert result.returncode == 0, (result.stdout, result.stderr) + secondary = output.parent / ("picker.picker.bin" + suffix) + assert secondary.read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), secondary + assert "Executable: picker/picker.bin\n" in result.stdout, result.stdout + assert f"Executable file: {secondary}\n" in result.stdout, result.stdout + assert f"app{suffix}\npicker.picker.bin{suffix}\nlibs/\n" in result.stdout, result.stdout + assert (output.parent / "picker.picker.bin.registry.json").is_file() + assert sorted(path.name for path in output.parent.rglob("*.guest.prx")) == ["bundled.prx.guest.prx"] + if not windows: + assert "$ORIGIN/app0/sce_module/bundled.prx.guest.prx" in needed_libraries(secondary.read_bytes()) + if windows and os.name == "nt": + for executable in (output, secondary): + run = subprocess.run([str(executable)], capture_output=True, text=True, timeout=30) + assert run.returncode == 42, (executable, run.returncode, run.stdout, run.stderr) + + for options, message in ((["--executable", "../eboot.bin"], "inside the guest module parent directory"), + (["--executable", "picker"], "is not a file"), + (["--executable", "picker/missing.bin"], "is not a file"), + (["--executable", "eboot.bin"], "converted twice"), + (["--executable", "picker/picker.bin", "--executable", "picker/./picker.bin"], "converted twice"), + (["--module-dir", "picker", "--executable", "picker/picker.bin"], "in a guest module directory"), + (["--module-dir", "picker/", "--executable", "picker/picker.bin"], "in a guest module directory")): + case = work / f"{windows}-rejected-{len(message)}-{len(options)}" + result, output = convert(case, windows, options) + assert result.returncode == 2 and message in result.stderr, (options, result.stderr) + assert not output.exists(), options + + case = work / f"{windows}-needed" + (case / "Media" / "Modules").mkdir(parents=True) + (case / "Media" / "Modules" / "needed.prx").write_bytes(module_with_symbol(True)) + result, output = convert(case, windows, ["--module-dir", "Media/Modules", "--executable", "picker/picker.bin"], executable_with_needed()) + if windows: + assert result.returncode == 2 and "Guest modules convert differently for picker/picker.bin" in result.stderr, result.stderr + assert not output.exists() + else: + assert result.returncode == 0, (result.stdout, result.stderr) + secondary = output.parent / "picker.picker.bin.elf" + assert "$ORIGIN/app0/Media/Modules/needed.prx.guest.prx" in needed_libraries(secondary.read_bytes()) + assert "$ORIGIN/app0/Media/Modules/needed.prx.guest.prx" not in needed_libraries(output.read_bytes()) + + +if __name__ == "__main__": + main() diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 052211ca0..452c63a91 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -14,5 +14,5 @@ ### Functional -- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, only the title's own `/app0/eboot.bin` without arguments is loaded: after the guest shutdown the converted executable is started again with the host command line it was started with (`CreateProcessW` on Windows, `execv` of `/proc/self/exe` on Linux), and the old process exits with 0 without waiting, so its direct memory is released before the new one allocates. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself this way to see newly mounted drives and expects no return. On Windows the new process is a separate process, so a shell or script that started the title sees it end. Other executables, non-empty argument lists and macOS hosts throw +- [sceSystemServiceLoadExec](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - besides `"exit"`, it starts the title's own `/app0/eboot.bin` or another executable under `/app0` that the relinker converted with `--executable`, which is looked up beside the running one (`/app0/picker/picker.bin` as `picker.picker.bin` with the running executable's extension). The main executable is the one `APS5_EBOOT` names, which the first call sets to its own path. After the guest shutdown the new executable gets the argument list as its command line, after its own path (`CreateProcessW` on Windows, `execv` on Linux), and the old process ends without running host destructors and without waiting, so its direct memory is released before the new one allocates. [VLC for PS5](https://github.com/ZiZc3/VLC/blob/86bb7d474a303f62bb3c2fc12f728db4846085b8/src/platform_ps5.c#L565-L571) (PPSA85300) relaunches itself with no arguments; RetroArch (PPSA99169) starts its frontend picker with `{"", NULL}`, and the picker starts `eboot.bin` with `{"--ps5-mode=retroarch", NULL}` or `/app0/es-de/es-de.bin` ([picker.cpp](https://github.com/mihawk-99/PS5_RetroArch/blob/bead80725cc11e3c5c65bd9424b0a4494ae36d45/frontends/picker/picker.cpp#L444-L446)), so the list holds only arguments. What the console puts in `argv[0]` is not known; here it is the host path. On Windows the new process is a separate process, so a shell or script that started the title sees it end. Paths outside `/app0`, unconverted executables and macOS hosts throw - [Running application ID](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index ea2ef030a..b4f658a54 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -57,6 +57,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--autorun` | Run the output after conversion, print its exit code, and wait for Enter. Adds executable permissions for Linux output. Requires the target OS and prepared runtime layout. | | `--sce-module-path ` | Parent of the module directories, not the module directory itself. Default: input ELF parent; relative input paths resolve from the process working directory. Explicit absolute path: used directly. Explicit relative path (including `.` and `..`): relative to the process working directory. Output path has no effect. Requires one nonempty value; repeats, nonexistent or nondirectory paths, access failures, and combination with `--skip-sce-module` are errors. | | `--module-dir ` | Relink the ELF modules of a directory under the module parent directory, besides `sce_module`/`sce_modules`/`prx`; outputs keep the relative path. A module the executable or another module needs is initialized at startup, any other when the title loads it with `sceKernelLoadStartModule`, with its start arguments (for example Unity plugins in `Media/Plugins`). Repeatable; `.` is the parent itself, without the input ELF. Conflicts with `--skip-sce-module`. | +| `--executable ` | Also convert another executable of the title, which it starts with `sceSystemServiceLoadExec`; the path is relative to the module parent directory, for example `picker/picker.bin`. It is written beside the output, named after the path with `/` replaced by `.` and given the output's extension (`picker.picker.bin.exe`), and uses the same `libs/`, `app0/` and guest modules. It is an error if a guest module converts differently for it. Repeatable. Conflicts with `--skip-sce-module`. | | `--skip-sce-module` | Deprecated. Disable bundled module processing and module directory checks. Conflicts with explicit `--sce-module-path`. | | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. |