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 <noreply@anthropic.com>
This commit is contained in:
p3nicillin 2026-10-13 08:51:50 +01:00
parent 3292cd39fb
commit 99efb32c40
10 changed files with 455 additions and 152 deletions

View File

@ -5,8 +5,7 @@
#include <cstring>
#include <cstdlib>
#include <charconv>
#include <fstream>
#include <iterator>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <string_view>
@ -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<DWORD>(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<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()));
}
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<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(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<char>(file)), std::istreambuf_iterator<char>());
std::vector<char*> argv;
std::size_t start = 0;
while (start < commandLine.size()) {
argv.push_back(const_cast<char*>(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<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
}
@ -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) {

View File

@ -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<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 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<DWORD>(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<char*>(nullptr));
execl(self.c_str(), self.c_str(), "--relaunch", static_cast<char*>(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
}

View File

@ -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}

View File

@ -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[]);

View File

@ -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)

View File

@ -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();

View File

@ -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(), '\\', '/');

View File

@ -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()

View File

@ -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.

View File

@ -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. |