From b42aa8c540e5eeb631a6b09b982a84b7e5ef5c6f Mon Sep 17 00:00:00 2001 From: boykopovar Date: Tue, 13 Oct 2026 10:54:45 +0300 Subject: [PATCH] feat(relinker): expose in-memory conversion through a portable core library --- core/relinker/CMakeLists.txt | 56 ++++--- .../domain/include/domain/InputProgress.hpp | 25 +++ .../elfpatcher/windows/WindowsElfPatcher.hpp | 4 +- .../src/windows/WindowsPePatcher.cpp | 4 +- core/relinker/io/src/NativePath.cpp | 39 +---- core/relinker/main.cpp | 142 ++++------------- .../relinker/domain/ISyscallScanner.hpp | 7 +- .../include/relinker/pipeline/Conversion.hpp | 52 ++++++ .../relinker/src/analysis/SyscallScanner.cpp | 15 +- .../relinker/src/guest/GuestModuleBuilder.cpp | 5 +- .../relinker/src/pipeline/Conversion.cpp | 134 ++++++++++++++++ .../relinker/tests/ConversionTests.cpp | 148 ++++++++++++++++++ 12 files changed, 449 insertions(+), 182 deletions(-) create mode 100644 core/relinker/domain/include/domain/InputProgress.hpp create mode 100644 core/relinker/relinker/include/relinker/pipeline/Conversion.hpp create mode 100644 core/relinker/relinker/src/pipeline/Conversion.cpp create mode 100644 core/relinker/relinker/tests/ConversionTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 1fc035256..59c01cf56 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -1,12 +1,8 @@ -add_executable(relinker - ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/NativeModules.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp +option(ANYPS5_RELINKER_CLI "Build the native relinker command line" ON) + +add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsGuestStartup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/guest/GuestImageReader.cpp @@ -34,8 +30,6 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileReader.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileWriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp @@ -72,29 +66,47 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/ValidationPolicy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/output/SysVDynamicSectionBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/output/CallRegistryWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/pipeline/Conversion.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/pipeline/RelinkerPipeline.cpp ) -target_include_directories(relinker PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/cli/include +target_include_directories(relinker_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include - ${CMAKE_CURRENT_SOURCE_DIR}/../libs ) +target_compile_features(relinker_core PUBLIC cxx_std_20) +set_target_properties(relinker_core PROPERTIES CXX_EXTENSIONS OFF) + +if(BUILD_TESTING) + add_test_executable(relinker_conversion_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ConversionTests.cpp) + target_link_libraries(relinker_conversion_tests PRIVATE relinker_core) + add_test(NAME relinker_conversion COMMAND relinker_conversion_tests) + set_tests_properties(relinker_conversion PROPERTIES TIMEOUT 20) +endif() + +if(NOT ANYPS5_RELINKER_CLI) + return() +endif() + +add_executable(relinker + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxDesktopEntryWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/NativeModules.cpp) +target_link_libraries(relinker PRIVATE relinker_core) +target_include_directories(relinker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cli/include ${CMAKE_CURRENT_SOURCE_DIR}/../libs) +set_target_properties(relinker PROPERTIES CXX_EXTENSIONS OFF) + if(BUILD_TESTING AND WIN32) - get_target_property(windowsAliasTestSources relinker SOURCES) - list(REMOVE_ITEM windowsAliasTestSources - ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp) - add_test_executable(windows_guest_export_alias_tests - ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/WindowsExportAliasTests.cpp - ${windowsAliasTestSources}) - get_target_property(windowsAliasIncludes relinker INCLUDE_DIRECTORIES) - target_include_directories(windows_guest_export_alias_tests PRIVATE ${windowsAliasIncludes}) + add_test_executable(windows_guest_export_alias_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/WindowsExportAliasTests.cpp) + target_link_libraries(windows_guest_export_alias_tests PRIVATE relinker_core) add_test(NAME windows_guest_export_aliases COMMAND windows_guest_export_alias_tests) endif() diff --git a/core/relinker/domain/include/domain/InputProgress.hpp b/core/relinker/domain/include/domain/InputProgress.hpp new file mode 100644 index 000000000..185274552 --- /dev/null +++ b/core/relinker/domain/include/domain/InputProgress.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +namespace Domain { + +class InputProgress final : public IProgress { +public: + InputProgress(IProgress& progress, std::string input) : progress(progress), input(std::move(input)) {} + + void Report(const ProgressEvent& event) override { + auto named = event; + if (named.Input.empty()) named.Input = input; + progress.Report(named); + } + + void Message(std::string_view message) override { progress.Message(message); } + +private: + IProgress& progress; + std::string input; +}; + +} diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp index 69e42dd71..a9b348fc4 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp @@ -3,12 +3,13 @@ #include #include +#include namespace Elfpatcher::Windows { class WindowsPePatcher : public IElfPatcher { public: - explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt); + explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png"); const std::string& GetDiagnostics() const; std::vector Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& trampolines) override; @@ -17,6 +18,7 @@ private: bool windowsGui; std::optional> icon; std::string diagnostics; + std::filesystem::path iconName; }; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 8342f7967..5d1082bcb 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -28,7 +28,7 @@ void writeGotStub(std::vector& sections, const std::uint32_t targetRv } -WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon) : windowsGui(windowsGui), icon(std::move(icon)) { +WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName) : windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)) { } const std::string& WindowsPePatcher::GetDiagnostics() const { return diagnostics; } @@ -95,7 +95,7 @@ std::vector WindowsPePatcher::Patch(const std::vector -#include - -#ifdef _WIN32 -#include -#include -#endif namespace Io { -#ifdef _WIN32 - std::filesystem::path NativePath(const std::string& path) { - if (path.empty()) return {}; - const int wideSize = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path.data(), static_cast(path.size()), nullptr, 0); - if (wideSize <= 0) throw Domain::RelinkerException("Path is not valid UTF-8: " + path); - std::wstring wide(static_cast(wideSize), L'\0'); - if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path.data(), static_cast(path.size()), wide.data(), wideSize) != wideSize) - throw Domain::RelinkerException("Path is not valid UTF-8: " + path); - return std::filesystem::path(std::move(wide)); + const std::u8string utf8(path.begin(), path.end()); + return std::filesystem::path(utf8); } std::string Utf8Path(const std::filesystem::path& path) { - const std::wstring wide = path.wstring(); - if (wide.empty()) return {}; - const int utf8Size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), nullptr, 0, nullptr, nullptr); - if (utf8Size <= 0) throw Domain::RelinkerException("Path cannot be encoded as UTF-8"); - std::string utf8(static_cast(utf8Size), '\0'); - if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast(wide.size()), utf8.data(), utf8Size, nullptr, nullptr) != utf8Size) - throw Domain::RelinkerException("Path cannot be encoded as UTF-8"); - return utf8; + const auto utf8 = path.u8string(); + return std::string(utf8.begin(), utf8.end()); } -#else - -std::filesystem::path NativePath(const std::string& path) { - return std::filesystem::path(path); -} - -std::string Utf8Path(const std::filesystem::path& path) { - return path.string(); -} - -#endif - } diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 9e537e569..a01ace2aa 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -1,30 +1,13 @@ -#include #include +#include +#include #include #include #include #include -#include #include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include #include #include #include @@ -43,8 +26,6 @@ namespace { struct ConvertedExecutable { std::string InputPath; std::string OutputPath; - std::vector Source; - std::vector Trampolines; Relinker::RelinkResult Result; std::vector GuestArtifacts; std::vector Bytes; @@ -53,93 +34,32 @@ struct ConvertedExecutable { ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable, Domain::IProgress& progress) { Io::FileReader fileReader; - auto sourceBytes = fileReader.Read(inputPath); - - std::vector trampolines; - if (args.toIntel) { - const auto segments = Relinker::ElfReader(sourceBytes).ReadProgramHeaders(); - auto converted = Codegen::MakeAmd64OnlyConverter(args.toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(sourceBytes), segments); - sourceBytes = std::move(converted.Bytes); - trampolines = std::move(converted.Trampolines); - std::map stubsByName; - const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; - for (const auto& report : converted.Reports) { - if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) - std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; - else if (counted(report.InstructionName)) - ++stubsByName[report.InstructionName]; - else - std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n"; - } - for (const auto& [name, count] : stubsByName) - std::cout << "Intel substitution: " << name << " -> stub at " << count << " sites\n"; - std::cout << "Intel conversion: " << converted.ReplacedCount << " in place, " << trampolines.size() << " stubs, " << converted.KeptCount << " kept\n"; - } - - auto elfReader = std::make_shared(sourceBytes); - const std::shared_ptr syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(progress, args.trapSdkSyscallStubs); - - const auto pipeline = std::make_shared( - elfReader, - syscallScanner, - Relinker::MakeCallSiteResolver(), - std::make_shared(), - std::make_shared(), - args.unusedFilterLevel == 2 ? Relinker::MakeStrictUnusedNidFilter(progress) : Relinker::MakeUnusedNidFilter(), - args.unusedFilterLevel, progress - ); - - if (mainExecutable) { - std::cout << "System: " << (args.toWindows ? "Windows" : args.toMacos ? "macOS" : "Linux") << "; unused-filter=" << args.unusedFilterLevel << "\n"; - std::cout << Relinker::GuestModulePattern << " processing: " << (args.skipSceModule ? "disabled (--skip-sce-module)" : "enabled") << '\n'; - for (const auto& name : args.excludedSceModules) std::cout << "Guest module excluded: " << name << '\n'; - } - auto result = pipeline->Relink(sourceBytes); - for (const auto& patch : result.Patches) { - if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset) - throw Domain::RelinkerException("Relinker patch exceeds source image", patch.Offset); - for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index]; - } - - std::vector guestArtifacts; + Relinker::ConversionInput input; + input.Executable = fileReader.Read(inputPath); + input.InputPath = std::filesystem::absolute(Io::NativePath(inputPath)).lexically_normal(); + input.OutputPath = Io::NativePath(absPath); if (!args.skipSceModule) { - auto modules = Cli::ReadModules(std::filesystem::absolute(Io::NativePath(inputPath)), Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); - guestArtifacts = Relinker::GuestModuleBuilder().Build(std::filesystem::absolute(Io::NativePath(inputPath)), Io::NativePath(absPath), result.DynamicSection, args.toWindows, args.toMacos, args.toIntel, args.toRosetta, *syscallScanner, args.lazyBinding, args.runPath, args.excludedSceModules, modules.Root, modules.RuntimeDirectories, std::move(modules.Modules), progress); + auto modules = Cli::ReadModules(input.InputPath, Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories); + input.ModuleRoot = std::move(modules.Root); + input.Modules = std::move(modules.Modules); + input.RuntimeDirectories = std::move(modules.RuntimeDirectories); } - - return {inputPath, absPath, std::move(sourceBytes), std::move(trampolines), std::move(result), std::move(guestArtifacts), {}}; -} - -void Patch(const Cli::Args& args, ConvertedExecutable& converted, const bool mainExecutable) { - auto byteWriter = std::make_shared(); - - std::shared_ptr patcher; - if (args.toMacos) { - patcher = std::make_shared(); - } else if (args.toWindows) { - const auto iconPath = Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png"; - std::optional> icon; - if (std::filesystem::exists(iconPath)) icon = Io::FileReader().Read(Io::Utf8Path(iconPath)); - patcher = std::make_shared(args.windowsGui, std::move(icon)); - } else { - patcher = std::make_shared( - std::make_shared(), - std::make_shared( - std::make_shared(), - byteWriter - ), - std::make_shared(byteWriter), - byteWriter - ); - } - - try { - converted.Bytes = patcher->Patch(converted.Source, converted.Result.OriginalHeaders, converted.Result.DynamicSection, converted.Result.OriginalPltGotVaddr, args.runPath, args.lazyBinding, args.windowsDiagnostics && mainExecutable, converted.Trampolines); - } catch (Domain::RelinkerException& error) { - error.InputPath = converted.InputPath; - throw; - } - if (args.windowsDiagnostics && mainExecutable) converted.Diagnostics = std::static_pointer_cast(patcher)->GetDiagnostics(); + const auto iconPath = Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png"; + if (args.toWindows && std::filesystem::exists(iconPath)) input.Icon = fileReader.Read(Io::Utf8Path(iconPath)); + Relinker::ConversionOptions options; + options.Target = args.toWindows ? Relinker::TargetSystem::Windows : args.toMacos ? Relinker::TargetSystem::MacOs : Relinker::TargetSystem::Linux; + options.ToIntel = args.toIntel; + options.ToRosetta = args.toRosetta; + options.SkipSyscallCheck = args.skipSyscallCheck; + options.TrapSdkSyscallStubs = args.trapSdkSyscallStubs; + options.UnusedFilterLevel = args.unusedFilterLevel; + options.LazyBinding = args.lazyBinding; + options.RunPath = args.runPath; + options.WindowsGui = args.windowsGui; + options.WindowsDiagnostics = args.windowsDiagnostics && mainExecutable; + options.ExcludedModules = args.excludedSceModules; + auto converted = Relinker::Conversion(progress).Convert(std::move(input), options); + return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics)}; } int Run(const int argc, char* argv[]) { @@ -249,14 +169,20 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(Io::NativePath(absPath), "Executable"); for (const auto& artifact : guestArtifacts) checkOutputInputs(artifact.Path, "Guest module"); - for (std::size_t index = 0; index < executables.size(); ++index) Patch(args, executables[index], index == 0); if (args.windowsDiagnostics) fileWriter.Write("windows-diagnostics-imports.txt", executables.front().Diagnostics); + const auto outputCount = guestArtifacts.size() + executables.size(); + progress.Report({Domain::ProgressStage::Write, absPath, 0, outputCount}); + std::size_t written = 0; for (const auto& artifact : guestArtifacts) { std::filesystem::create_directories(artifact.Path.parent_path()); fileWriter.Write(Io::Utf8Path(artifact.Path), artifact.Bytes); + progress.Report({Domain::ProgressStage::Write, absPath, ++written, outputCount}); std::cout << "Guest module: " << Io::Utf8Path(artifact.Path) << '\n'; } - for (const auto& executable : executables) fileWriter.Write(executable.OutputPath, executable.Bytes); + for (const auto& executable : executables) { + fileWriter.Write(executable.OutputPath, executable.Bytes); + progress.Report({Domain::ProgressStage::Write, absPath, ++written, outputCount}); + } if (args.writeRegistry) { const auto registryWriter = std::make_shared(); for (std::size_t index = 0; index < executables.size(); ++index) diff --git a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp index 021938a31..71a9160da 100644 --- a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Relinker { @@ -11,11 +12,7 @@ class ISyscallScanner { public: virtual ~ISyscallScanner() = default; - virtual std::vector ScanCodeSectionForSyscalls( - const std::vector& codeSection, - FileByteOffset codeSectionOffset, - FileByteOffset codeSectionSize - ) = 0; + virtual std::vector ScanCodeSectionForSyscalls(const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}) = 0; }; } diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp new file mode 100644 index 000000000..d1b0cf021 --- /dev/null +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include + +namespace Relinker { + +enum class TargetSystem { Linux, Windows, MacOs }; + +struct ConversionOptions { + TargetSystem Target = TargetSystem::Linux; + std::uint32_t UnusedFilterLevel = 0; + bool ToIntel = false; + bool ToRosetta = false; + bool SkipSyscallCheck = false; + bool TrapSdkSyscallStubs = false; + bool LazyBinding = false; + bool WindowsGui = false; + bool WindowsDiagnostics = false; + std::string RunPath = "$ORIGIN/libs"; + std::set ExcludedModules; +}; + +struct ConversionInput { + std::vector Executable; + std::filesystem::path InputPath; + std::filesystem::path OutputPath; + std::filesystem::path ModuleRoot; + std::vector Modules; + std::vector RuntimeDirectories; + std::optional> Icon; +}; + +struct ConversionOutput { + std::vector Executable; + std::vector Modules; + RelinkResult Analysis; + std::string WindowsDiagnostics; +}; + +class Conversion { +public: + explicit Conversion(Domain::IProgress& progress); + ConversionOutput Convert(ConversionInput input, const ConversionOptions& options) const; + +private: + Domain::IProgress& progress; +}; + +} diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 79a68d5cc..d8a880fef 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -169,7 +169,7 @@ public: std::vector ScanCodeSectionForSyscalls( const std::vector& codeSection, FileByteOffset codeSectionOffset, - FileByteOffset codeSectionSize) override; + FileByteOffset codeSectionSize, std::string_view input) override; private: Domain::IProgress& progress; @@ -179,16 +179,16 @@ private: std::vector SyscallScanner::ScanCodeSectionForSyscalls( const std::vector& codeSection, const FileByteOffset codeSectionOffset, - const FileByteOffset codeSectionSize) { + const FileByteOffset codeSectionSize, std::string_view input) { const std::size_t limit = std::min(codeSection.size(), static_cast(codeSectionSize)); - const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, {}, completed, total}); }); + const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, std::string(input), completed, total}); }); const auto matches = scanner->ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize); - progress.Report({Domain::ProgressStage::Syscalls, {}, 0, matches.size()}); + progress.Report({Domain::ProgressStage::Syscalls, std::string(input), 0, matches.size()}); std::vector trapped; for (std::size_t index = 0; index < matches.size(); ++index) { - if (index % 4096 == 0) progress.Report({Domain::ProgressStage::Syscalls, {}, index, matches.size()}); + if (index != 0 && index % 4096 == 0) progress.Report({Domain::ProgressStage::Syscalls, std::string(input), index, matches.size()}); const auto& match = matches[index]; const std::size_t start = static_cast(match.Offset - codeSectionOffset); const auto opcodeOffset = Codegen::DecodedInstruction{codeSection.data() + start, match.Length}.OpcodeOffset(); @@ -218,7 +218,7 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls( throw RelinkerException(msg.str(), match.Offset); } - progress.Report({Domain::ProgressStage::Syscalls, {}, matches.size(), matches.size()}); + progress.Report({Domain::ProgressStage::Syscalls, std::string(input), matches.size(), matches.size()}); return trapped; } @@ -231,7 +231,8 @@ public: std::vector ScanCodeSectionForSyscalls( const std::vector&, FileByteOffset, - FileByteOffset + FileByteOffset, + std::string_view ) override { return {}; } }; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index b91161016..c391a5306 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -61,7 +61,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path progress.Report({Domain::ProgressStage::Modules, {}, 0, modules.size()}); for (auto& module : modules) { const auto path = module.Path.lexically_normal(); - if (path == inputPath.lexically_normal() || path == outputPath.lexically_normal()) throw Domain::RelinkerException("Module path collides with executable: " + Io::Utf8Path(path)); + if (path == inputPath.lexically_normal()) throw Domain::RelinkerException("Module path collides with the input executable: " + Io::Utf8Path(path)); + if (path == outputPath.lexically_normal()) throw Domain::RelinkerException("Executable output would overwrite an input module: " + Io::Utf8Path(path)); const auto relative = path.lexically_relative(root); if (relative.empty() || relative.has_root_path() || *relative.begin() == "..") throw Domain::RelinkerException("Module is outside module root: " + Io::Utf8Path(path)); if (excludedModules.contains(Io::Utf8Path(path.filename()))) throw Domain::RelinkerException("Excluded module supplied to converter: " + Io::Utf8Path(path)); @@ -299,7 +300,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } for (const auto& header : codeHeaders) { const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); - for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize)) { + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize, Io::Utf8Path(image.SourcePath))) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); image.Bytes[header.Offset + offset] = 0x0F; diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp new file mode 100644 index 000000000..1ca72e6dd --- /dev/null +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -0,0 +1,134 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Relinker { + +Conversion::Conversion(Domain::IProgress& progress) : progress(progress) {} + +ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOptions& options) const { + Domain::InputProgress inputProgress(progress, Io::Utf8Path(input.InputPath)); + static_assert(std::endian::native == std::endian::little); + if (options.Target != TargetSystem::Linux && options.Target != TargetSystem::Windows && options.Target != TargetSystem::MacOs) throw Domain::RelinkerException("Unknown target system"); + if (options.ToRosetta && (!options.ToIntel || options.Target == TargetSystem::Windows)) throw Domain::RelinkerException("Rosetta conversion requires Intel lowering and a non-Windows target"); + if (options.UnusedFilterLevel > 2) throw Domain::RelinkerException("Unused NID filter level must be 0, 1 or 2"); + if (input.InputPath.empty() || input.OutputPath.empty()) throw Domain::RelinkerException("Input and output names are required"); + if (input.InputPath.lexically_normal() == input.OutputPath.lexically_normal()) throw Domain::RelinkerException("Executable output would overwrite the input executable"); + if (!input.Modules.empty() && input.ModuleRoot.empty()) throw Domain::RelinkerException("Module root is required"); + if (options.WindowsDiagnostics && options.Target != TargetSystem::Windows) throw Domain::RelinkerException("Windows diagnostics require the Windows target"); + const bool windows = options.Target == TargetSystem::Windows; + const bool macos = options.Target == TargetSystem::MacOs; + inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 0, 1}); + auto sourceBytes = std::move(input.Executable); + std::vector trampolines; + if (options.ToIntel) { + const auto segments = Relinker::ElfReader(sourceBytes).ReadProgramHeaders(); + auto converted = Codegen::MakeAmd64OnlyConverter(options.ToRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(sourceBytes), segments); + sourceBytes = std::move(converted.Bytes); + trampolines = std::move(converted.Trampolines); + std::map stubsByName; + const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; + for (const auto& report : converted.Reports) { + if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) + { std::ostringstream message; message << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; inputProgress.Message(message.str()); } + else if (counted(report.InstructionName)) + ++stubsByName[report.InstructionName]; + else + { std::ostringstream message; message << "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"; inputProgress.Message(message.str()); } + } + for (const auto& [name, count] : stubsByName) + { std::ostringstream message; message << "Intel substitution: " << name << " -> stub at " << count << " sites\n"; inputProgress.Message(message.str()); } + { std::ostringstream message; message << "Intel conversion: " << converted.ReplacedCount << " in place, " << trampolines.size() << " stubs, " << converted.KeptCount << " kept\n"; inputProgress.Message(message.str()); } + } + + auto elfReader = std::make_shared(sourceBytes); + const std::shared_ptr syscallScanner = options.SkipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(inputProgress, options.TrapSdkSyscallStubs); + + const auto pipeline = std::make_shared( + elfReader, + syscallScanner, + Relinker::MakeCallSiteResolver(), + std::make_shared(), + std::make_shared(), + options.UnusedFilterLevel == 2 ? Relinker::MakeStrictUnusedNidFilter(inputProgress) : Relinker::MakeUnusedNidFilter(), + options.UnusedFilterLevel, inputProgress + ); + + { std::ostringstream message; message << "System: " << (windows ? "Windows" : macos ? "macOS" : "Linux") << "; unused-filter=" << options.UnusedFilterLevel << "\n"; inputProgress.Message(message.str()); } + for (const auto& name : options.ExcludedModules) { std::ostringstream message; message << "Guest module excluded: " << name << '\n'; inputProgress.Message(message.str()); } + auto result = pipeline->Relink(sourceBytes); + for (const auto& patch : result.Patches) { + if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset) + throw Domain::RelinkerException("Relinker patch exceeds source image", patch.Offset); + for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index]; + } + + std::vector guestArtifacts; + if (!input.Modules.empty()) { + guestArtifacts = Relinker::GuestModuleBuilder().Build(input.InputPath, input.OutputPath, result.DynamicSection, windows, macos, options.ToIntel, options.ToRosetta, *syscallScanner, options.LazyBinding, options.RunPath, options.ExcludedModules, input.ModuleRoot, input.RuntimeDirectories, std::move(input.Modules), inputProgress); + } + + auto byteWriter = std::make_shared(); + + std::shared_ptr patcher; + if (macos) { + patcher = std::make_shared(); + } else if (windows) { + patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png"); + } else { + patcher = std::make_shared( + std::make_shared(), + std::make_shared( + std::make_shared(), + byteWriter + ), + std::make_shared(byteWriter), + byteWriter + ); + } + + inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 0, 1}); + std::vector executableBytes; + try { + executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, options.RunPath, options.LazyBinding, options.WindowsDiagnostics, trampolines); + } catch (Domain::RelinkerException& error) { + error.InputPath = Io::Utf8Path(input.InputPath); + throw; + } + + inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 1, 1}); + std::string diagnostics; + if (windows) diagnostics = std::static_pointer_cast(patcher)->GetDiagnostics(); + inputProgress.Report({Domain::ProgressStage::Executable, Io::Utf8Path(input.InputPath), 1, 1}); + return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics)}; +} + +} diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp new file mode 100644 index 000000000..21240cbc7 --- /dev/null +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -0,0 +1,148 @@ +#include "TestProgress.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void reject(TAction action) { + try { action(); } catch (const Domain::RelinkerException&) { return; } + throw std::runtime_error("Invalid conversion was accepted"); +} + +std::vector fixture() { + std::vector bytes(0x3000); + const std::array magic{0x7f, 'E', 'L', 'F', 2, 1, 1}; + std::copy(magic.begin(), magic.end(), bytes.begin()); + Io::WriteU16(bytes, 16, 3); + Io::WriteU16(bytes, 18, 62); + Io::WriteU32(bytes, 20, 1); + Io::WriteU64(bytes, 24, 0x1000); + Io::WriteU64(bytes, 32, 64); + Io::WriteU16(bytes, 52, 64); + Io::WriteU16(bytes, 54, 56); + Io::WriteU16(bytes, 56, 8); + Io::WriteU16(bytes, 58, 64); + const auto header = [&](std::size_t index, unsigned type, unsigned flags, unsigned offset, unsigned size) { + const auto at = 64 + index * 56; + Io::WriteU32(bytes, at, type); + Io::WriteU32(bytes, at + 4, flags); + Io::WriteU64(bytes, at + 8, offset); + Io::WriteU64(bytes, at + 16, offset); + Io::WriteU64(bytes, at + 24, offset); + Io::WriteU64(bytes, at + 32, size); + Io::WriteU64(bytes, at + 40, size); + Io::WriteU64(bytes, at + 48, 8); + }; + header(0, 1, 5, 0x1000, 8); + header(1, 1, 6, 0x2000, 0x1000); + for (std::size_t index = 3; index < 8; ++index) header(index, 0x6fffff01, 0, 0, 0); + bytes[0x1000] = 0xc3; + const std::array, 9> tags{{{5, 0x2600}, {10, 1}, {6, 0x2620}, {11, 24}, {7, 0x2700}, {8, 0}, {9, 24}, {4, 0x2680}, {0, 0}}}; + header(2, 2, 6, 0x2000, tags.size() * 16); + for (std::size_t index = 0; index < tags.size(); ++index) { + Io::WriteU64(bytes, 0x2000 + index * 16, tags[index].first); + Io::WriteU64(bytes, 0x2008 + index * 16, tags[index].second); + } + Io::WriteU32(bytes, 0x2680, 1); + Io::WriteU32(bytes, 0x2684, 1); + return bytes; +} + +Relinker::ConversionInput input() { + Relinker::ConversionInput result; + result.Executable = fixture(); + result.InputPath = "virtual/title/custom-name.bin"; + result.OutputPath = "virtual/output/title"; + result.ModuleRoot = "virtual/title"; + return result; +} + +void convert() { + TestProgress progress; + const Relinker::Conversion converter(progress); + for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::Linux}) { + Relinker::ConversionOptions options; + options.Target = target; + auto source = input(); + source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); + const auto output = converter.Convert(std::move(source), options); + require(output.Executable.size() > 64, "No executable returned"); + require(output.Executable.front() == (target == Relinker::TargetSystem::Windows ? 'M' : 0x7f), "Wrong target format"); + require(output.Modules.size() == 1, "Module was not converted"); + require(output.Modules.front().Path.generic_string() == "virtual/output/app0/sce_module/plugin.prx.guest.prx", "Wrong module layout"); + require(output.Modules.front().Bytes.front() == output.Executable.front(), "Wrong module format"); + require(progress.Events.back().Stage == Domain::ProgressStage::Executable && progress.Events.back().Completed == 1, "Conversion did not finish"); + for (const auto& event : progress.Events) require(event.Completed <= event.Total, "Invalid counters"); + } + auto source = input(); + source.Modules.push_back({"virtual/escaped.prx", fixture()}); + reject([&] { converter.Convert(source, {}); }); + source = input(); + source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()}); + source.Modules.push_back(source.Modules.front()); + reject([&] { converter.Convert(source, {}); }); + source = input(); + source.Icon = std::vector{}; + Relinker::ConversionOptions windows; + windows.Target = Relinker::TargetSystem::Windows; + reject([&] { converter.Convert(source, windows); }); + source = input(); + source.Executable = {}; + reject([&] { converter.Convert(source, {}); }); + require(progress.Events.back().Completed != 1 || progress.Events.back().Stage != Domain::ProgressStage::Executable, "Failed conversion reported completion"); +} + +void scanning() { + TestProgress progress; + const auto scanner = Relinker::MakeSyscallScanner(progress); + std::vector code(150000, 0x90); + require(scanner->ScanCodeSectionForSyscalls(code, 0, code.size(), "sample.prx").empty(), "Unexpected syscall trap"); + require(std::any_of(progress.Events.begin(), progress.Events.end(), [](const auto& event) { return event.Completed > 0 && event.Completed < event.Total; }), "Only start and finish were reported"); + for (const auto& event : progress.Events) require(event.Input == "sample.prx", "Lost scanner input identity"); + code[70000] = 0x0f; + code[70001] = 0x05; + progress.Events.clear(); + reject([&] { scanner->ScanCodeSectionForSyscalls(code, 0, code.size()); }); + require(progress.Events.back().Completed < progress.Events.back().Total, "Failed scan reported completion"); +} + +class CancellingProgress final : public Domain::IProgress { +public: + void Report(const Domain::ProgressEvent& event) override { + if (event.Stage == Domain::ProgressStage::Decode && event.Completed > 0) throw std::runtime_error("cancelled"); + } + void Message(std::string_view) override {} +}; + +void cancellation() { + CancellingProgress progress; + try { Relinker::Conversion(progress).Convert(input(), {}); } + catch (const std::runtime_error& error) { + require(std::string(error.what()) == "cancelled", "Callback exception was replaced"); + return; + } + throw std::runtime_error("Callback exception was swallowed"); +} + +} + +int main() { + try { + convert(); + scanning(); + cancellation(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +}