feat(relinker): expose in-memory conversion through a portable core library

This commit is contained in:
boykopovar 2026-10-13 10:54:45 +03:00
parent 5e5eba8922
commit b42aa8c540
12 changed files with 449 additions and 182 deletions

View File

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

View File

@ -0,0 +1,25 @@
#pragma once
#include <domain/IProgress.hpp>
#include <utility>
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;
};
}

View File

@ -3,12 +3,13 @@
#include <elfpatcher/general/IElfPatcher.hpp>
#include <optional>
#include <filesystem>
namespace Elfpatcher::Windows {
class WindowsPePatcher : public IElfPatcher {
public:
explicit WindowsPePatcher(bool windowsGui = false, std::optional<std::vector<std::uint8_t>> icon = std::nullopt);
explicit WindowsPePatcher(bool windowsGui = false, std::optional<std::vector<std::uint8_t>> icon = std::nullopt, std::filesystem::path iconName = "icon0.png");
const std::string& GetDiagnostics() const;
std::vector<std::uint8_t> Patch(const std::vector<std::uint8_t>& sourceElf, const std::vector<Domain::ProgramHeader>& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector<Codegen::TrampolineSite>& trampolines) override;
@ -17,6 +18,7 @@ private:
bool windowsGui;
std::optional<std::vector<std::uint8_t>> icon;
std::string diagnostics;
std::filesystem::path iconName;
};
}

View File

@ -28,7 +28,7 @@ void writeGotStub(std::vector<PeSection>& sections, const std::uint32_t targetRv
}
WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional<std::vector<std::uint8_t>> icon) : windowsGui(windowsGui), icon(std::move(icon)) {
WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional<std::vector<std::uint8_t>> 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<std::uint8_t> WindowsPePatcher::Patch(const std::vector<std::uint8_t
sections.push_back({".reloc", nextRva, SectionRead | 0x02000040u, std::move(relocationData)});
nextRva = AlignRva(nextRva + sections.back().Data.size());
}
directories[2] = icon ? WindowsIconResourceBuilder().Build(*icon, "icon0.png", sections, nextRva) : PeDirectory{};
directories[2] = icon ? WindowsIconResourceBuilder().Build(*icon, iconName, sections, nextRva) : PeDirectory{};
return WindowsPeWriter().Write(sections, entryRva, directories, windowsGui);
}

View File

@ -1,46 +1,15 @@
#include <io/NativePath.hpp>
#include <domain/Types.hpp>
#ifdef _WIN32
#include <windows.h>
#include <utility>
#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<int>(path.size()), nullptr, 0);
if (wideSize <= 0) throw Domain::RelinkerException("Path is not valid UTF-8: " + path);
std::wstring wide(static_cast<std::size_t>(wideSize), L'\0');
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path.data(), static_cast<int>(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<int>(wide.size()), nullptr, 0, nullptr, nullptr);
if (utf8Size <= 0) throw Domain::RelinkerException("Path cannot be encoded as UTF-8");
std::string utf8(static_cast<std::size_t>(utf8Size), '\0');
if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide.data(), static_cast<int>(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
}

View File

@ -1,30 +1,13 @@
#include <NativeModules.hpp>
#include <ConsoleProgress.hpp>
#include <NativeModules.hpp>
#include <relinker/pipeline/Conversion.hpp>
#include <Cli.hpp>
#include <domain/Types.hpp>
#include <io/FileReader.hpp>
#include <io/NativePath.hpp>
#include <elfpatcher/linux/LinuxElfPatcher.hpp>
#include <elfpatcher/linux/LinuxDesktopEntryWriter.hpp>
#include <elfpatcher/general/SegmentFilter.hpp>
#include <elfpatcher/general/EntryStubBuilder.hpp>
#include <elfpatcher/general/ProgramHeaderLayoutBuilder.hpp>
#include <elfpatcher/general/SectionHeaderTableBuilder.hpp>
#include <elfpatcher/windows/WindowsElfPatcher.hpp>
#include <elfpatcher/macos/MacOsMachOPatcher.hpp>
#include <io/ByteWriter.hpp>
#include <io/FileWriter.hpp>
#include <relinker/parsing/ElfReader.hpp>
#include <relinker/analysis/ValidationPolicy.hpp>
#include <relinker/analysis/SyscallScanner.hpp>
#include <relinker/analysis/CallSiteResolver.hpp>
#include <relinker/analysis/UnusedNidFilter.hpp>
#include <relinker/output/SysVDynamicSectionBuilder.hpp>
#include <relinker/output/CallRegistryWriter.hpp>
#include <relinker/pipeline/RelinkerPipeline.hpp>
#include <relinker/guest/GuestImage.hpp>
#include <codegen/IAmd64OnlyConverter.hpp>
#include <map>
#include <codegen/CodegenException.hpp>
#include <algorithm>
#include <filesystem>
@ -43,8 +26,6 @@ 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;
@ -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<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(progress, 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(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<Relinker::GuestArtifact> 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<Io::ByteWriter>();
std::shared_ptr<Elfpatcher::IElfPatcher> patcher;
if (args.toMacos) {
patcher = std::make_shared<Elfpatcher::MacOs::MacOsMachOPatcher>();
} else if (args.toWindows) {
const auto iconPath = Io::NativePath(args.inputPath).parent_path() / "sce_sys" / "icon0.png";
std::optional<std::vector<std::uint8_t>> icon;
if (std::filesystem::exists(iconPath)) icon = Io::FileReader().Read(Io::Utf8Path(iconPath));
patcher = std::make_shared<Elfpatcher::Windows::WindowsPePatcher>(args.windowsGui, std::move(icon));
} 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;
}
if (args.windowsDiagnostics && mainExecutable) converted.Diagnostics = std::static_pointer_cast<Elfpatcher::Windows::WindowsPePatcher>(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<Relinker::CallRegistryWriter>();
for (std::size_t index = 0; index < executables.size(); ++index)

View File

@ -4,6 +4,7 @@
#include <relinker/domain/Types.hpp>
#include <cstddef>
#include <vector>
#include <string_view>
namespace Relinker {
@ -11,11 +12,7 @@ class ISyscallScanner {
public:
virtual ~ISyscallScanner() = default;
virtual std::vector<std::size_t> ScanCodeSectionForSyscalls(
const std::vector<std::uint8_t>& codeSection,
FileByteOffset codeSectionOffset,
FileByteOffset codeSectionSize
) = 0;
virtual std::vector<std::size_t> ScanCodeSectionForSyscalls(const std::vector<std::uint8_t>& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize, std::string_view input = {}) = 0;
};
}

View File

@ -0,0 +1,52 @@
#pragma once
#include <domain/IProgress.hpp>
#include <relinker/domain/RelinkResult.hpp>
#include <relinker/guest/GuestImage.hpp>
#include <optional>
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<std::string> ExcludedModules;
};
struct ConversionInput {
std::vector<std::uint8_t> Executable;
std::filesystem::path InputPath;
std::filesystem::path OutputPath;
std::filesystem::path ModuleRoot;
std::vector<GuestModuleInput> Modules;
std::vector<std::filesystem::path> RuntimeDirectories;
std::optional<std::vector<std::uint8_t>> Icon;
};
struct ConversionOutput {
std::vector<std::uint8_t> Executable;
std::vector<GuestArtifact> 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;
};
}

View File

@ -169,7 +169,7 @@ public:
std::vector<std::size_t> ScanCodeSectionForSyscalls(
const std::vector<std::uint8_t>& codeSection,
FileByteOffset codeSectionOffset,
FileByteOffset codeSectionSize) override;
FileByteOffset codeSectionSize, std::string_view input) override;
private:
Domain::IProgress& progress;
@ -179,16 +179,16 @@ private:
std::vector<std::size_t> SyscallScanner::ScanCodeSectionForSyscalls(
const std::vector<std::uint8_t>& 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<std::size_t>(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<std::size_t> 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<std::size_t>(match.Offset - codeSectionOffset);
const auto opcodeOffset = Codegen::DecodedInstruction{codeSection.data() + start, match.Length}.OpcodeOffset();
@ -218,7 +218,7 @@ std::vector<std::size_t> 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<std::size_t> ScanCodeSectionForSyscalls(
const std::vector<std::uint8_t>&,
FileByteOffset,
FileByteOffset
FileByteOffset,
std::string_view
) override { return {}; }
};

View File

@ -61,7 +61,8 @@ std::vector<GuestArtifact> 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<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
}
for (const auto& header : codeHeaders) {
const std::vector<std::uint8_t> 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;

View File

@ -0,0 +1,134 @@
#include <domain/Types.hpp>
#include <io/NativePath.hpp>
#include <elfpatcher/linux/LinuxElfPatcher.hpp>
#include <elfpatcher/general/SegmentFilter.hpp>
#include <elfpatcher/general/EntryStubBuilder.hpp>
#include <elfpatcher/general/ProgramHeaderLayoutBuilder.hpp>
#include <elfpatcher/general/SectionHeaderTableBuilder.hpp>
#include <elfpatcher/windows/WindowsElfPatcher.hpp>
#include <elfpatcher/macos/MacOsMachOPatcher.hpp>
#include <io/ByteWriter.hpp>
#include <relinker/parsing/ElfReader.hpp>
#include <relinker/analysis/ValidationPolicy.hpp>
#include <relinker/analysis/SyscallScanner.hpp>
#include <relinker/analysis/CallSiteResolver.hpp>
#include <relinker/analysis/UnusedNidFilter.hpp>
#include <relinker/output/SysVDynamicSectionBuilder.hpp>
#include <relinker/output/CallRegistryWriter.hpp>
#include <relinker/pipeline/RelinkerPipeline.hpp>
#include <relinker/guest/GuestImage.hpp>
#include <codegen/IAmd64OnlyConverter.hpp>
#include <map>
#include <codegen/CodegenException.hpp>
#include <algorithm>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
#include <relinker/pipeline/Conversion.hpp>
#include <sstream>
#include <bit>
#include <domain/InputProgress.hpp>
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<Codegen::TrampolineSite> 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<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::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<Relinker::ElfReader>(sourceBytes);
const std::shared_ptr<Relinker::ISyscallScanner> syscallScanner = options.SkipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(inputProgress, options.TrapSdkSyscallStubs);
const auto pipeline = std::make_shared<Relinker::RelinkerPipeline>(
elfReader,
syscallScanner,
Relinker::MakeCallSiteResolver(),
std::make_shared<Relinker::ValidationPolicy>(),
std::make_shared<Relinker::SysVDynamicSectionBuilder>(),
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<Relinker::GuestArtifact> 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<Io::ByteWriter>();
std::shared_ptr<Elfpatcher::IElfPatcher> patcher;
if (macos) {
patcher = std::make_shared<Elfpatcher::MacOs::MacOsMachOPatcher>();
} else if (windows) {
patcher = std::make_shared<Elfpatcher::Windows::WindowsPePatcher>(options.WindowsGui, std::move(input.Icon), input.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
);
}
inputProgress.Report({Domain::ProgressStage::Image, Io::Utf8Path(input.InputPath), 0, 1});
std::vector<std::uint8_t> 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<Elfpatcher::Windows::WindowsPePatcher>(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)};
}
}

View File

@ -0,0 +1,148 @@
#include "TestProgress.hpp"
#include <relinker/pipeline/Conversion.hpp>
#include <relinker/analysis/SyscallScanner.hpp>
#include <io/BufferUtils.hpp>
#include <algorithm>
#include <array>
#include <iostream>
#include <stdexcept>
namespace {
void require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
template<typename TAction>
void reject(TAction action) {
try { action(); } catch (const Domain::RelinkerException&) { return; }
throw std::runtime_error("Invalid conversion was accepted");
}
std::vector<std::uint8_t> fixture() {
std::vector<std::uint8_t> bytes(0x3000);
const std::array<std::uint8_t, 7> 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<std::pair<std::uint64_t, std::uint64_t>, 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<std::uint8_t>{};
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<std::uint8_t> 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;
}
}