Merge pull request #4786 from p3n1cillin/claude/load-exec-restart
feat: convert a title's other executables and start them with sceSystemServiceLoadExec
This commit is contained in:
commit
c080fc2d5b
|
|
@ -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:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <charconv>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#elif defined(__linux__)
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
|
@ -29,18 +38,169 @@ int ConsoleLanguage() {
|
|||
return language;
|
||||
}
|
||||
|
||||
constexpr char MainExecutable[] = "/app0/eboot.bin";
|
||||
constexpr char ApplicationRoot[] = "/app0/";
|
||||
constexpr char MainExecutableVariable[] = "APS5_EBOOT";
|
||||
#if defined(_WIN32)
|
||||
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<int>(text.size()), nullptr, 0);
|
||||
if (size <= 0) throw std::runtime_error("sceSystemServiceLoadExec: argument is not valid UTF-8");
|
||||
std::wstring wide(static_cast<std::size_t>(size), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast<int>(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<DWORD>(own.size()));
|
||||
if (length == 0 || length >= own.size()) {
|
||||
throw std::runtime_error("sceSystemServiceLoadExec: GetModuleFileNameW failed with error " + std::to_string(GetLastError()));
|
||||
}
|
||||
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<DWORD>(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(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__)
|
||||
struct HostLaunch {
|
||||
std::string executable;
|
||||
std::vector<std::string> 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<char*> 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
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
NotImplemented_nid_no_patch("sceSystemServiceLoadExec: executable replacement");
|
||||
if (std::strcmp(path, "exit") == 0) {
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::exit(0);
|
||||
}
|
||||
(void)arguments;
|
||||
auto launch = PrepareLaunch(path, arguments);
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::exit(0);
|
||||
std::fflush(nullptr);
|
||||
StartHostExecutable(launch);
|
||||
LibcTerminate_nid_no_patch(0);
|
||||
}
|
||||
|
||||
int APS5_VABI sceSystemServiceDisableNoticeScreenSkipFlagAutoSet(void) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,21 @@
|
|||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include "prx/libSceSystemService/SystemService.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
extern "C" int APS5_VABI sceSystemServiceLoadExec(const char*, const char* const*);
|
||||
extern "C" void APS5_VABI _Exit_nid_postfix(int);
|
||||
|
|
@ -33,6 +42,79 @@ void UnexpectedCleanup() { std::_Exit(3); }
|
|||
#ifdef _WIN32
|
||||
void WINAPI UnexpectedDetach(PVOID) { TerminateProcess(GetCurrentProcess(), 4); }
|
||||
#endif
|
||||
constexpr char RelaunchMarker[] = "ANYPS5_TEST_RELAUNCH_MARKER";
|
||||
constexpr char MainExecutable[] = "ANYPS5_TEST_MAIN_EXECUTABLE";
|
||||
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
|
||||
}
|
||||
std::filesystem::path OwnExecutable() {
|
||||
#ifdef _WIN32
|
||||
std::wstring executable(32768, L'\0');
|
||||
executable.resize(GetModuleFileNameW(nullptr, executable.data(), static_cast<DWORD>(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 MainShutdown() { AppendMarker("shutdown"); }
|
||||
void SecondaryShutdown() { AppendMarker("secondary shutdown"); }
|
||||
int RunRelaunchingChild(const std::filesystem::path& self) {
|
||||
#ifdef _WIN32
|
||||
std::wstring commandLine = L"\"" + self.wstring() + L"\" --relaunch";
|
||||
STARTUPINFOW startup{};
|
||||
startup.cb = sizeof(startup);
|
||||
PROCESS_INFORMATION process{};
|
||||
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);
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
return static_cast<int>(code);
|
||||
#else
|
||||
const pid_t child = fork();
|
||||
Require(child >= 0);
|
||||
if (child == 0) {
|
||||
execl(self.c_str(), self.c_str(), "--relaunch", static_cast<char*>(nullptr));
|
||||
std::_Exit(127);
|
||||
}
|
||||
int status = 0;
|
||||
Require(waitpid(child, &status, 0) == child);
|
||||
return WIFEXITED(status) ? WEXITSTATUS(status) : 1;
|
||||
#endif
|
||||
}
|
||||
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) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
std::ifstream file(marker);
|
||||
text.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
|
||||
}
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(marker, ignored);
|
||||
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;
|
||||
}
|
||||
}
|
||||
int main(int argc, char** argv) {
|
||||
if (argc > 1 && std::strcmp(argv[1], "--immediate") == 0) {
|
||||
|
|
@ -55,6 +137,31 @@ 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();
|
||||
}
|
||||
if (argc > 1 && std::strcmp(argv[1], "--relaunch") == 0) {
|
||||
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);
|
||||
|
|
@ -63,8 +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);
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -440,6 +440,8 @@ if(BUILD_TESTING)
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_dir.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME guest_module_directories COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_directories.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME relinker_executables COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_executables.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME sce_module_path COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_path.py $<TARGET_FILE:relinker>)
|
||||
add_test(NAME linux_guest_stack COMMAND ${Python3_EXECUTABLE}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ struct Args {
|
|||
std::string runPath = "$ORIGIN/libs";
|
||||
std::set<std::string> excludedSceModules;
|
||||
std::vector<std::string> moduleDirectories;
|
||||
std::vector<std::string> executables;
|
||||
};
|
||||
|
||||
Args ParseArgs(int argc, char* argv[]);
|
||||
|
|
|
|||
|
|
@ -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 <path>] [--exclude-sce-module <file>]... [--module-dir <dir>]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath <path>] [--lazy-binding] [--autorun] <input.elf> <output.elf>\n"
|
||||
return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path <path>] [--exclude-sce-module <file>]... [--module-dir <dir>]... [--executable <path>]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath <path>] [--lazy-binding] [--autorun] <input.elf> <output.elf>\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)
|
||||
|
|
|
|||
|
|
@ -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<std::string>{"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();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,102 @@
|
|||
|
||||
namespace {
|
||||
|
||||
struct ConvertedExecutable {
|
||||
std::string InputPath;
|
||||
std::string OutputPath;
|
||||
std::vector<std::uint8_t> Source;
|
||||
std::vector<Codegen::TrampolineSite> Trampolines;
|
||||
Relinker::RelinkResult Result;
|
||||
std::vector<Relinker::GuestArtifact> GuestArtifacts;
|
||||
std::vector<std::uint8_t> 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<Codegen::TrampolineSite> 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<std::string, std::size_t> 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<Relinker::ElfReader>(sourceBytes);
|
||||
const std::shared_ptr<Relinker::ISyscallScanner> syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(args.trapSdkSyscallStubs);
|
||||
|
||||
const auto pipeline = std::make_shared<Relinker::RelinkerPipeline>(
|
||||
elfReader,
|
||||
syscallScanner,
|
||||
Relinker::MakeCallSiteResolver(),
|
||||
std::make_shared<Relinker::ValidationPolicy>(),
|
||||
std::make_shared<Relinker::SysVDynamicSectionBuilder>(),
|
||||
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<Relinker::GuestArtifact> 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<Io::ByteWriter>();
|
||||
|
||||
std::shared_ptr<Elfpatcher::IElfPatcher> patcher;
|
||||
if (args.toMacos) {
|
||||
patcher = std::make_shared<Elfpatcher::MacOs::MacOsMachOPatcher>();
|
||||
} else if (args.toWindows) {
|
||||
patcher = std::make_shared<Elfpatcher::Windows::WindowsPePatcher>(args.windowsGui, Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png");
|
||||
} else {
|
||||
patcher = std::make_shared<Elfpatcher::Linux::LinuxElfPatcher>(
|
||||
std::make_shared<Elfpatcher::EntryStubBuilder>(),
|
||||
std::make_shared<Elfpatcher::ProgramHeaderLayoutBuilder>(
|
||||
std::make_shared<Elfpatcher::SegmentFilter>(),
|
||||
byteWriter
|
||||
),
|
||||
std::make_shared<Elfpatcher::SectionHeaderTableBuilder>(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<char>(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<Codegen::TrampolineSite> 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<std::string, std::size_t> 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<ExtraExecutable> 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<std::filesystem::path> 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<Relinker::ElfReader>(sourceBytes);
|
||||
const std::shared_ptr<Relinker::ISyscallScanner> syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(args.trapSdkSyscallStubs);
|
||||
|
||||
const auto pipeline = std::make_shared<Relinker::RelinkerPipeline>(
|
||||
elfReader,
|
||||
syscallScanner,
|
||||
Relinker::MakeCallSiteResolver(),
|
||||
std::make_shared<Relinker::ValidationPolicy>(),
|
||||
std::make_shared<Relinker::SysVDynamicSectionBuilder>(),
|
||||
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<Relinker::GuestArtifact> 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<ConvertedExecutable> 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<std::filesystem::path> 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<Io::ByteWriter>();
|
||||
|
||||
std::shared_ptr<Elfpatcher::IElfPatcher> patcher;
|
||||
if (args.toMacos) {
|
||||
patcher = std::make_shared<Elfpatcher::MacOs::MacOsMachOPatcher>();
|
||||
} else if (args.toWindows) {
|
||||
patcher = std::make_shared<Elfpatcher::Windows::WindowsPePatcher>(args.windowsGui, Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png");
|
||||
} else {
|
||||
patcher = std::make_shared<Elfpatcher::Linux::LinuxElfPatcher>(
|
||||
std::make_shared<Elfpatcher::EntryStubBuilder>(),
|
||||
std::make_shared<Elfpatcher::ProgramHeaderLayoutBuilder>(
|
||||
std::make_shared<Elfpatcher::SegmentFilter>(),
|
||||
byteWriter
|
||||
),
|
||||
std::make_shared<Elfpatcher::SectionHeaderTableBuilder>(byteWriter),
|
||||
byteWriter
|
||||
);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> 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<Relinker::CallRegistryWriter>();
|
||||
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 <game resources>\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 <game resources>\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(), '\\', '/');
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -14,4 +14,5 @@
|
|||
|
||||
### Functional
|
||||
|
||||
- [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.
|
||||
|
|
|
|||
|
|
@ -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 <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 <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 <path>` | 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 <file>` | 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. |
|
||||
|
|
|
|||
Loading…
Reference in New Issue