mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-18 23:07:07 -05:00
feat(relinker): add conversion-time game patches
A per-game relinker-patches.toml (or --patches) replaces verified bytes in the input executable and bundled modules before conversion. Entries are checked against the expected bytes and against every in-place writer, the ELF header range and relocation targets. An <output-stem>.patches.toml manifest records the applied entries, and --verify-patches checks an input or a converted output. With no patch file the output is unchanged.
This commit is contained in:
1 parent
8625038579
commit
346b0a92ca
15 files changed
+1824
-24
No files matched your search
@@ -69,6 +69,7 @@ add_library(relinker_core STATIC
|
||||
${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/patching/GamePatch.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/pipeline/Conversion.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/pipeline/RelinkerPipeline.cpp
|
||||
)
|
||||
@@ -93,6 +94,10 @@ if(BUILD_TESTING)
|
||||
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)
|
||||
add_test_executable(relinker_game_patch_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/GamePatchTests.cpp)
|
||||
target_link_libraries(relinker_game_patch_tests PRIVATE relinker_core)
|
||||
add_test(NAME relinker_game_patch COMMAND relinker_game_patch_tests)
|
||||
set_tests_properties(relinker_game_patch PROPERTIES TIMEOUT 30)
|
||||
endif()
|
||||
|
||||
if(ANYPS5_BUILD_WASM)
|
||||
@@ -501,6 +506,8 @@ if(BUILD_TESTING)
|
||||
${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 game_patches COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_game_patches.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}
|
||||
|
||||
@@ -30,6 +30,10 @@ struct Args {
|
||||
std::string inputPath;
|
||||
std::string outputPath;
|
||||
std::string sceModulePath;
|
||||
std::string patchesPath;
|
||||
bool noPatches = false;
|
||||
std::string verifyPatchesPath;
|
||||
bool verifyOutput = false;
|
||||
std::string runPath = "$ORIGIN/libs";
|
||||
std::set<std::string> excludedSceModules;
|
||||
std::vector<std::string> moduleDirectories;
|
||||
|
||||
@@ -5,16 +5,36 @@
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
namespace Cli {
|
||||
|
||||
const char* Usage() {
|
||||
return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base <address>] [--linux-fixed-base <address>] [--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"
|
||||
return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--trace-imports] [--windows-fixed-base <address>] [--linux-fixed-base <address>] [--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] [--patches <file> | --no-patches] <input.elf> <output.elf>\n"
|
||||
" relinker --verify-patches <file> [--verify-output] [--skip-sce-module | --sce-module-path <path>] [--exclude-sce-module <file>]... [--module-dir <dir>]... [--executable <path>]... <path>\n"
|
||||
"Example: relinker input.elf output.elf";
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kVerifyRejectedOptions[] = {
|
||||
"--windows", "--macos", "--windows-gui", "--windows-diagnostics", "--windows-fixed-base", "--trace-imports", "--linux-fixed-base",
|
||||
"--to-intel", "--to-rosetta", "--skip-syscall-check", "--trap-sdk-syscall-stubs", "unused-filter=",
|
||||
"--rpath", "--registry", "--lazy-binding", "--autorun", "--patches", "--no-patches",
|
||||
};
|
||||
|
||||
constexpr std::string_view kModuleDiscoveryOptions[] = {"--sce-module-path", "--module-dir", "--exclude-sce-module", "--skip-sce-module"};
|
||||
|
||||
template<std::size_t Count>
|
||||
bool IsOneOf(const std::string& option, const std::string_view (&names)[Count]) {
|
||||
return std::find(std::begin(names), std::end(names), option) != std::end(names);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static std::uint64_t ParseLinuxFixedBase(std::string_view value) {
|
||||
constexpr std::uint64_t lowest = 0x10000;
|
||||
constexpr std::uint64_t highest = 0x7fffffff0000;
|
||||
@@ -32,9 +52,12 @@ Args ParseArgs(int argc, char* argv[]) {
|
||||
Args args;
|
||||
bool unusedFilterSpecified = false;
|
||||
bool sceModulePathSpecified = false;
|
||||
std::vector<std::string> givenOptions;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::string arg = argv[i];
|
||||
if (arg.rfind("--", 0) == 0) givenOptions.push_back(arg);
|
||||
else if (arg.rfind("unused-filter=", 0) == 0) givenOptions.push_back("unused-filter=");
|
||||
if (arg == "--help") {
|
||||
args.showHelp = true;
|
||||
} else if (arg == "--windows-fixed-base") {
|
||||
@@ -87,6 +110,18 @@ Args ParseArgs(int argc, char* argv[]) {
|
||||
throw std::runtime_error("unused-filter must be specified once with a value of 0, 1 or 2");
|
||||
args.unusedFilterLevel = static_cast<std::uint32_t>(value[0] - '0');
|
||||
unusedFilterSpecified = true;
|
||||
} else if (arg == "--patches") {
|
||||
if (!args.patchesPath.empty()) throw std::runtime_error("--patches cannot be repeated");
|
||||
if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--")) throw std::runtime_error("--patches requires a file");
|
||||
args.patchesPath = argv[++i];
|
||||
} else if (arg == "--no-patches") {
|
||||
args.noPatches = true;
|
||||
} else if (arg == "--verify-patches") {
|
||||
if (!args.verifyPatchesPath.empty()) throw std::runtime_error("--verify-patches cannot be repeated");
|
||||
if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--")) throw std::runtime_error("--verify-patches requires a file");
|
||||
args.verifyPatchesPath = argv[++i];
|
||||
} else if (arg == "--verify-output") {
|
||||
args.verifyOutput = true;
|
||||
} else if (arg == "--registry") {
|
||||
args.writeRegistry = true;
|
||||
} else if (arg == "--rpath") {
|
||||
@@ -120,6 +155,19 @@ Args ParseArgs(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
const bool verifying = !args.verifyPatchesPath.empty();
|
||||
if (args.verifyOutput && !verifying) throw std::runtime_error("--verify-output requires --verify-patches");
|
||||
if (!args.patchesPath.empty() && args.noPatches) throw std::runtime_error("--patches conflicts with --no-patches");
|
||||
if (verifying) {
|
||||
for (const auto& option : givenOptions) {
|
||||
if (IsOneOf(option, kVerifyRejectedOptions))
|
||||
throw std::runtime_error(option + " cannot be used with --verify-patches");
|
||||
if (args.verifyOutput && IsOneOf(option, kModuleDiscoveryOptions))
|
||||
throw std::runtime_error("module discovery options have no effect with --verify-output");
|
||||
}
|
||||
if (!args.outputPath.empty()) throw std::runtime_error("unexpected argument: " + args.outputPath);
|
||||
}
|
||||
|
||||
if (args.skipSceModule && sceModulePathSpecified) throw std::runtime_error("--sce-module-path conflicts with --skip-sce-module");
|
||||
|
||||
if (args.skipSyscallCheck && args.trapSdkSyscallStubs) throw std::runtime_error("--skip-syscall-check conflicts with --trap-sdk-syscall-stubs");
|
||||
@@ -154,11 +202,11 @@ Args ParseArgs(int argc, char* argv[]) {
|
||||
if (args.linuxFixedBase && args.lazyBinding)
|
||||
throw std::runtime_error("--linux-fixed-base conflicts with --lazy-binding");
|
||||
|
||||
if (!args.showHelp && (args.inputPath.empty() || args.outputPath.empty()))
|
||||
if (!args.showHelp && (args.inputPath.empty() || (!verifying && args.outputPath.empty())))
|
||||
throw std::runtime_error(Usage());
|
||||
|
||||
|
||||
if (!sceModulePathSpecified && !args.skipSceModule && !args.inputPath.empty()) args.sceModulePath = Io::Utf8Path(std::filesystem::absolute(Io::NativePath(args.inputPath)).parent_path());
|
||||
if (!sceModulePathSpecified && !args.skipSceModule && !args.inputPath.empty() && !args.verifyOutput) args.sceModulePath = Io::Utf8Path(std::filesystem::absolute(Io::NativePath(args.inputPath)).parent_path());
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -278,6 +278,43 @@ void targetSelection() {
|
||||
expectThrows({"--to-rosetta", "--windows", "input.elf", "output.elf"}, rosettaConflict);
|
||||
}
|
||||
|
||||
void patchOptions() {
|
||||
auto args = parse({"--patches", "p.toml", "in.elf", "out.elf"});
|
||||
require(args.patchesPath == "p.toml" && !args.noPatches, "--patches value");
|
||||
args = parse({"--no-patches", "in.elf", "out.elf"});
|
||||
require(args.noPatches && args.patchesPath.empty(), "--no-patches");
|
||||
expectThrows({"--patches", "p.toml", "--no-patches", "in.elf", "out.elf"}, "--patches conflicts with --no-patches");
|
||||
expectThrows({"--patches", "p.toml", "--patches", "q.toml", "in.elf", "out.elf"}, "--patches cannot be repeated");
|
||||
expectThrows({"in.elf", "out.elf", "--patches"}, "--patches requires a file");
|
||||
}
|
||||
|
||||
void verifyMode() {
|
||||
auto args = parse({"--verify-patches", "p.toml", "in.elf"});
|
||||
require(args.verifyPatchesPath == "p.toml" && args.inputPath == "in.elf" && args.outputPath.empty() && !args.verifyOutput, "verify positional");
|
||||
require(!args.sceModulePath.empty(), "input mode keeps the default module path");
|
||||
args = parse({"in.elf", "--verify-patches", "p.toml", "--sce-module-path", "m", "--module-dir", "d", "--exclude-sce-module", "x.prx", "--executable", "a/b.bin"});
|
||||
require(args.sceModulePath == "m" && args.executables.size() == 1, "input mode options");
|
||||
args = parse({"--verify-patches", "p.toml", "--skip-sce-module", "in.elf"});
|
||||
require(args.skipSceModule, "input mode --skip-sce-module");
|
||||
args = parse({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--executable", "a/b.bin"});
|
||||
require(args.verifyOutput && args.sceModulePath.empty() && args.executables.size() == 1, "output mode accepts --executable");
|
||||
const std::string noEffect = "module discovery options have no effect with --verify-output";
|
||||
expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--sce-module-path", "m"}, noEffect);
|
||||
expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--module-dir", "d"}, noEffect);
|
||||
expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--exclude-sce-module", "x.prx"}, noEffect);
|
||||
expectThrows({"--verify-patches", "p.toml", "--verify-output", "out.elf", "--skip-sce-module"}, noEffect);
|
||||
expectThrows({"--verify-patches", "p.toml", "in.elf", "extra"}, "unexpected argument: extra");
|
||||
for (const auto* option : {"--windows", "--macos", "--to-intel", "--to-rosetta", "--trace-imports", "--lazy-binding", "--autorun", "--registry", "--skip-syscall-check", "--no-patches"})
|
||||
expectThrows({"--verify-patches", "p.toml", option, "in.elf"}, std::string(option) + " cannot be used with --verify-patches");
|
||||
expectThrows({"--verify-patches", "p.toml", "--rpath", "x", "in.elf"}, "--rpath cannot be used with --verify-patches");
|
||||
expectThrows({"--verify-patches", "p.toml", "--patches", "q.toml", "in.elf"}, "--patches cannot be used with --verify-patches");
|
||||
expectThrows({"--verify-patches", "p.toml", "unused-filter=1", "in.elf"}, "unused-filter= cannot be used with --verify-patches");
|
||||
expectThrows({"--verify-patches", "p.toml", "--linux-fixed-base", "0x10000", "in.elf"}, "--linux-fixed-base cannot be used with --verify-patches");
|
||||
expectThrows({"--verify-output", "in.elf", "out.elf"}, "--verify-output requires --verify-patches");
|
||||
expectThrows({"--verify-patches"}, "--verify-patches requires a file");
|
||||
expectUsage({"--verify-patches", "p.toml"});
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
defaults();
|
||||
@@ -308,6 +345,8 @@ int main() {
|
||||
missingPositionals();
|
||||
windowsFixedBase();
|
||||
linuxFixedBase();
|
||||
patchOptions();
|
||||
verifyMode();
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
|
||||
+127
-18
@@ -10,6 +10,7 @@
|
||||
#include <io/FileWriter.hpp>
|
||||
#include <relinker/output/CallRegistryWriter.hpp>
|
||||
#include <codegen/CodegenException.hpp>
|
||||
#include <relinker/patching/GamePatch.hpp>
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
@@ -32,9 +33,10 @@ struct ConvertedExecutable {
|
||||
std::vector<Relinker::GuestArtifact> GuestArtifacts;
|
||||
std::vector<std::uint8_t> Bytes;
|
||||
std::string Diagnostics;
|
||||
std::vector<Relinker::GamePatch::AppliedPatch> AppliedPatches;
|
||||
};
|
||||
|
||||
ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable, Cli::ConsoleProgress& progress) {
|
||||
ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, const std::string& absPath, const bool mainExecutable, const std::string& targetName, const std::vector<Relinker::GamePatch::PatchEntry>& patches, Cli::ConsoleProgress& progress) {
|
||||
Io::FileReader fileReader;
|
||||
Relinker::ConversionInput input;
|
||||
input.Executable = fileReader.Read(inputPath);
|
||||
@@ -63,11 +65,13 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath,
|
||||
options.WindowsDiagnostics = args.windowsDiagnostics && mainExecutable;
|
||||
options.LinuxFixedBase = args.linuxFixedBase;
|
||||
options.ExcludedModules = args.excludedSceModules;
|
||||
options.Patches = patches;
|
||||
options.TargetName = targetName;
|
||||
progress.IncludeFile(Io::Utf8Path(input.InputPath));
|
||||
progress.IncludeFile(Io::Utf8Path(input.OutputPath));
|
||||
for (const auto& module : input.Modules) progress.IncludeFile(Io::Utf8Path(module.Path));
|
||||
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)};
|
||||
return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches)};
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -164,6 +168,93 @@ bool SameOutputDestination(const std::filesystem::path& first, const std::filesy
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string ReadText(const std::string& path) {
|
||||
const auto bytes = Io::FileReader().Read(path);
|
||||
return std::string(bytes.begin(), bytes.end());
|
||||
}
|
||||
|
||||
std::filesystem::path ExecutableRelative(const std::string& executable) {
|
||||
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);
|
||||
return relative;
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> ModuleDirectories(const Cli::Args& args, const std::filesystem::path& moduleRoot) {
|
||||
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();
|
||||
return moduleDirectories;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveExtraExecutable(const std::filesystem::path& moduleRoot, const std::vector<std::filesystem::path>& moduleDirectories, const std::filesystem::path& relative, const std::string& 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::any_of(moduleDirectories.begin(), moduleDirectories.end(), [&](const auto& directory) { return Cli::SameDirectory(directory, input.parent_path()); }))
|
||||
throw Domain::RelinkerException("--executable is in a guest module directory: " + executable);
|
||||
return input;
|
||||
}
|
||||
|
||||
std::vector<Relinker::GamePatch::PatchEntry> LoadPatches(const Cli::Args& args) {
|
||||
if (args.noPatches) return {};
|
||||
std::filesystem::path path;
|
||||
if (!args.patchesPath.empty()) {
|
||||
path = Io::NativePath(args.patchesPath);
|
||||
if (!std::filesystem::is_regular_file(path)) throw Domain::RelinkerException("Patch file not found: " + args.patchesPath);
|
||||
} else {
|
||||
path = Io::NativePath(args.inputPath).parent_path() / Relinker::GamePatch::PatchFileName;
|
||||
if (!std::filesystem::is_regular_file(path)) return {};
|
||||
}
|
||||
std::cout << "Patches: " << Io::Utf8Path(path) << '\n';
|
||||
return Relinker::GamePatch::ParsePatchFile(ReadText(Io::Utf8Path(path))).Entries;
|
||||
}
|
||||
|
||||
int RunVerifyPatches(const Cli::Args& args) {
|
||||
using namespace Relinker::GamePatch;
|
||||
const auto file = ParsePatchFile(ReadText(args.verifyPatchesPath));
|
||||
Io::FileReader reader;
|
||||
std::vector<VerifyTarget> executables;
|
||||
std::vector<VerifyTarget> modules;
|
||||
const auto inputPath = Io::NativePath(args.inputPath);
|
||||
if (!std::filesystem::is_regular_file(inputPath))
|
||||
throw Domain::RelinkerException((args.verifyOutput ? "output not found: " : "input not found: ") + args.inputPath);
|
||||
if (args.verifyOutput) {
|
||||
executables.push_back({file.MainTarget.value_or(std::string()), reader.Read(args.inputPath)});
|
||||
for (const auto& executable : args.executables) {
|
||||
const auto relative = ExecutableRelative(executable);
|
||||
const auto output = OutputExecutablePath(inputPath, executable);
|
||||
if (!std::filesystem::is_regular_file(output)) throw Domain::RelinkerException("output not found: " + Io::Utf8Path(output));
|
||||
executables.push_back({relative.generic_string(), reader.Read(Io::Utf8Path(output))});
|
||||
}
|
||||
} else {
|
||||
executables.push_back({Io::Utf8Path(inputPath.filename()), reader.Read(args.inputPath)});
|
||||
std::filesystem::path moduleRoot = args.executables.empty() ? std::filesystem::path() : std::filesystem::absolute(Io::NativePath(args.sceModulePath)).lexically_normal();
|
||||
if (!args.skipSceModule) {
|
||||
auto native = Cli::ReadModules(std::filesystem::absolute(inputPath).lexically_normal(), Io::NativePath(args.sceModulePath), args.excludedSceModules, args.moduleDirectories);
|
||||
for (auto& module : native.Modules) modules.push_back({ModuleTargetName(module.Path, native.Root), std::move(module.Bytes)});
|
||||
}
|
||||
const auto moduleDirectories = ModuleDirectories(args, moduleRoot);
|
||||
for (const auto& executable : args.executables) {
|
||||
const auto relative = ExecutableRelative(executable);
|
||||
const auto input = ResolveExtraExecutable(moduleRoot, moduleDirectories, relative, executable);
|
||||
executables.push_back({relative.generic_string(), reader.Read(Io::Utf8Path(input))});
|
||||
}
|
||||
}
|
||||
const auto results = VerifyPatches(file, executables, modules, args.verifyOutput);
|
||||
std::size_t mismatches = 0;
|
||||
std::size_t notChecked = 0;
|
||||
for (const auto& result : results) {
|
||||
std::cout << StatusName(result.Status) << ": " << (result.Name.empty() ? std::string() : result.Name + " ") << result.Target << " va=0x" << std::hex << result.VirtualAddress;
|
||||
if (result.Status != VerifyStatus::NotChecked) std::cout << " off=0x" << result.Offset;
|
||||
std::cout << std::dec << ' ' << (result.Status == VerifyStatus::NotChecked ? "(" + result.Detail + ")" : result.Detail) << '\n';
|
||||
if (result.Status == VerifyStatus::Mismatch) ++mismatches;
|
||||
if (result.Status == VerifyStatus::NotChecked) ++notChecked;
|
||||
}
|
||||
std::cout << "Verified " << results.size() << " patches: " << results.size() - mismatches - notChecked << " ok, " << notChecked << " not-checked, " << mismatches << " mismatch\n";
|
||||
return mismatches == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
int Run(const int argc, char* argv[]) {
|
||||
Cli::Args args;
|
||||
try {
|
||||
@@ -180,6 +271,7 @@ int Run(const int argc, char* argv[]) {
|
||||
|
||||
Cli::ConsoleProgress progress(std::cout);
|
||||
try {
|
||||
if (!args.verifyPatchesPath.empty()) return RunVerifyPatches(args);
|
||||
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";
|
||||
@@ -200,21 +292,11 @@ int Run(const int argc, char* argv[]) {
|
||||
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();
|
||||
const auto moduleDirectories = ModuleDirectories(args, moduleRoot);
|
||||
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::any_of(moduleDirectories.begin(), moduleDirectories.end(), [&](const auto& directory) { return Cli::SameDirectory(directory, input.parent_path()); }))
|
||||
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()));
|
||||
const auto relative = ExecutableRelative(executable);
|
||||
const auto input = ResolveExtraExecutable(moduleRoot, moduleDirectories, relative, executable);
|
||||
const auto output = Relinker::GamePatch::OutputExecutablePath(Io::NativePath(absPath), executable);
|
||||
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) {
|
||||
@@ -224,6 +306,8 @@ int Run(const int argc, char* argv[]) {
|
||||
extraExecutables.push_back({executable, relative, input, output});
|
||||
}
|
||||
|
||||
const auto patches = LoadPatches(args);
|
||||
const std::string mainTarget = Io::Utf8Path(Io::NativePath(args.inputPath).filename());
|
||||
progress.IncludeFile(args.inputPath);
|
||||
progress.IncludeFile(absPath);
|
||||
for (const auto& extra : extraExecutables) {
|
||||
@@ -231,10 +315,10 @@ int Run(const int argc, char* argv[]) {
|
||||
progress.IncludeFile(Io::Utf8Path(extra.Output));
|
||||
}
|
||||
std::vector<ConvertedExecutable> executables;
|
||||
executables.push_back(Convert(args, args.inputPath, absPath, true, progress));
|
||||
executables.push_back(Convert(args, args.inputPath, absPath, true, mainTarget, patches, progress));
|
||||
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, progress);
|
||||
auto converted = Convert(args, Io::Utf8Path(input), Io::Utf8Path(output), false, relative.generic_string(), patches, progress);
|
||||
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;
|
||||
@@ -243,6 +327,23 @@ int Run(const int argc, char* argv[]) {
|
||||
executables.push_back(std::move(converted));
|
||||
}
|
||||
const auto& guestArtifacts = executables.front().GuestArtifacts;
|
||||
if (!patches.empty()) {
|
||||
std::vector<std::string> targetNames{mainTarget};
|
||||
for (const auto& extra : extraExecutables) targetNames.push_back(extra.Relative.generic_string());
|
||||
if (!guestArtifacts.empty()) {
|
||||
const auto patchModuleRoot = std::filesystem::absolute(Io::NativePath(args.sceModulePath)).lexically_normal();
|
||||
for (const auto& artifact : guestArtifacts) targetNames.push_back(Relinker::GamePatch::ModuleTargetName(artifact.SourcePath, patchModuleRoot));
|
||||
}
|
||||
Relinker::GamePatch::MatchTargets(patches, targetNames);
|
||||
}
|
||||
std::vector<std::vector<Relinker::GamePatch::AppliedPatch>> appliedRecords;
|
||||
for (const auto& executable : executables) appliedRecords.push_back(executable.AppliedPatches);
|
||||
const auto appliedPatches = Relinker::GamePatch::AggregateManifest(appliedRecords);
|
||||
const auto patchFile = args.patchesPath.empty() ? Io::NativePath(args.inputPath).parent_path() / Relinker::GamePatch::PatchFileName : Io::NativePath(args.patchesPath);
|
||||
std::filesystem::path manifestPath;
|
||||
std::string manifestText;
|
||||
if (!appliedPatches.empty()) manifestPath = Io::NativePath(absPath).parent_path() / Io::NativePath(Io::Utf8Path(Io::NativePath(absPath).stem()) + Relinker::GamePatch::ManifestSuffix);
|
||||
if (!manifestPath.empty()) manifestText = Relinker::GamePatch::WriteManifest(appliedPatches, mainTarget, Io::Utf8Path(patchFile));
|
||||
|
||||
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));
|
||||
@@ -284,6 +385,11 @@ int Run(const int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
checkOutputInputs(Io::NativePath(absPath), "Executable");
|
||||
if (!manifestPath.empty()) {
|
||||
checkOutputInputs(manifestPath, "Patch manifest");
|
||||
if (std::filesystem::exists(manifestPath) && std::filesystem::exists(patchFile) && std::filesystem::equivalent(manifestPath, patchFile))
|
||||
throw Domain::RelinkerException("Patch manifest output would overwrite the patch file: " + Io::Utf8Path(manifestPath));
|
||||
}
|
||||
for (const auto& artifact : guestArtifacts) checkOutputInputs(artifact.Path, "Guest module");
|
||||
|
||||
std::vector<std::filesystem::path> outputPaths;
|
||||
@@ -291,6 +397,7 @@ int Run(const int argc, char* argv[]) {
|
||||
for (const auto& artifact : guestArtifacts) outputPaths.push_back(artifact.Path);
|
||||
outputPaths.insert(outputPaths.end(), registryPaths.begin(), registryPaths.end());
|
||||
outputPaths.insert(outputPaths.end(), neededPaths.begin(), neededPaths.end());
|
||||
if (!manifestPath.empty()) outputPaths.push_back(manifestPath);
|
||||
if (desktopEntry) {
|
||||
outputPaths.push_back(desktopEntry->IconPath);
|
||||
outputPaths.push_back(desktopEntry->EntryPath);
|
||||
@@ -334,7 +441,9 @@ int Run(const int argc, char* argv[]) {
|
||||
fileWriter.Write(Io::Utf8Path(neededPaths[executables.size() + index]), registryWriter->WriteNeededLibraries(guestArtifacts[index].NeededLibraries));
|
||||
}
|
||||
}
|
||||
if (!manifestPath.empty()) fileWriter.Write(Io::Utf8Path(manifestPath), manifestText);
|
||||
progress.Finish();
|
||||
if (!manifestPath.empty()) std::cout << "Patch manifest: " << Io::Utf8Path(manifestPath) << '\n';
|
||||
if (desktopEntry) std::cout << "Desktop entry: " << Io::Utf8Path(desktopEntryWriter.Write(*desktopEntry)) << '\n';
|
||||
std::cout << "\nConversion complete\n"
|
||||
<< "Output file: " << absPath << '\n'
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <codegen/CodegenTypes.hpp>
|
||||
#include <domain/Types.hpp>
|
||||
#include <relinker/domain/ISyscallScanner.hpp>
|
||||
#include <relinker/patching/GamePatch.hpp>
|
||||
#include <filesystem>
|
||||
#include <domain/IProgress.hpp>
|
||||
#include <set>
|
||||
@@ -43,6 +44,7 @@ struct GuestImage {
|
||||
std::vector<std::string> ModuleNames;
|
||||
std::vector<std::uint8_t> Bytes;
|
||||
std::vector<Codegen::TrampolineSite> Trampolines;
|
||||
std::vector<GamePatch::PatchedRange> PatchedRanges;
|
||||
std::vector<Domain::ProgramHeader> Headers;
|
||||
std::vector<GuestSymbol> Symbols;
|
||||
std::vector<std::string> Dependencies;
|
||||
@@ -72,6 +74,7 @@ struct GuestArtifact {
|
||||
struct GuestModuleInput {
|
||||
std::filesystem::path Path;
|
||||
std::vector<std::uint8_t> Bytes;
|
||||
std::vector<GamePatch::PatchedRange> PatchedRanges;
|
||||
};
|
||||
|
||||
class GuestModuleBuilder {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#pragma once
|
||||
|
||||
#include <codegen/CodegenTypes.hpp>
|
||||
#include <relinker/domain/RelinkResult.hpp>
|
||||
#include <relinker/parsing/ElfReader.hpp>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace Relinker::GamePatch {
|
||||
|
||||
inline constexpr char PatchFileName[] = "relinker-patches.toml";
|
||||
inline constexpr char ManifestSuffix[] = ".patches.toml";
|
||||
|
||||
struct PatchInteger {
|
||||
bool Negative = false;
|
||||
std::uint64_t Magnitude = 0;
|
||||
};
|
||||
|
||||
struct PatchEntry {
|
||||
std::string Target;
|
||||
std::uint64_t VirtualAddress = 0;
|
||||
std::vector<std::uint8_t> Expected;
|
||||
std::vector<std::uint8_t> Value;
|
||||
std::string Description;
|
||||
std::string Name;
|
||||
std::string Type;
|
||||
PatchInteger ExpectedNumber;
|
||||
PatchInteger ValueNumber;
|
||||
std::optional<PatchInteger> Min;
|
||||
std::optional<PatchInteger> Max;
|
||||
std::optional<PatchInteger> Align;
|
||||
std::size_t Line = 0;
|
||||
};
|
||||
|
||||
struct PatchFile {
|
||||
std::vector<PatchEntry> Entries;
|
||||
std::optional<std::string> MainTarget;
|
||||
};
|
||||
|
||||
struct PatchedRange {
|
||||
std::uint64_t Offset = 0;
|
||||
std::uint64_t Length = 0;
|
||||
std::uint64_t VirtualAddress = 0;
|
||||
};
|
||||
|
||||
struct AppliedPatch {
|
||||
std::string Target;
|
||||
std::uint64_t VirtualAddress = 0;
|
||||
std::uint64_t Offset = 0;
|
||||
std::vector<std::uint8_t> Expected;
|
||||
std::vector<std::uint8_t> Value;
|
||||
std::string Description;
|
||||
std::string Name;
|
||||
std::string Type;
|
||||
PatchInteger ExpectedNumber;
|
||||
PatchInteger ValueNumber;
|
||||
std::optional<PatchInteger> Min;
|
||||
std::optional<PatchInteger> Max;
|
||||
std::optional<PatchInteger> Align;
|
||||
};
|
||||
|
||||
struct PatchedTarget {
|
||||
std::vector<AppliedPatch> Applied;
|
||||
std::vector<PatchedRange> Ranges;
|
||||
};
|
||||
|
||||
struct WriterRange {
|
||||
std::uint64_t Offset = 0;
|
||||
std::uint64_t Length = 0;
|
||||
std::string Writer;
|
||||
};
|
||||
|
||||
struct WriterSites {
|
||||
std::span<const Codegen::Amd64OnlySubstitutionReport> Reports;
|
||||
std::span<const Codegen::TrampolineSite> Trampolines;
|
||||
std::span<const RelinkPatch> RelinkPatches;
|
||||
std::span<const WriterRange> Traps;
|
||||
std::span<const std::uint8_t> RelaData;
|
||||
std::span<const std::uint8_t> RelaPltData;
|
||||
};
|
||||
|
||||
enum class VerifyStatus { Applicable, Applied, NotChecked, Mismatch };
|
||||
|
||||
struct VerifyTarget {
|
||||
std::string Name;
|
||||
std::vector<std::uint8_t> Bytes;
|
||||
};
|
||||
|
||||
struct VerifyResult {
|
||||
std::string Name;
|
||||
std::string Target;
|
||||
std::uint64_t VirtualAddress = 0;
|
||||
std::uint64_t Offset = 0;
|
||||
VerifyStatus Status = VerifyStatus::Mismatch;
|
||||
std::string Detail;
|
||||
};
|
||||
|
||||
PatchFile ParsePatchFile(std::string_view text);
|
||||
|
||||
std::vector<PatchedRange> ResolveRanges(const ElfReader& reader, const std::vector<PatchEntry>& entries);
|
||||
|
||||
std::vector<AppliedPatch> ApplyPatches(std::vector<std::uint8_t>& bytes, const std::string& target, const std::vector<PatchEntry>& entries, const std::vector<PatchedRange>& ranges);
|
||||
|
||||
PatchedTarget PatchTarget(std::vector<std::uint8_t>& bytes, const std::string& target, const std::vector<PatchEntry>& entries);
|
||||
|
||||
void CheckOverlaps(const std::string& target, const std::vector<PatchedRange>& patched, const WriterSites& writers);
|
||||
|
||||
void MatchTargets(const std::vector<PatchEntry>& entries, const std::vector<std::string>& targetNames);
|
||||
|
||||
std::vector<AppliedPatch> AggregateManifest(const std::vector<std::vector<AppliedPatch>>& records);
|
||||
|
||||
std::string WriteManifest(const std::vector<AppliedPatch>& patches, const std::string& mainTarget, const std::string& configPath);
|
||||
|
||||
std::vector<VerifyResult> VerifyPatches(const PatchFile& file, const std::vector<VerifyTarget>& executables, const std::vector<VerifyTarget>& modules, bool outputMode);
|
||||
|
||||
std::string_view StatusName(VerifyStatus status);
|
||||
|
||||
std::filesystem::path OutputExecutablePath(const std::filesystem::path& outputPath, const std::string& executableArgument);
|
||||
|
||||
std::string Hex(std::span<const std::uint8_t> bytes);
|
||||
|
||||
std::string FormatApplied(const AppliedPatch& patch);
|
||||
|
||||
std::string ModuleTargetName(const std::filesystem::path& modulePath, const std::filesystem::path& moduleRoot);
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <domain/IProgress.hpp>
|
||||
#include <relinker/domain/RelinkResult.hpp>
|
||||
#include <relinker/guest/GuestImage.hpp>
|
||||
#include <relinker/patching/GamePatch.hpp>
|
||||
#include <optional>
|
||||
|
||||
namespace Relinker {
|
||||
@@ -24,6 +25,8 @@ struct ConversionOptions {
|
||||
std::optional<std::uint64_t> LinuxFixedBase;
|
||||
std::string RunPath = "$ORIGIN/libs";
|
||||
std::set<std::string> ExcludedModules;
|
||||
std::vector<GamePatch::PatchEntry> Patches;
|
||||
std::string TargetName;
|
||||
};
|
||||
|
||||
struct ConversionInput {
|
||||
@@ -41,6 +44,7 @@ struct ConversionOutput {
|
||||
std::vector<GuestArtifact> Modules;
|
||||
RelinkResult Analysis;
|
||||
std::string WindowsDiagnostics;
|
||||
std::vector<GamePatch::AppliedPatch> AppliedPatches;
|
||||
};
|
||||
|
||||
class Conversion {
|
||||
|
||||
@@ -70,7 +70,9 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
|
||||
if (excludedModules.contains(Io::Utf8Path(path.filename()))) throw Domain::RelinkerException("Excluded module supplied to converter: " + Io::Utf8Path(path));
|
||||
if (discovered.contains(path)) throw Domain::RelinkerException("Duplicate module path: " + Io::Utf8Path(path));
|
||||
paths.push_back(path);
|
||||
discovered.emplace(path, GuestImageReader().Read(path, std::move(module.Bytes), windows));
|
||||
auto image = GuestImageReader().Read(path, std::move(module.Bytes), windows);
|
||||
image.PatchedRanges = std::move(module.PatchedRanges);
|
||||
discovered.emplace(path, std::move(image));
|
||||
}
|
||||
const auto neededNames = ReadNeededNames(dynamic);
|
||||
std::set<std::string> missingNeeded;
|
||||
@@ -337,7 +339,9 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
|
||||
auto converted = Codegen::MakeAmd64OnlyConverter(toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel, [&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Lowering, Io::Utf8Path(image.SourcePath), completed, total}); })->Convert(std::move(image.Bytes), image.Headers);
|
||||
image.Trampolines = std::move(converted.Trampolines);
|
||||
image.Bytes = std::move(converted.Bytes);
|
||||
if (!image.PatchedRanges.empty()) GamePatch::CheckOverlaps(Io::Utf8Path(image.SourcePath), image.PatchedRanges, {.Reports = converted.Reports, .Trampolines = image.Trampolines});
|
||||
}
|
||||
if (!image.PatchedRanges.empty()) GamePatch::CheckOverlaps(Io::Utf8Path(image.SourcePath), image.PatchedRanges, {.RelaData = image.Dynamic.RelaData, .RelaPltData = image.Dynamic.RelaPltData});
|
||||
const bool sceImage = std::any_of(image.Headers.begin(), image.Headers.end(), [](const Domain::ProgramHeader& header) { return header.Type == 0x61000000; });
|
||||
const auto unwindFunctions = sceImage ? UnusedNidFilter::ReadExceptionFunctions(image.Bytes, image.Headers, {}, {}) : std::vector<UnusedNidFilter::StrictCodeRegion>{};
|
||||
for (const auto& header : codeHeaders) {
|
||||
@@ -348,6 +352,10 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
|
||||
for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), 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);
|
||||
if (!image.PatchedRanges.empty()) {
|
||||
const GamePatch::WriterRange trap{header.Offset + offset, 2, "syscall trap"};
|
||||
GamePatch::CheckOverlaps(Io::Utf8Path(image.SourcePath), image.PatchedRanges, {.Traps = std::span<const GamePatch::WriterRange>(&trap, 1)});
|
||||
}
|
||||
image.Bytes[header.Offset + offset] = 0x0F;
|
||||
image.Bytes[header.Offset + offset + 1] = 0x0B;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,602 @@
|
||||
#include <relinker/patching/GamePatch.hpp>
|
||||
#include <domain/Types.hpp>
|
||||
#include <elfpatcher/general/ElfConstants.hpp>
|
||||
#include <io/NativePath.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace Relinker::GamePatch {
|
||||
|
||||
namespace {
|
||||
|
||||
using Integer = PatchInteger;
|
||||
|
||||
struct RawValue {
|
||||
bool IsString = false;
|
||||
std::string Text;
|
||||
Integer Number;
|
||||
std::size_t Line = 0;
|
||||
};
|
||||
|
||||
struct IntegerType {
|
||||
std::string_view Name;
|
||||
unsigned Bits;
|
||||
bool Signed;
|
||||
};
|
||||
|
||||
constexpr std::array<IntegerType, 8> kIntegerTypes{{
|
||||
{"u8", 8, false}, {"u16", 16, false}, {"u32", 32, false}, {"u64", 64, false},
|
||||
{"i8", 8, true}, {"i16", 16, true}, {"i32", 32, true}, {"i64", 64, true},
|
||||
}};
|
||||
|
||||
constexpr std::array<std::string_view, 7> kIgnoredTopLevelKeys{"relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"};
|
||||
constexpr std::array<std::string_view, 8> kReservedNames{"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"};
|
||||
constexpr std::array<std::string_view, 10> kEntryKeys{"name", "target", "va", "type", "expected", "value", "min", "max", "align", "description"};
|
||||
|
||||
std::string HexValue(const std::uint64_t value) {
|
||||
std::ostringstream stream;
|
||||
stream << "0x" << std::hex << value;
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string IntegerText(const Integer& number) {
|
||||
return number.Negative ? "-" + std::to_string(number.Magnitude) : HexValue(number.Magnitude);
|
||||
}
|
||||
|
||||
[[noreturn]] void FailLine(const std::size_t line, const std::string& message, const std::string_view text) {
|
||||
throw Domain::RelinkerException("patch file line " + std::to_string(line) + ": " + message + ": `" + std::string(text) + "`");
|
||||
}
|
||||
|
||||
std::string_view Trim(std::string_view text) {
|
||||
constexpr std::string_view whitespace = " \t\r";
|
||||
const auto first = text.find_first_not_of(whitespace);
|
||||
if (first == std::string_view::npos) return {};
|
||||
return text.substr(first, text.find_last_not_of(whitespace) - first + 1);
|
||||
}
|
||||
|
||||
std::string_view StripComment(const std::string_view line) {
|
||||
bool inString = false;
|
||||
for (std::size_t index = 0; index < line.size(); ++index) {
|
||||
if (line[index] == '"') inString = !inString;
|
||||
else if (line[index] == '#' && !inString) return line.substr(0, index);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
bool Contains(const auto& names, const std::string_view name) {
|
||||
return std::find(names.begin(), names.end(), name) != names.end();
|
||||
}
|
||||
|
||||
bool IsKeyCharacter(const char character) {
|
||||
return (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || character == '_';
|
||||
}
|
||||
|
||||
int HexDigit(const char character) {
|
||||
if (character >= '0' && character <= '9') return character - '0';
|
||||
if (character >= 'a' && character <= 'f') return character - 'a' + 10;
|
||||
if (character >= 'A' && character <= 'F') return character - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
Integer ParseInteger(const std::string_view text, const std::size_t line, const std::string_view whole) {
|
||||
std::string_view digits = text;
|
||||
Integer number;
|
||||
if (digits.starts_with('-')) {
|
||||
number.Negative = true;
|
||||
digits.remove_prefix(1);
|
||||
}
|
||||
if (digits.empty()) FailLine(line, "unsupported value", whole);
|
||||
const bool hexadecimal = digits.starts_with("0x");
|
||||
if (hexadecimal) {
|
||||
if (number.Negative) FailLine(line, "hexadecimal integers cannot be negative", whole);
|
||||
digits.remove_prefix(2);
|
||||
if (digits.empty()) FailLine(line, "unsupported value", whole);
|
||||
}
|
||||
if (!hexadecimal && digits.size() > 1 && digits.front() == '0') FailLine(line, "decimal integers cannot have leading zeros", whole);
|
||||
for (const char character : digits) {
|
||||
const int digit = HexDigit(character);
|
||||
if (digit < 0 || (!hexadecimal && digit > 9)) {
|
||||
if (!hexadecimal && (character == '.' || character == 'e' || character == 'E')) FailLine(line, "floats are not supported", whole);
|
||||
FailLine(line, "unsupported value", whole);
|
||||
}
|
||||
const std::uint64_t base = hexadecimal ? 16 : 10;
|
||||
if (number.Magnitude > (UINT64_MAX - static_cast<std::uint64_t>(digit)) / base) FailLine(line, "integer is out of range", whole);
|
||||
number.Magnitude = number.Magnitude * base + static_cast<std::uint64_t>(digit);
|
||||
}
|
||||
if (number.Magnitude == 0) number.Negative = false;
|
||||
return number;
|
||||
}
|
||||
|
||||
RawValue ParseValue(const std::string_view text, const std::size_t line, const std::string_view whole) {
|
||||
RawValue value;
|
||||
value.Line = line;
|
||||
if (text.empty()) FailLine(line, "missing value", whole);
|
||||
if (text.front() == '"') {
|
||||
if (text.starts_with("\"\"\"")) FailLine(line, "multiline strings are not supported", whole);
|
||||
const auto end = text.find('"', 1);
|
||||
if (end == std::string_view::npos) FailLine(line, "unterminated string", whole);
|
||||
if (end + 1 != text.size()) FailLine(line, "unexpected text after string", whole);
|
||||
const auto content = text.substr(1, end - 1);
|
||||
if (content.find('\\') != std::string_view::npos) FailLine(line, "escapes are not supported in strings", whole);
|
||||
for (const char character : content)
|
||||
if (static_cast<unsigned char>(character) < 0x20 && character != '\t') FailLine(line, "control characters are not supported in strings", whole);
|
||||
value.IsString = true;
|
||||
value.Text = std::string(content);
|
||||
return value;
|
||||
}
|
||||
if (text.front() == '\'') FailLine(line, "single-quoted strings are not supported", whole);
|
||||
if (text.front() == '[') FailLine(line, "arrays are not supported", whole);
|
||||
if (text.front() == '{') FailLine(line, "inline tables are not supported", whole);
|
||||
if (text == "true" || text == "false") FailLine(line, "booleans are not supported", whole);
|
||||
if (text == "inf" || text == "nan" || text.front() == '+') FailLine(line, "unsupported value", whole);
|
||||
value.Number = ParseInteger(text, line, whole);
|
||||
return value;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> ParseHexBytes(const RawValue& value, const std::string_view key, const std::string_view whole) {
|
||||
if (!value.IsString) FailLine(value.Line, std::string(key) + " must be a hex byte string without a type", whole);
|
||||
if (value.Text.empty() || value.Text.size() % 2 != 0) FailLine(value.Line, std::string(key) + " must be a nonempty even-length hex byte string", value.Text);
|
||||
std::vector<std::uint8_t> bytes;
|
||||
for (std::size_t index = 0; index < value.Text.size(); index += 2) {
|
||||
const int high = HexDigit(value.Text[index]);
|
||||
const int low = HexDigit(value.Text[index + 1]);
|
||||
if (high < 0 || low < 0) FailLine(value.Line, std::string(key) + " contains a non-hex character", value.Text);
|
||||
bytes.push_back(static_cast<std::uint8_t>(high * 16 + low));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
bool IntegerLess(const Integer& left, const Integer& right) {
|
||||
if (left.Negative != right.Negative) return left.Negative;
|
||||
return left.Negative ? left.Magnitude > right.Magnitude : left.Magnitude < right.Magnitude;
|
||||
}
|
||||
|
||||
bool FitsType(const Integer& number, const IntegerType& type) {
|
||||
if (!type.Signed) return !number.Negative && (type.Bits == 64 || number.Magnitude < (std::uint64_t{1} << type.Bits));
|
||||
const std::uint64_t limit = std::uint64_t{1} << (type.Bits - 1);
|
||||
return number.Negative ? number.Magnitude <= limit : number.Magnitude <= limit - 1;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> EncodeInteger(const Integer& number, const IntegerType& type) {
|
||||
const std::uint64_t raw = number.Negative ? std::uint64_t{0} - number.Magnitude : number.Magnitude;
|
||||
std::vector<std::uint8_t> bytes;
|
||||
for (unsigned index = 0; index < type.Bits / 8; ++index) bytes.push_back(static_cast<std::uint8_t>(raw >> (8 * index)));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
using Table = std::map<std::string, RawValue>;
|
||||
|
||||
const RawValue* Find(const Table& table, const std::string& key) {
|
||||
const auto found = table.find(key);
|
||||
return found == table.end() ? nullptr : &found->second;
|
||||
}
|
||||
|
||||
PatchEntry BuildEntry(const Table& table, const std::size_t headerLine, std::set<std::string>& names) {
|
||||
PatchEntry entry;
|
||||
entry.Line = headerLine;
|
||||
const auto require = [&](const char* key) -> const RawValue& {
|
||||
const auto* value = Find(table, key);
|
||||
if (!value) FailLine(headerLine, std::string("missing key `") + key + "`", "[[patch]]");
|
||||
return *value;
|
||||
};
|
||||
if (const auto* name = Find(table, "name")) {
|
||||
if (!name->IsString || name->Text.empty() || !std::all_of(name->Text.begin(), name->Text.end(), [](const char c) { return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_'; }))
|
||||
FailLine(name->Line, "name must be a nonempty string of [a-z0-9_]", name->Text);
|
||||
if (Contains(kReservedNames, name->Text)) FailLine(name->Line, "name is reserved", name->Text);
|
||||
if (!names.insert(name->Text).second) FailLine(name->Line, "duplicate name", name->Text);
|
||||
entry.Name = name->Text;
|
||||
}
|
||||
const auto& target = require("target");
|
||||
if (!target.IsString || target.Text.empty()) FailLine(target.Line, "target must be a nonempty string", target.Text);
|
||||
entry.Target = target.Text;
|
||||
const auto& address = require("va");
|
||||
if (address.IsString || address.Number.Negative) FailLine(address.Line, "va must be a nonnegative integer", address.Text);
|
||||
entry.VirtualAddress = address.Number.Magnitude;
|
||||
const auto& expected = require("expected");
|
||||
const auto& replacement = require("value");
|
||||
if (const auto* description = Find(table, "description")) {
|
||||
if (!description->IsString) FailLine(description->Line, "description must be a string", description->Text);
|
||||
entry.Description = description->Text;
|
||||
}
|
||||
const auto* type = Find(table, "type");
|
||||
if (!type) {
|
||||
for (const char* key : {"min", "max", "align"})
|
||||
if (const auto* value = Find(table, key)) FailLine(value->Line, std::string(key) + " requires a type", key);
|
||||
entry.Expected = ParseHexBytes(expected, "expected", "expected");
|
||||
entry.Value = ParseHexBytes(replacement, "value", "value");
|
||||
if (entry.Expected.size() != entry.Value.size()) FailLine(replacement.Line, "expected and value must have the same width", "value");
|
||||
return entry;
|
||||
}
|
||||
if (!type->IsString) FailLine(type->Line, "type must be a string", type->Text);
|
||||
const auto found = std::find_if(kIntegerTypes.begin(), kIntegerTypes.end(), [&](const IntegerType& candidate) { return candidate.Name == type->Text; });
|
||||
if (found == kIntegerTypes.end()) FailLine(type->Line, "unknown type", type->Text);
|
||||
const auto integer = [&](const RawValue& value, const char* key) -> const Integer& {
|
||||
if (value.IsString) FailLine(value.Line, std::string(key) + " must be an integer for a typed entry", value.Text);
|
||||
return value.Number;
|
||||
};
|
||||
const auto& expectedNumber = integer(expected, "expected");
|
||||
const auto& valueNumber = integer(replacement, "value");
|
||||
if (!FitsType(expectedNumber, *found)) FailLine(expected.Line, "expected does not fit " + std::string(found->Name), "expected");
|
||||
if (!FitsType(valueNumber, *found)) FailLine(replacement.Line, "value does not fit " + std::string(found->Name), "value");
|
||||
if (const auto* minimum = Find(table, "min"); minimum && IntegerLess(valueNumber, integer(*minimum, "min"))) FailLine(minimum->Line, "value is below min", "min");
|
||||
if (const auto* maximum = Find(table, "max"); maximum && IntegerLess(integer(*maximum, "max"), valueNumber)) FailLine(maximum->Line, "value is above max", "max");
|
||||
if (const auto* align = Find(table, "align")) {
|
||||
const auto& alignment = integer(*align, "align");
|
||||
if (alignment.Negative || alignment.Magnitude == 0) FailLine(align->Line, "align must be positive", "align");
|
||||
if (valueNumber.Magnitude % alignment.Magnitude != 0) FailLine(align->Line, "value is not a multiple of align", "align");
|
||||
}
|
||||
entry.Type = type->Text;
|
||||
entry.ExpectedNumber = expectedNumber;
|
||||
entry.ValueNumber = valueNumber;
|
||||
if (const auto* minimum = Find(table, "min")) entry.Min = minimum->Number;
|
||||
if (const auto* maximum = Find(table, "max")) entry.Max = maximum->Number;
|
||||
if (const auto* align = Find(table, "align")) entry.Align = align->Number;
|
||||
entry.Expected = EncodeInteger(expectedNumber, *found);
|
||||
entry.Value = EncodeInteger(valueNumber, *found);
|
||||
return entry;
|
||||
}
|
||||
|
||||
struct EntryRange {
|
||||
std::optional<PatchedRange> Range;
|
||||
std::string Reason;
|
||||
};
|
||||
|
||||
std::uint64_t HeaderTableEnd(const ElfHeader& header) {
|
||||
const std::uint64_t table = header.ProgramHeaderOffset + static_cast<std::uint64_t>(header.ProgramHeaderCount) * header.ProgramHeaderEntrySize;
|
||||
return std::max<std::uint64_t>(64, table);
|
||||
}
|
||||
|
||||
EntryRange ResolveEntry(const std::vector<ProgramHeader>& segments, const std::uint64_t fileSize, const std::uint64_t headerEnd, const PatchEntry& entry) {
|
||||
const std::uint64_t length = entry.Expected.size();
|
||||
for (const auto& segment : segments) {
|
||||
if (segment.Type != Elfpatcher::PT_LOAD) continue;
|
||||
if (entry.VirtualAddress < segment.MappedAddress) continue;
|
||||
const std::uint64_t delta = entry.VirtualAddress - segment.MappedAddress;
|
||||
if (delta >= segment.FileSize || length > segment.FileSize - delta) continue;
|
||||
if (segment.Offset > fileSize || segment.FileSize > fileSize - segment.Offset) return {std::nullopt, "PT_LOAD file mapping is out of bounds"};
|
||||
const PatchedRange range{segment.Offset + delta, length, entry.VirtualAddress};
|
||||
if (range.Offset < headerEnd) return {std::nullopt, "range overlaps the ELF header or program header table"};
|
||||
return {range, {}};
|
||||
}
|
||||
return {std::nullopt, "range is not inside the file bytes of one PT_LOAD"};
|
||||
}
|
||||
|
||||
std::vector<EntryRange> ResolveEach(const ElfReader& reader, const std::vector<PatchEntry>& entries) {
|
||||
const auto segments = reader.ReadProgramHeaders();
|
||||
const auto headerEnd = HeaderTableEnd(reader.ReadHeader());
|
||||
std::vector<EntryRange> resolved;
|
||||
for (const auto& entry : entries) resolved.push_back(ResolveEntry(segments, reader.GetFileSize(), headerEnd, entry));
|
||||
return resolved;
|
||||
}
|
||||
|
||||
std::string Describe(const PatchEntry& entry) {
|
||||
return "patch " + entry.Target + " va=" + HexValue(entry.VirtualAddress) + " (line " + std::to_string(entry.Line) + ")";
|
||||
}
|
||||
|
||||
bool Overlaps(const std::uint64_t firstOffset, const std::uint64_t firstLength, const std::uint64_t secondOffset, const std::uint64_t secondLength) {
|
||||
return firstOffset < secondOffset + secondLength && secondOffset < firstOffset + firstLength;
|
||||
}
|
||||
|
||||
void RejectEntryOverlaps(const std::vector<PatchEntry>& entries, const std::vector<EntryRange>& resolved) {
|
||||
for (std::size_t index = 0; index < entries.size(); ++index) {
|
||||
if (!resolved[index].Range) continue;
|
||||
for (std::size_t previous = 0; previous < index; ++previous) {
|
||||
if (!resolved[previous].Range) continue;
|
||||
const auto& first = *resolved[previous].Range;
|
||||
const auto& second = *resolved[index].Range;
|
||||
if (Overlaps(first.Offset, first.Length, second.Offset, second.Length))
|
||||
throw Domain::RelinkerException(Describe(entries[index]) + " overlaps " + Describe(entries[previous]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool IsElfDyn(const std::vector<std::uint8_t>& bytes) {
|
||||
return bytes.size() >= 18 && bytes[0] == 0x7f && bytes[1] == 'E' && bytes[2] == 'L' && bytes[3] == 'F' && bytes[16] == Elfpatcher::ET_DYN && bytes[17] == 0;
|
||||
}
|
||||
|
||||
VerifyResult VerifyOne(const PatchEntry& entry, const EntryRange& resolved, const std::vector<std::uint8_t>& bytes, const bool outputMode) {
|
||||
VerifyResult result;
|
||||
result.Name = entry.Name;
|
||||
result.Target = entry.Target;
|
||||
result.VirtualAddress = entry.VirtualAddress;
|
||||
if (!resolved.Range) {
|
||||
result.Detail = resolved.Reason;
|
||||
return result;
|
||||
}
|
||||
result.Offset = resolved.Range->Offset;
|
||||
const auto first = bytes.begin() + static_cast<std::ptrdiff_t>(resolved.Range->Offset);
|
||||
const std::vector<std::uint8_t> found(first, first + static_cast<std::ptrdiff_t>(resolved.Range->Length));
|
||||
const auto& wanted = outputMode ? entry.Value : entry.Expected;
|
||||
const auto& other = outputMode ? entry.Expected : entry.Value;
|
||||
if (found == wanted) {
|
||||
result.Status = outputMode ? VerifyStatus::Applied : VerifyStatus::Applicable;
|
||||
result.Detail = Hex(found);
|
||||
} else if (found == other) {
|
||||
result.Detail = outputMode ? "patch is not applied: found " + Hex(found) : "input already holds the patched value " + Hex(found);
|
||||
} else {
|
||||
result.Detail = "expected " + Hex(wanted) + " found " + Hex(found);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const VerifyTarget* FindTarget(const std::vector<VerifyTarget>& targets, const std::string& name) {
|
||||
const auto found = std::find_if(targets.begin(), targets.end(), [&](const VerifyTarget& target) { return target.Name == name; });
|
||||
return found == targets.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::string Hex(const std::span<const std::uint8_t> bytes) {
|
||||
constexpr char digits[] = "0123456789abcdef";
|
||||
std::string text;
|
||||
for (const auto byte : bytes) {
|
||||
text += digits[byte >> 4];
|
||||
text += digits[byte & 15];
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string FormatApplied(const AppliedPatch& patch) {
|
||||
std::string text = "Patch: " + patch.Target + (patch.Name.empty() ? std::string() : " [" + patch.Name + "]") + " va=" + HexValue(patch.VirtualAddress) + " off=" + HexValue(patch.Offset) + " " + Hex(patch.Expected) + " -> " + Hex(patch.Value);
|
||||
if (!patch.Description.empty()) text += " (" + patch.Description + ")";
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string ModuleTargetName(const std::filesystem::path& modulePath, const std::filesystem::path& moduleRoot) {
|
||||
auto name = Io::Utf8Path(modulePath.lexically_normal().lexically_relative(moduleRoot.lexically_normal()));
|
||||
std::replace(name.begin(), name.end(), '\\', '/');
|
||||
return name;
|
||||
}
|
||||
|
||||
PatchFile ParsePatchFile(const std::string_view text) {
|
||||
PatchFile file;
|
||||
Table top;
|
||||
Table current;
|
||||
std::size_t entryLine = 0;
|
||||
std::set<std::string> names;
|
||||
bool inEntry = false;
|
||||
const auto finishEntry = [&] {
|
||||
if (inEntry) file.Entries.push_back(BuildEntry(current, entryLine, names));
|
||||
current.clear();
|
||||
};
|
||||
std::size_t lineNumber = 0;
|
||||
std::size_t position = 0;
|
||||
while (position <= text.size()) {
|
||||
auto end = text.find('\n', position);
|
||||
if (end == std::string_view::npos) end = text.size();
|
||||
const auto whole = text.substr(position, end - position);
|
||||
position = end + 1;
|
||||
++lineNumber;
|
||||
const auto line = Trim(StripComment(whole));
|
||||
if (line.empty()) continue;
|
||||
if (line.front() == '[') {
|
||||
if (line != "[[patch]]") FailLine(lineNumber, "unsupported table header", Trim(whole));
|
||||
finishEntry();
|
||||
inEntry = true;
|
||||
entryLine = lineNumber;
|
||||
continue;
|
||||
}
|
||||
std::size_t keyEnd = 0;
|
||||
while (keyEnd < line.size() && IsKeyCharacter(line[keyEnd])) ++keyEnd;
|
||||
const std::string key(line.substr(0, keyEnd));
|
||||
const auto rest = Trim(line.substr(keyEnd));
|
||||
if (key.empty() || rest.empty() || rest.front() != '=') {
|
||||
if (!rest.empty() && rest.front() == '.') FailLine(lineNumber, "dotted keys are not supported", line);
|
||||
if (key.empty() && (line.front() == '"' || line.front() == '\'')) FailLine(lineNumber, "quoted keys are not supported", line);
|
||||
FailLine(lineNumber, "expected `key = value`", line);
|
||||
}
|
||||
auto value = ParseValue(Trim(rest.substr(1)), lineNumber, line);
|
||||
auto& table = inEntry ? current : top;
|
||||
if (inEntry ? !Contains(kEntryKeys, key) : !(key == "format" || Contains(kIgnoredTopLevelKeys, key)))
|
||||
FailLine(lineNumber, inEntry ? "unknown key" : "unknown top-level key", line);
|
||||
if (table.contains(key)) FailLine(lineNumber, "duplicate key", line);
|
||||
table.emplace(key, std::move(value));
|
||||
}
|
||||
finishEntry();
|
||||
const auto* format = Find(top, "format");
|
||||
if (!format) throw Domain::RelinkerException("patch file: `format = 1` is required");
|
||||
if (format->IsString || format->Number.Negative || format->Number.Magnitude != 1) FailLine(format->Line, "unsupported format", "format");
|
||||
if (const auto* mainTarget = Find(top, "main_target")) {
|
||||
if (!mainTarget->IsString || mainTarget->Text.empty()) FailLine(mainTarget->Line, "main_target must be a nonempty string", "main_target");
|
||||
file.MainTarget = mainTarget->Text;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
std::vector<PatchedRange> ResolveRanges(const ElfReader& reader, const std::vector<PatchEntry>& entries) {
|
||||
const auto resolved = ResolveEach(reader, entries);
|
||||
std::vector<PatchedRange> ranges;
|
||||
for (std::size_t index = 0; index < entries.size(); ++index) {
|
||||
if (!resolved[index].Range) throw Domain::RelinkerException(Describe(entries[index]) + ": " + resolved[index].Reason);
|
||||
ranges.push_back(*resolved[index].Range);
|
||||
}
|
||||
RejectEntryOverlaps(entries, resolved);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
std::vector<AppliedPatch> ApplyPatches(std::vector<std::uint8_t>& bytes, const std::string& target, const std::vector<PatchEntry>& entries, const std::vector<PatchedRange>& ranges) {
|
||||
if (entries.size() != ranges.size()) throw Domain::RelinkerException("Patch ranges do not match patch entries");
|
||||
for (std::size_t index = 0; index < entries.size(); ++index) {
|
||||
const auto& range = ranges[index];
|
||||
if (range.Offset > bytes.size() || range.Length > bytes.size() - range.Offset) throw Domain::RelinkerException(Describe(entries[index]) + ": range exceeds the image", range.Offset);
|
||||
const auto first = bytes.begin() + static_cast<std::ptrdiff_t>(range.Offset);
|
||||
if (!std::equal(entries[index].Expected.begin(), entries[index].Expected.end(), first))
|
||||
throw Domain::RelinkerException(Describe(entries[index]) + " off=" + HexValue(range.Offset) + ": expected " + Hex(entries[index].Expected) + " found " + Hex(std::span<const std::uint8_t>(&*first, range.Length)), range.Offset);
|
||||
}
|
||||
std::vector<AppliedPatch> applied;
|
||||
for (std::size_t index = 0; index < entries.size(); ++index) {
|
||||
std::copy(entries[index].Value.begin(), entries[index].Value.end(), bytes.begin() + static_cast<std::ptrdiff_t>(ranges[index].Offset));
|
||||
applied.push_back({target, entries[index].VirtualAddress, ranges[index].Offset, entries[index].Expected, entries[index].Value, entries[index].Description, entries[index].Name, entries[index].Type, entries[index].ExpectedNumber, entries[index].ValueNumber, entries[index].Min, entries[index].Max, entries[index].Align});
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
PatchedTarget PatchTarget(std::vector<std::uint8_t>& bytes, const std::string& target, const std::vector<PatchEntry>& entries) {
|
||||
std::vector<PatchEntry> own;
|
||||
for (const auto& entry : entries) if (entry.Target == target) own.push_back(entry);
|
||||
if (own.empty()) return {};
|
||||
ElfReader reader(std::move(bytes));
|
||||
PatchedTarget patched;
|
||||
try {
|
||||
patched.Ranges = ResolveRanges(reader, own);
|
||||
} catch (...) {
|
||||
bytes = reader.TakeBytes();
|
||||
throw;
|
||||
}
|
||||
bytes = reader.TakeBytes();
|
||||
patched.Applied = ApplyPatches(bytes, target, own, patched.Ranges);
|
||||
return patched;
|
||||
}
|
||||
|
||||
void CheckOverlaps(const std::string& target, const std::vector<PatchedRange>& patched, const WriterSites& writers) {
|
||||
if (patched.empty()) return;
|
||||
std::vector<WriterRange> sites;
|
||||
for (const auto& report : writers.Reports)
|
||||
if (report.Lowering != Codegen::Amd64OnlyLowering::Kept) sites.push_back({report.Offset, report.OriginalLength, "Intel in-place report"});
|
||||
for (const auto& site : writers.Trampolines) sites.push_back({site.Offset, site.Length, "Intel trampoline site"});
|
||||
for (const auto& patch : writers.RelinkPatches) sites.push_back({patch.Offset, patch.Bytes.size(), "relinker patch"});
|
||||
sites.insert(sites.end(), writers.Traps.begin(), writers.Traps.end());
|
||||
const auto relaViolation = [&](const std::span<const std::uint8_t> table, const char* name, const PatchedRange& range) {
|
||||
for (std::size_t position = 0; position + 24 <= table.size(); position += 24) {
|
||||
std::uint64_t relocation = 0;
|
||||
for (unsigned byte = 0; byte < 8; ++byte) relocation |= static_cast<std::uint64_t>(table[position + byte]) << (8 * byte);
|
||||
if (Overlaps(range.VirtualAddress, range.Length, relocation, 8))
|
||||
throw Domain::RelinkerException("patch " + target + " va=" + HexValue(range.VirtualAddress) + " off=" + HexValue(range.Offset) + " overlaps " + name + " relocation target at va=" + HexValue(relocation), range.Offset);
|
||||
}
|
||||
};
|
||||
for (const auto& range : patched) {
|
||||
relaViolation(writers.RelaData, "RelaData", range);
|
||||
relaViolation(writers.RelaPltData, "RelaPltData", range);
|
||||
}
|
||||
for (const auto& range : patched)
|
||||
for (const auto& site : sites)
|
||||
if (Overlaps(range.Offset, range.Length, site.Offset, site.Length))
|
||||
throw Domain::RelinkerException("patch " + target + " va=" + HexValue(range.VirtualAddress) + " off=" + HexValue(range.Offset) + " overlaps " + site.Writer + " at off=" + HexValue(site.Offset) + " length " + std::to_string(site.Length), range.Offset);
|
||||
}
|
||||
|
||||
void MatchTargets(const std::vector<PatchEntry>& entries, const std::vector<std::string>& targetNames) {
|
||||
for (const auto& entry : entries) {
|
||||
const auto count = std::count(targetNames.begin(), targetNames.end(), entry.Target);
|
||||
if (count == 0) throw Domain::RelinkerException("unmatched patch target: " + entry.Target + " (patch file line " + std::to_string(entry.Line) + ")");
|
||||
if (count > 1) throw Domain::RelinkerException("ambiguous patch target: " + entry.Target + " (patch file line " + std::to_string(entry.Line) + ")");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<AppliedPatch> AggregateManifest(const std::vector<std::vector<AppliedPatch>>& records) {
|
||||
std::vector<AppliedPatch> merged;
|
||||
for (const auto& group : records) {
|
||||
for (const auto& patch : group) {
|
||||
const bool known = std::any_of(merged.begin(), merged.end(), [&](const AppliedPatch& existing) {
|
||||
return existing.Target == patch.Target && existing.VirtualAddress == patch.VirtualAddress && existing.Offset == patch.Offset;
|
||||
});
|
||||
if (!known) merged.push_back(patch);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
std::string WriteManifest(const std::vector<AppliedPatch>& patches, const std::string& mainTarget, const std::string& configPath) {
|
||||
const auto holdable = [](const std::string& text) {
|
||||
return std::none_of(text.begin(), text.end(), [](const char character) {
|
||||
return character == '"' || character == '\\' || (static_cast<unsigned char>(character) < 0x20 && character != '\t') || character == 0x7f;
|
||||
});
|
||||
};
|
||||
const auto quoted = [&](const std::string& text, const char* field) {
|
||||
if (!holdable(text)) throw Domain::RelinkerException(std::string("Patch manifest cannot hold the ") + field + ": " + text);
|
||||
return "\"" + text + "\"";
|
||||
};
|
||||
std::string config = std::filesystem::path(Io::NativePath(configPath)).generic_string();
|
||||
if (!config.empty() && !holdable(config)) config = std::filesystem::path(Io::NativePath(configPath)).filename().generic_string();
|
||||
std::ostringstream manifest;
|
||||
manifest << "format = 1\nmain_target = " << quoted(mainTarget, "main_target") << '\n';
|
||||
if (!config.empty()) manifest << "config = " << quoted(config, "config") << '\n';
|
||||
for (const auto& patch : patches) {
|
||||
manifest << "\n[[patch]]\n";
|
||||
if (!patch.Name.empty()) manifest << "name = " << quoted(patch.Name, "name") << '\n';
|
||||
manifest << "target = " << quoted(patch.Target, "target") << "\nva = " << HexValue(patch.VirtualAddress) << '\n';
|
||||
if (patch.Type.empty()) {
|
||||
manifest << "expected = \"" << Hex(patch.Expected) << "\"\nvalue = \"" << Hex(patch.Value) << "\"\n";
|
||||
} else {
|
||||
manifest << "type = " << quoted(patch.Type, "type") << "\nexpected = " << IntegerText(patch.ExpectedNumber) << "\nvalue = " << IntegerText(patch.ValueNumber) << '\n';
|
||||
if (patch.Min) manifest << "min = " << IntegerText(*patch.Min) << '\n';
|
||||
if (patch.Max) manifest << "max = " << IntegerText(*patch.Max) << '\n';
|
||||
if (patch.Align) manifest << "align = " << IntegerText(*patch.Align) << '\n';
|
||||
}
|
||||
if (!patch.Description.empty()) manifest << "description = " << quoted(patch.Description, "description") << '\n';
|
||||
}
|
||||
return manifest.str();
|
||||
}
|
||||
|
||||
std::string_view StatusName(const VerifyStatus status) {
|
||||
switch (status) {
|
||||
case VerifyStatus::Applicable: return "applicable";
|
||||
case VerifyStatus::Applied: return "applied";
|
||||
case VerifyStatus::NotChecked: return "not-checked";
|
||||
case VerifyStatus::Mismatch: break;
|
||||
}
|
||||
return "mismatch";
|
||||
}
|
||||
|
||||
std::vector<VerifyResult> VerifyPatches(const PatchFile& file, const std::vector<VerifyTarget>& executables, const std::vector<VerifyTarget>& modules, const bool outputMode) {
|
||||
if (outputMode) {
|
||||
if (!file.MainTarget) throw Domain::RelinkerException("The patch file has no main_target, so its entries cannot be matched to an output: verify the input executable, or verify the manifest written by the conversion");
|
||||
for (std::size_t index = 0; index < executables.size(); ++index)
|
||||
for (std::size_t other = index + 1; other < executables.size(); ++other)
|
||||
if (executables[index].Name == executables[other].Name) throw Domain::RelinkerException("ambiguous patch target: " + executables[index].Name);
|
||||
for (const auto& executable : executables)
|
||||
if (!IsElfDyn(executable.Bytes)) throw Domain::RelinkerException(executable.Name + " is not an ET_DYN Linux ELF output (ET_EXEC from --linux-fixed-base, PE and Mach-O outputs are not supported): verify the input instead");
|
||||
} else {
|
||||
std::vector<std::string> names;
|
||||
for (const auto& executable : executables) names.push_back(executable.Name);
|
||||
for (const auto& module : modules) names.push_back(module.Name);
|
||||
try {
|
||||
MatchTargets(file.Entries, names);
|
||||
} catch (const Domain::RelinkerException& error) {
|
||||
if (std::string_view(error.what()).starts_with("unmatched")) throw Domain::RelinkerException(std::string(error.what()) + "; to check a converted output, use --verify-output");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
std::vector<VerifyResult> results(file.Entries.size());
|
||||
std::vector<bool> done(file.Entries.size());
|
||||
for (std::size_t index = 0; index < file.Entries.size(); ++index) {
|
||||
if (done[index]) continue;
|
||||
const auto& name = file.Entries[index].Target;
|
||||
const auto* target = FindTarget(executables, name);
|
||||
if (!target && !outputMode) target = FindTarget(modules, name);
|
||||
std::vector<std::size_t> indices;
|
||||
for (std::size_t other = index; other < file.Entries.size(); ++other) if (file.Entries[other].Target == name) indices.push_back(other);
|
||||
if (!target) {
|
||||
for (const auto other : indices) {
|
||||
const auto& entry = file.Entries[other];
|
||||
results[other] = {entry.Name, entry.Target, entry.VirtualAddress, 0, VerifyStatus::NotChecked, "module, or executable not named with --executable"};
|
||||
done[other] = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
std::vector<PatchEntry> own;
|
||||
for (const auto other : indices) own.push_back(file.Entries[other]);
|
||||
const ElfReader reader(target->Bytes);
|
||||
const auto resolved = ResolveEach(reader, own);
|
||||
RejectEntryOverlaps(own, resolved);
|
||||
for (std::size_t position = 0; position < indices.size(); ++position) {
|
||||
results[indices[position]] = VerifyOne(own[position], resolved[position], target->Bytes, outputMode);
|
||||
done[indices[position]] = true;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
std::filesystem::path OutputExecutablePath(const std::filesystem::path& outputPath, const std::string& executableArgument) {
|
||||
const auto relative = Io::NativePath(executableArgument).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: " + executableArgument);
|
||||
std::string name;
|
||||
for (const auto& part : relative) name += (name.empty() ? "" : ".") + Io::Utf8Path(part);
|
||||
return outputPath.parent_path() / Io::NativePath(name + Io::Utf8Path(outputPath.extension()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <sstream>
|
||||
#include <bit>
|
||||
#include <domain/InputProgress.hpp>
|
||||
#include <relinker/patching/GamePatch.hpp>
|
||||
|
||||
namespace Relinker {
|
||||
|
||||
@@ -49,12 +50,23 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti
|
||||
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<GamePatch::AppliedPatch> appliedPatches;
|
||||
std::vector<GamePatch::PatchedRange> patchedRanges;
|
||||
if (!options.Patches.empty()) {
|
||||
auto patched = GamePatch::PatchTarget(sourceBytes, options.TargetName, options.Patches);
|
||||
patchedRanges = std::move(patched.Ranges);
|
||||
for (auto& patch : patched.Applied) {
|
||||
inputProgress.Message(GamePatch::FormatApplied(patch) + "\n");
|
||||
appliedPatches.push_back(std::move(patch));
|
||||
}
|
||||
}
|
||||
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, [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Lowering, {}, completed, total}); })->Convert(std::move(sourceBytes), segments);
|
||||
sourceBytes = std::move(converted.Bytes);
|
||||
trampolines = std::move(converted.Trampolines);
|
||||
GamePatch::CheckOverlaps(options.TargetName, patchedRanges, {.Reports = converted.Reports, .Trampolines = 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" || name == "scalar compare"; };
|
||||
for (const auto& report : converted.Reports) {
|
||||
@@ -92,6 +104,18 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti
|
||||
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];
|
||||
}
|
||||
GamePatch::CheckOverlaps(options.TargetName, patchedRanges, {.RelinkPatches = result.Patches, .RelaData = result.DynamicSection.RelaData, .RelaPltData = result.DynamicSection.RelaPltData});
|
||||
|
||||
if (!options.Patches.empty()) {
|
||||
for (auto& module : input.Modules) {
|
||||
auto patched = GamePatch::PatchTarget(module.Bytes, GamePatch::ModuleTargetName(module.Path, input.ModuleRoot), options.Patches);
|
||||
module.PatchedRanges = std::move(patched.Ranges);
|
||||
for (auto& patch : patched.Applied) {
|
||||
inputProgress.Message(GamePatch::FormatApplied(patch) + "\n");
|
||||
appliedPatches.push_back(std::move(patch));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Relinker::GuestArtifact> guestArtifacts;
|
||||
if (!input.Modules.empty()) {
|
||||
@@ -131,7 +155,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti
|
||||
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)};
|
||||
return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics), std::move(appliedPatches)};
|
||||
} catch (Domain::RelinkerException& error) {
|
||||
if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath);
|
||||
throw;
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
#include "TestProgress.hpp"
|
||||
#include <relinker/pipeline/Conversion.hpp>
|
||||
#include <relinker/patching/GamePatch.hpp>
|
||||
#include <relinker/parsing/ElfReader.hpp>
|
||||
#include <io/BufferUtils.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace Relinker;
|
||||
using namespace Relinker::GamePatch;
|
||||
|
||||
void require(const bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
template<typename TAction>
|
||||
void rejects(TAction action, const std::string& fragment) {
|
||||
try {
|
||||
action();
|
||||
} catch (const Domain::RelinkerException& error) {
|
||||
require(std::string(error.what()).find(fragment) != std::string::npos, "wrong error, expected `" + fragment + "` in: " + error.what());
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error("accepted: expected `" + fragment + "`");
|
||||
}
|
||||
|
||||
std::string header(const std::string& body) {
|
||||
return "format = 1\n" + body;
|
||||
}
|
||||
|
||||
void rejectsText(const std::string& text, const std::size_t line, const std::string& fragment = {}) {
|
||||
rejects([&] { ParsePatchFile(text); }, "line " + std::to_string(line) + ":");
|
||||
if (!fragment.empty()) rejects([&] { ParsePatchFile(text); }, fragment);
|
||||
}
|
||||
|
||||
PatchEntry entry(const std::string& target, const std::uint64_t address, std::vector<std::uint8_t> expected, std::vector<std::uint8_t> value, const std::string& name = {}) {
|
||||
PatchEntry result;
|
||||
result.Target = target;
|
||||
result.VirtualAddress = address;
|
||||
result.Expected = std::move(expected);
|
||||
result.Value = std::move(value);
|
||||
result.Name = name;
|
||||
result.Line = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> twoSegmentFixture() {
|
||||
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, 32, 64);
|
||||
Io::WriteU16(bytes, 52, 64);
|
||||
Io::WriteU16(bytes, 54, 56);
|
||||
Io::WriteU16(bytes, 56, 2);
|
||||
const auto segment = [&](const std::size_t index, const std::uint64_t offset, const std::uint64_t address, const std::uint64_t fileSize, const std::uint64_t memorySize) {
|
||||
const auto at = 64 + index * 56;
|
||||
Io::WriteU32(bytes, at, 1);
|
||||
Io::WriteU32(bytes, at + 4, 6);
|
||||
Io::WriteU64(bytes, at + 8, offset);
|
||||
Io::WriteU64(bytes, at + 16, address);
|
||||
Io::WriteU64(bytes, at + 24, address);
|
||||
Io::WriteU64(bytes, at + 32, fileSize);
|
||||
Io::WriteU64(bytes, at + 40, memorySize);
|
||||
Io::WriteU64(bytes, at + 48, 8);
|
||||
};
|
||||
segment(0, 0, 0, 0x1100, 0x1100);
|
||||
segment(1, 0x2000, 0x1100, 0x100, 0x300);
|
||||
for (std::size_t index = 0; index < 0x100; ++index) {
|
||||
bytes[0x1000 + index] = static_cast<std::uint8_t>(index);
|
||||
bytes[0x2000 + index] = static_cast<std::uint8_t>(0x80 + index);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> conversionFixture() {
|
||||
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 programHeader = [&](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);
|
||||
};
|
||||
programHeader(0, 1, 5, 0x1000, 8);
|
||||
programHeader(1, 1, 6, 0x2000, 0x1000);
|
||||
for (std::size_t index = 3; index < 8; ++index) programHeader(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}}};
|
||||
programHeader(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;
|
||||
}
|
||||
|
||||
ConversionInput conversionInput(const std::vector<std::uint8_t>& executable) {
|
||||
ConversionInput result;
|
||||
result.Executable = executable;
|
||||
result.InputPath = "virtual/title/custom-name.bin";
|
||||
result.OutputPath = "virtual/output/title";
|
||||
result.ModuleRoot = "virtual/title";
|
||||
result.Modules.push_back({"virtual/title/sce_module/plugin.prx", conversionFixture()});
|
||||
return result;
|
||||
}
|
||||
|
||||
void grammarAccepted() {
|
||||
const auto file = ParsePatchFile(
|
||||
"# leading comment\n"
|
||||
"format = 1 # trailing comment\n"
|
||||
"relinker_version = \"x\"\n"
|
||||
"relinker_commit = \"y\"\n"
|
||||
"config = \"z\"\n"
|
||||
"config_sha256 = \"w\"\n"
|
||||
"main_target = \"eboot.elf\"\n"
|
||||
"\n"
|
||||
"[[patch]]\n"
|
||||
"name = \"pool_size\"\n"
|
||||
"target = \"eboot.elf\"\n"
|
||||
"va = 0x1234\n"
|
||||
"type = \"i32\"\n"
|
||||
"expected = 0x10000000\n"
|
||||
"value = 0x18000000\n"
|
||||
"max = 0x7f000000\n"
|
||||
"align = 0x1000\n"
|
||||
"description = \"has # inside\"\n"
|
||||
"\n"
|
||||
"[[patch]]\n"
|
||||
"target = \"sce_module/libc.prx\"\n"
|
||||
"va = 4096\n"
|
||||
"expected = \"00ff\"\n"
|
||||
"value = \"AB12\"\n");
|
||||
require(file.MainTarget == std::string("eboot.elf"), "main_target was not read");
|
||||
require(file.Entries.size() == 2, "wrong entry count");
|
||||
const auto& typed = file.Entries[0];
|
||||
require(typed.Name == "pool_size" && typed.Target == "eboot.elf" && typed.VirtualAddress == 0x1234, "typed entry fields");
|
||||
require(typed.Expected == std::vector<std::uint8_t>({0, 0, 0, 0x10}) && typed.Value == std::vector<std::uint8_t>({0, 0, 0, 0x18}), "typed encoding is not little-endian");
|
||||
require(typed.Description == "has # inside" && typed.Line == 9, "description or line");
|
||||
const auto& raw = file.Entries[1];
|
||||
require(raw.Name.empty() && raw.VirtualAddress == 4096, "raw entry fields");
|
||||
require(raw.Expected == std::vector<std::uint8_t>({0x00, 0xff}) && raw.Value == std::vector<std::uint8_t>({0xab, 0x12}), "raw hex bytes");
|
||||
require(!ParsePatchFile("format = 1\n").MainTarget && ParsePatchFile("format = 1\n").Entries.empty(), "empty file");
|
||||
}
|
||||
|
||||
void grammarRejected() {
|
||||
rejects([] { ParsePatchFile(""); }, "format = 1");
|
||||
rejectsText("format = 2\n", 1, "unsupported format");
|
||||
rejectsText(header("flavour = 1\n"), 2, "unknown top-level key");
|
||||
rejectsText(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"01\"\ncolour = 1\n"), 7, "unknown key");
|
||||
rejectsText(header("[[patch]]\ntarget = \"a\"\ntarget = \"b\"\n"), 4, "duplicate key");
|
||||
rejectsText(header("[patch]\n"), 2, "unsupported table header");
|
||||
rejectsText(header("[other]\n"), 2, "unsupported table header");
|
||||
rejectsText(header("[[patch]]\ndescription = \"a\\nb\"\n"), 3, "escapes");
|
||||
rejectsText(header("[[patch]]\ndescription = [1, 2]\n"), 3, "arrays");
|
||||
rejectsText(header("[[patch]]\ndescription = 'x'\n"), 3, "single-quoted");
|
||||
rejectsText(header("[[patch]]\ndescription = { a = 1 }\n"), 3, "inline tables");
|
||||
rejectsText(header("[[patch]]\ndescription = \"\"\"x\"\"\"\n"), 3, "multiline");
|
||||
rejectsText(header("[[patch]]\nva = 1.5\n"), 3, "floats");
|
||||
rejectsText(header("[[patch]]\nva = true\n"), 3, "booleans");
|
||||
rejectsText(header("[[patch]]\nsub.key = 1\n"), 3, "dotted keys");
|
||||
rejectsText(header("[[patch]]\n\"quoted\" = 1\n"), 3, "quoted keys");
|
||||
rejectsText(header("[[patch]]\nva\n"), 3, "key = value");
|
||||
rejectsText(header("[[patch]]\nva = 1_000\n"), 3);
|
||||
rejectsText(header("[[patch]]\nva = \"unterminated\n"), 3, "unterminated");
|
||||
}
|
||||
|
||||
std::string typedEntry(const std::string& type, const std::string& expected, const std::string& value, const std::string& extra = {}) {
|
||||
return header("[[patch]]\ntarget = \"a\"\nva = 1\ntype = \"" + type + "\"\nexpected = " + expected + "\nvalue = " + value + "\n" + extra);
|
||||
}
|
||||
|
||||
void integerRanges() {
|
||||
struct Case { const char* Type; const char* Fits; const char* Overflows; std::size_t Width; };
|
||||
for (const auto& c : {Case{"u8", "255", "256", 1}, Case{"u16", "65535", "65536", 2}, Case{"u32", "0xffffffff", "0x100000000", 4}, Case{"u64", "0xffffffffffffffff", "0x10000000000000000", 8},
|
||||
Case{"i8", "127", "128", 1}, Case{"i16", "32767", "32768", 2}, Case{"i32", "0x7fffffff", "0x80000000", 4}, Case{"i64", "0x7fffffffffffffff", "0x8000000000000000", 8}}) {
|
||||
const auto accepted = ParsePatchFile(typedEntry(c.Type, "0", c.Fits));
|
||||
require(accepted.Entries.front().Value.size() == c.Width, std::string("width of ") + c.Type);
|
||||
rejects([&] { ParsePatchFile(typedEntry(c.Type, "0", c.Overflows)); }, "line 7");
|
||||
}
|
||||
require(ParsePatchFile(typedEntry("i32", "-1", "-2147483648")).Entries.front().Value == std::vector<std::uint8_t>({0, 0, 0, 0x80}), "i32 minimum");
|
||||
require(ParsePatchFile(typedEntry("i8", "-128", "-1")).Entries.front().Value == std::vector<std::uint8_t>({0xff}), "i8 -1");
|
||||
require(ParsePatchFile(typedEntry("i64", "-1", "-1")).Entries.front().Expected == std::vector<std::uint8_t>(8, 0xff), "i64 -1");
|
||||
rejects([] { ParsePatchFile(typedEntry("i8", "0", "-129")); }, "line 7");
|
||||
rejects([] { ParsePatchFile(typedEntry("u8", "0", "-1")); }, "line 7");
|
||||
rejects([] { ParsePatchFile(typedEntry("i16", "40000", "0")); }, "line 6");
|
||||
rejects([] { ParsePatchFile(typedEntry("f32", "0", "0")); }, "unknown type");
|
||||
rejects([] { ParsePatchFile(typedEntry("u32", "\"00\"", "1")); }, "integer");
|
||||
}
|
||||
|
||||
void boundsAndWidth() {
|
||||
ParsePatchFile(typedEntry("u32", "0", "8", "min = 8\nmax = 8\nalign = 8\n"));
|
||||
rejects([] { ParsePatchFile(typedEntry("u32", "0", "7", "min = 8\n")); }, "below min");
|
||||
rejects([] { ParsePatchFile(typedEntry("u32", "0", "9", "max = 8\n")); }, "above max");
|
||||
rejects([] { ParsePatchFile(typedEntry("u32", "0", "12", "align = 8\n")); }, "multiple of align");
|
||||
rejects([] { ParsePatchFile(typedEntry("u32", "0", "8", "align = 0\n")); }, "align must be positive");
|
||||
ParsePatchFile(typedEntry("i32", "0", "-2097152", "align = 2097152\n"));
|
||||
rejects([] { ParsePatchFile(typedEntry("i32", "0", "-1048576", "align = 2097152\n")); }, "multiple of align");
|
||||
rejects([] { ParsePatchFile(typedEntry("u32", "0", "768MiB")); }, "line 7");
|
||||
rejects([] { ParsePatchFile(typedEntry("u32", "0", "007")); }, "leading zeros");
|
||||
ParsePatchFile(typedEntry("u32", "0", "0"));
|
||||
ParsePatchFile(typedEntry("i32", "-0", "10"));
|
||||
rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"0000\"\nmin = 1\n")); }, "requires a type");
|
||||
rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"0000\"\n")); }, "same width");
|
||||
rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"0\"\nvalue = \"1\"\n")); }, "even-length");
|
||||
rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = 1\nexpected = \"zz\"\nvalue = \"00\"\n")); }, "non-hex");
|
||||
rejects([] { ParsePatchFile(header("[[patch]]\nva = 1\nexpected = \"00\"\nvalue = \"01\"\n")); }, "missing key `target`");
|
||||
rejects([] { ParsePatchFile(header("[[patch]]\ntarget = \"a\"\nva = -1\nexpected = \"00\"\nvalue = \"01\"\n")); }, "va must be");
|
||||
}
|
||||
|
||||
std::string namedEntry(const std::string& name, const std::string& extra = {}) {
|
||||
return header("[[patch]]\nname = \"" + name + "\"\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"01\"\n" + extra);
|
||||
}
|
||||
|
||||
void names() {
|
||||
require(ParsePatchFile(namedEntry("pool_size_2")).Entries.front().Name == "pool_size_2", "valid name");
|
||||
for (const char* bad : {"Upper", "has-dash", "has space", "", "dot.ted", "\xc3\xa9"}) rejects([&] { ParsePatchFile(namedEntry(bad)); }, "line 3: name must be");
|
||||
rejects([] { ParsePatchFile(header("[[patch]]\nname = 5\ntarget = \"a\"\nva = 1\nexpected = \"00\"\nvalue = \"01\"\n")); }, "line 3: name must be");
|
||||
rejects([] { ParsePatchFile(namedEntry("same") + "\n[[patch]]\nname = \"same\"\ntarget = \"a\"\nva = 9\nexpected = \"00\"\nvalue = \"01\"\n"); }, "line 10: duplicate name");
|
||||
ParsePatchFile(namedEntry("one") + "\n[[patch]]\nname = \"two\"\ntarget = \"a\"\nva = 9\nexpected = \"00\"\nvalue = \"01\"\n");
|
||||
}
|
||||
|
||||
void ranges() {
|
||||
const ElfReader reader(twoSegmentFixture());
|
||||
const auto resolved = ResolveRanges(reader, {entry("t", 0x1000, {0, 1}, {9, 9}), entry("t", 0x1100, {0x80}, {1}), entry("t", 0x11fc, {0xfc, 0xfd, 0xfe, 0xff}, {0, 0, 0, 0})});
|
||||
require(resolved[0].Offset == 0x1000 && resolved[0].Length == 2, "first segment translation");
|
||||
require(resolved[1].Offset == 0x2000, "second segment translation");
|
||||
require(resolved[2].Offset == 0x20fc, "second segment end");
|
||||
rejects([&] { ResolveRanges(reader, {entry("t", 0x10fe, {0, 0, 0, 0}, {1, 1, 1, 1})}); }, "one PT_LOAD");
|
||||
rejects([&] { ResolveRanges(reader, {entry("t", 0x1200, {0}, {1})}); }, "one PT_LOAD");
|
||||
rejects([&] { ResolveRanges(reader, {entry("t", 0x11fe, {0, 0, 0}, {1, 1, 1})}); }, "one PT_LOAD");
|
||||
rejects([&] { ResolveRanges(reader, {entry("t", 0x5000, {0}, {1})}); }, "one PT_LOAD");
|
||||
rejects([&] { ResolveRanges(reader, {entry("t", 0x1000, {0, 1, 2}, {1, 1, 1}), entry("t", 0x1002, {2, 3}, {1, 1})}); }, "overlaps");
|
||||
ResolveRanges(reader, {entry("t", 0x1000, {0, 1}, {1, 1}), entry("t", 0x1002, {2, 3}, {1, 1})});
|
||||
}
|
||||
|
||||
void headerRange() {
|
||||
const ElfReader reader(twoSegmentFixture());
|
||||
rejects([&] { ResolveRanges(reader, {entry("t", 0x0, {0x7f}, {0})}); }, "program header table");
|
||||
rejects([&] { ResolveRanges(reader, {entry("t", 0x40, {0}, {1})}); }, "program header table");
|
||||
rejects([&] { ResolveRanges(reader, {entry("t", 0xaf, {0}, {1})}); }, "program header table");
|
||||
const auto ok = ResolveRanges(reader, {entry("t", 0xb0, {0}, {1})});
|
||||
require(ok.front().Offset == 0xb0, "range after the header table was rejected");
|
||||
}
|
||||
|
||||
void applyAndMismatch() {
|
||||
auto bytes = twoSegmentFixture();
|
||||
const auto original = bytes;
|
||||
const std::vector<PatchEntry> entries{entry("t", 0x1000, {0, 1}, {9, 8}, "first"), entry("t", 0x1100, {0x80}, {1})};
|
||||
const auto patched = PatchTarget(bytes, "t", entries);
|
||||
require(patched.Applied.size() == 2 && patched.Applied[0].Offset == 0x1000 && patched.Applied[0].Name == "first", "applied records");
|
||||
require(bytes[0x1000] == 9 && bytes[0x1001] == 8 && bytes[0x2000] == 1, "values were not written");
|
||||
for (std::size_t index = 0; index < bytes.size(); ++index)
|
||||
if (index != 0x1000 && index != 0x1001 && index != 0x2000) require(bytes[index] == original[index], "byte outside the patch changed");
|
||||
|
||||
bytes = original;
|
||||
rejects([&] { PatchTarget(bytes, "t", {entry("t", 0x1000, {0, 1}, {9, 8}), entry("t", 0x1100, {0x81}, {1})}); }, "expected 81 found 80");
|
||||
require(bytes == original, "a mismatch wrote bytes");
|
||||
require(PatchTarget(bytes, "other", entries).Applied.empty() && bytes == original, "entry hit another target");
|
||||
}
|
||||
|
||||
void overlapWithWriters() {
|
||||
const std::vector<PatchedRange> patched{{0x1010, 2, 0x401010}};
|
||||
const auto reject = [&](const WriterSites& sites, const std::string& writer) { rejects([&] { CheckOverlaps("t", patched, sites); }, writer); };
|
||||
const std::vector<Codegen::Amd64OnlySubstitutionReport> inPlace{{"VPSHUFB", 0x100f, 4, 4, Codegen::Amd64OnlyLowering::InPlace}};
|
||||
reject({.Reports = inPlace}, "Intel in-place report");
|
||||
const std::vector<Codegen::Amd64OnlySubstitutionReport> kept{{"VPSHUFB", 0x100f, 4, 4, Codegen::Amd64OnlyLowering::Kept}};
|
||||
CheckOverlaps("t", patched, {.Reports = kept});
|
||||
const std::vector<Codegen::Amd64OnlySubstitutionReport> distant{{"VPSHUFB", 0x1000, 4, 4, Codegen::Amd64OnlyLowering::InPlace}};
|
||||
CheckOverlaps("t", patched, {.Reports = distant});
|
||||
const std::vector<Codegen::TrampolineSite> absorbed{{0x1008, 0x401008, 0x10, {}, {}, 0}};
|
||||
reject({.Trampolines = absorbed}, "Intel trampoline site");
|
||||
const std::vector<Codegen::TrampolineSite> shortSite{{0x1008, 0x401008, 4, {}, {}, 0}};
|
||||
CheckOverlaps("t", patched, {.Trampolines = shortSite});
|
||||
const std::vector<RelinkPatch> relink{{0x1011, {1, 2, 3}}};
|
||||
reject({.RelinkPatches = relink}, "relinker patch");
|
||||
const std::vector<RelinkPatch> relinkAfter{{0x1012, {1, 2, 3}}};
|
||||
CheckOverlaps("t", patched, {.RelinkPatches = relinkAfter});
|
||||
const std::vector<WriterRange> trap{{0x1011, 2, "syscall trap"}};
|
||||
reject({.Traps = trap}, "syscall trap");
|
||||
const std::vector<WriterRange> trapBefore{{0x100e, 2, "syscall trap"}};
|
||||
CheckOverlaps("t", patched, {.Traps = trapBefore});
|
||||
const std::vector<WriterRange> window{{0x100f, 4, "window"}};
|
||||
reject({.Traps = window}, "window");
|
||||
}
|
||||
|
||||
void conversion() {
|
||||
TestProgress progress;
|
||||
const Conversion converter(progress);
|
||||
const auto executable = conversionFixture();
|
||||
ConversionOptions plain;
|
||||
plain.TargetName = "custom-name.bin";
|
||||
const auto baseline = converter.Convert(conversionInput(executable), plain);
|
||||
require(baseline.AppliedPatches.empty(), "patches applied without entries");
|
||||
|
||||
ConversionOptions patched = plain;
|
||||
patched.Patches = {entry("custom-name.bin", 0x1001, {0}, {0x90}, "nop"), entry("sce_module/plugin.prx", 0x1002, {0}, {0x41})};
|
||||
const auto output = converter.Convert(conversionInput(executable), patched);
|
||||
require(output.AppliedPatches.size() == 2, "executable and module patches were not both applied");
|
||||
require(output.AppliedPatches[0].Target == "custom-name.bin" && output.AppliedPatches[1].Target == "sce_module/plugin.prx", "applied targets");
|
||||
require(output.Executable != baseline.Executable && output.Modules.front().Bytes != baseline.Modules.front().Bytes, "patches changed nothing");
|
||||
require(progress.Messages.find("Patch: custom-name.bin [nop] va=0x1001 off=0x1001 00 -> 90\n") != std::string::npos, "progress line: " + progress.Messages);
|
||||
require(progress.Messages.find("Patch: sce_module/plugin.prx va=0x1002 off=0x1002 00 -> 41\n") != std::string::npos, "module progress line");
|
||||
|
||||
ConversionOptions other = patched;
|
||||
other.TargetName = "other.bin";
|
||||
auto different = executable;
|
||||
different[0x1005] = 0x90;
|
||||
const auto second = converter.Convert(conversionInput(different), other);
|
||||
require(second.Modules.size() == output.Modules.size() && second.Modules.front().Bytes == output.Modules.front().Bytes, "shared modules differ between executables");
|
||||
require(second.AppliedPatches.size() == 1, "an executable entry hit another target");
|
||||
|
||||
ConversionOptions failing = plain;
|
||||
failing.Patches = {entry("custom-name.bin", 0x1001, {7}, {1})};
|
||||
rejects([&] { converter.Convert(conversionInput(executable), failing); }, "expected 07 found 00");
|
||||
failing.Patches = {entry("sce_module/plugin.prx", 0x1002, {7}, {1})};
|
||||
rejects([&] { converter.Convert(conversionInput(executable), failing); }, "expected 07 found 00");
|
||||
failing.Patches = {entry("custom-name.bin", 0x1000, {0xc3, 0}, {0, 0}), entry("custom-name.bin", 0x1001, {0}, {1})};
|
||||
rejects([&] { converter.Convert(conversionInput(executable), failing); }, "overlaps");
|
||||
|
||||
const auto repeated = converter.Convert(conversionInput(executable), plain);
|
||||
require(repeated.Executable == baseline.Executable && repeated.Modules.front().Bytes == baseline.Modules.front().Bytes, "no entries changed the output");
|
||||
}
|
||||
|
||||
void targets() {
|
||||
const std::vector<PatchEntry> entries{entry("a.elf", 1, {0}, {1}), entry("sce_module/libc.prx", 1, {0}, {1})};
|
||||
MatchTargets(entries, {"a.elf", "b.elf", "sce_module/libc.prx"});
|
||||
rejects([&] { MatchTargets(entries, {"a.elf", "b.elf"}); }, "unmatched patch target: sce_module/libc.prx");
|
||||
rejects([&] { MatchTargets(entries, {"a.elf", "a.elf", "sce_module/libc.prx"}); }, "ambiguous patch target: a.elf");
|
||||
MatchTargets({}, {});
|
||||
}
|
||||
|
||||
void verify() {
|
||||
auto bytes = twoSegmentFixture();
|
||||
const std::vector<VerifyTarget> executables{{"main.elf", bytes}};
|
||||
auto file = ParsePatchFile(header("main_target = \"main.elf\"\n[[patch]]\nname = \"one\"\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"0001\"\nvalue = \"0908\"\n"
|
||||
"[[patch]]\ntarget = \"main.elf\"\nva = 0x1100\nexpected = \"ff\"\nvalue = \"01\"\n"
|
||||
"[[patch]]\ntarget = \"sce_module/libc.prx\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n"));
|
||||
const auto before = bytes;
|
||||
const std::vector<VerifyTarget> modules{{"sce_module/libc.prx", twoSegmentFixture()}};
|
||||
auto input = VerifyPatches(file, executables, modules, false);
|
||||
require(input.size() == 3 && input[0].Status == VerifyStatus::Applicable && input[0].Name == "one" && input[0].Offset == 0x1000, "input applicable");
|
||||
require(input[1].Status == VerifyStatus::Mismatch && input[1].Detail.find("expected ff found 80") != std::string::npos, "input mismatch");
|
||||
require(input[2].Status == VerifyStatus::Applicable, "module entry in input mode");
|
||||
require(bytes == before && executables.front().Bytes == before, "verify wrote bytes");
|
||||
|
||||
auto patchedBytes = twoSegmentFixture();
|
||||
patchedBytes[0x1000] = 9;
|
||||
patchedBytes[0x1001] = 8;
|
||||
patchedBytes[0x2000] = 1;
|
||||
const std::vector<VerifyTarget> outputs{{"main.elf", patchedBytes}};
|
||||
auto output = VerifyPatches(file, outputs, {}, true);
|
||||
require(output[0].Status == VerifyStatus::Applied && output[1].Status == VerifyStatus::Applied, "output applied");
|
||||
require(output[2].Status == VerifyStatus::NotChecked && output[2].Detail == "module, or executable not named with --executable", "output module not-checked");
|
||||
rejects([&] { VerifyPatches(file, {{"main.elf", patchedBytes}, {"main.elf", patchedBytes}}, {}, true); }, "ambiguous patch target: main.elf");
|
||||
rejects([&] { VerifyPatches(file, {{"main.elf", patchedBytes}, {"bin/a.elf", patchedBytes}, {"bin/a.elf", patchedBytes}}, {}, true); }, "ambiguous patch target: bin/a.elf");
|
||||
output = VerifyPatches(file, {{"main.elf", twoSegmentFixture()}}, {}, true);
|
||||
require(output[0].Status == VerifyStatus::Mismatch && output[0].Detail.find("not applied") != std::string::npos, "unpatched output");
|
||||
|
||||
rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"typo.prx\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), executables, modules, false); }, "unmatched patch target: typo.prx");
|
||||
rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"typo.prx\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), executables, modules, false); }, "--verify-output");
|
||||
rejects([&] { VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), outputs, {}, true); }, "no main_target");
|
||||
auto exec = twoSegmentFixture();
|
||||
Io::WriteU16(exec, 16, 2);
|
||||
rejects([&] { VerifyPatches(file, {{"main.elf", exec}}, {}, true); }, "verify the input instead");
|
||||
rejects([&] { VerifyPatches(file, {{"main.elf", {'M', 'Z', 0x90, 0}}}, {}, true); }, "verify the input instead");
|
||||
rejects([&] { VerifyPatches(file, {{"main.elf", {0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1}}}, {}, true); }, "verify the input instead");
|
||||
rejects([&] { VerifyPatches(file, {{"main.elf", {}}}, {}, true); }, "verify the input instead");
|
||||
|
||||
auto sce = twoSegmentFixture();
|
||||
Io::WriteU16(sce, 16, 0xfe10);
|
||||
require(VerifyPatches(ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1000\nexpected = \"00\"\nvalue = \"01\"\n")), {{"main.elf", sce}}, {}, false).front().Status == VerifyStatus::Applicable,
|
||||
"input mode must accept any e_type");
|
||||
|
||||
require(OutputExecutablePath("out/app.elf", "bin/tool.elf") == std::filesystem::path("out/bin.tool.elf.elf"), "output executable name");
|
||||
require(OutputExecutablePath("out/app.exe", "./a/b/c.bin") == std::filesystem::path("out/a.b.c.bin.exe"), "output executable extension");
|
||||
for (const char* bad : {"../x", "/abs/x", "dir/", ""}) rejects([&] { OutputExecutablePath("out/app.elf", bad); }, "--executable must be a file");
|
||||
}
|
||||
|
||||
void rangeFailureInVerify() {
|
||||
const auto file = ParsePatchFile(header("[[patch]]\ntarget = \"main.elf\"\nva = 0x1200\nexpected = \"00\"\nvalue = \"01\"\n"));
|
||||
const auto results = VerifyPatches(file, {{"main.elf", twoSegmentFixture()}}, {}, false);
|
||||
require(results.front().Status == VerifyStatus::Mismatch && results.front().Detail.find("PT_LOAD") != std::string::npos, "range failure should be a mismatch");
|
||||
}
|
||||
|
||||
void manifest() {
|
||||
const AppliedPatch first{"main.elf", 0x1000, 0x1000, {0, 1}, {9, 8}, "set \"x\" later", "alpha"};
|
||||
const AppliedPatch second{"sce_module/libc.prx", 0x2000, 0x2000, {5}, {6}, "", ""};
|
||||
AppliedPatch duplicate = first;
|
||||
const auto merged = AggregateManifest({{first, second}, {duplicate, second}});
|
||||
require(merged.size() == 2, "records were not deduplicated");
|
||||
rejects([&] { WriteManifest(merged, "main.elf", ""); }, "Patch manifest cannot hold the description: set \"x\" later");
|
||||
|
||||
const AppliedPatch plain{"main.elf", 0x1000, 0x1000, {0, 1}, {9, 8}, "plain description", "alpha"};
|
||||
const auto text = WriteManifest(AggregateManifest({{plain}, {second}}), "main.elf", "");
|
||||
const auto parsed = ParsePatchFile(text);
|
||||
require(parsed.MainTarget == std::string("main.elf"), "main_target did not round-trip");
|
||||
require(parsed.Entries.size() == 2, "entry count did not round-trip");
|
||||
require(parsed.Entries[0].Name == "alpha" && parsed.Entries[0].Target == "main.elf" && parsed.Entries[0].VirtualAddress == 0x1000, "first entry");
|
||||
require(parsed.Entries[0].Expected == plain.Expected && parsed.Entries[0].Value == plain.Value && parsed.Entries[0].Description == "plain description", "first bytes");
|
||||
require(parsed.Entries[1].Name.empty() && parsed.Entries[1].Target == "sce_module/libc.prx" && parsed.Entries[1].Description.empty(), "second entry");
|
||||
require(text.find("name = \"alpha\"") != std::string::npos && text.find("main_target = \"main.elf\"") != std::string::npos, "manifest text");
|
||||
require(WriteManifest(AggregateManifest({{plain}, {second}}), "main.elf", "") == text, "manifest is not deterministic");
|
||||
}
|
||||
|
||||
void reservedNames() {
|
||||
for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"})
|
||||
rejects([&] { ParsePatchFile(namedEntry(reserved)); }, "line 3: name is reserved");
|
||||
ParsePatchFile(header("settings = \"a\"\nsettings_sha256 = \"b\"\n"));
|
||||
}
|
||||
|
||||
void relocationOverlap() {
|
||||
std::vector<std::uint8_t> table(48);
|
||||
Io::WriteU64(table, 0, 0x2010);
|
||||
Io::WriteU64(table, 24, 0x2100);
|
||||
const std::vector<PatchedRange> hit{{0x1010, 2, 0x2017}};
|
||||
const std::vector<PatchedRange> adjacent{{0x1010, 2, 0x2018}};
|
||||
const std::vector<PatchedRange> before{{0x1010, 2, 0x200f}};
|
||||
rejects([&] { CheckOverlaps("t", hit, {.RelaData = table}); }, "RelaData relocation target");
|
||||
rejects([&] { CheckOverlaps("t", hit, {.RelaPltData = table}); }, "RelaPltData relocation target");
|
||||
rejects([&] { CheckOverlaps("t", {{0x1010, 1, 0x2100}}, {.RelaData = table}); }, "relocation target at va=0x2100");
|
||||
rejects([&] { CheckOverlaps("t", before, {.RelaPltData = table}); }, "RelaPltData");
|
||||
CheckOverlaps("t", adjacent, {.RelaData = table, .RelaPltData = table});
|
||||
CheckOverlaps("t", {{0x1010, 2, 0x200c}}, {.RelaData = table});
|
||||
}
|
||||
|
||||
void manifestConfig() {
|
||||
const AppliedPatch patch{"main.elf", 0x1000, 0x1000, {0}, {1}, "", ""};
|
||||
const auto config = [&](const std::string& path) { return WriteManifest({patch}, "main.elf", path); };
|
||||
require(config("").find("config =") == std::string::npos, "empty config was recorded");
|
||||
require(config("dir/p.toml").find("config = \"dir/p.toml\"\n") != std::string::npos, "forward-slash path");
|
||||
rejects([&] { config("dir/p\x7f.toml"); }, "Patch manifest cannot hold the config");
|
||||
rejects([&] { config("dir\\p.toml"); }, "Patch manifest cannot hold the config: dir\\p.toml");
|
||||
const auto quoted = config("we\"ird/p.toml");
|
||||
require(quoted.find("config = \"p.toml\"\n") != std::string::npos, "quote path was not reduced to the file name");
|
||||
require(ParsePatchFile(quoted).Entries.size() == 1, "manifest with a reduced config did not parse");
|
||||
rejects([&] { config("dir/a\"b.toml"); }, "Patch manifest cannot hold the config");
|
||||
const auto hold = [&](AppliedPatch bad, const std::string& field) { rejects([&] { WriteManifest({bad}, "main.elf", ""); }, "Patch manifest cannot hold the " + field + ": "); };
|
||||
auto bad = patch;
|
||||
bad.Name = "a\"b";
|
||||
hold(bad, "name");
|
||||
bad = patch;
|
||||
bad.Target = "a\"b";
|
||||
hold(bad, "target");
|
||||
bad = patch;
|
||||
bad.Description = "a\"b";
|
||||
hold(bad, "description");
|
||||
}
|
||||
|
||||
void typedManifest() {
|
||||
const auto file = ParsePatchFile(header("main_target = \"main.elf\"\n[[patch]]\nname = \"neg\"\ntarget = \"main.elf\"\nva = 0x1000\ntype = \"i32\"\nexpected = -1\nvalue = 0x18000000\nmin = -5\nmax = 0x7f000000\nalign = 0x1000\n"
|
||||
"[[patch]]\ntarget = \"main.elf\"\nva = 0x1100\ntype = \"u8\"\nexpected = 128\nvalue = 1\n"
|
||||
"[[patch]]\ntarget = \"main.elf\"\nva = 0x1104\nexpected = \"84\"\nvalue = \"02\"\n"));
|
||||
auto bytes = twoSegmentFixture();
|
||||
Io::WriteU32(bytes, 0x1000, 0xffffffff);
|
||||
const auto before = bytes;
|
||||
const auto patched = PatchTarget(bytes, "main.elf", file.Entries);
|
||||
require(patched.Applied.size() == 3 && patched.Applied[0].Type == "i32" && patched.Applied[0].Min && patched.Applied[0].Min->Negative, "typed fields are not carried");
|
||||
const auto text = WriteManifest(AggregateManifest({patched.Applied}), "main.elf", "");
|
||||
require(text.find("type = \"i32\"") != std::string::npos && text.find("expected = -1\n") != std::string::npos && text.find("min = -5\n") != std::string::npos
|
||||
&& text.find("max = 0x7f000000\n") != std::string::npos && text.find("align = 0x1000\n") != std::string::npos && text.find("value = 0x18000000\n") != std::string::npos, "typed manifest text: " + text);
|
||||
require(text.find("expected = \"84\"") != std::string::npos, "raw entry stayed raw");
|
||||
const auto reparsed = ParsePatchFile(text);
|
||||
require(reparsed.Entries.size() == 3 && reparsed.Entries[0].Name == "neg" && reparsed.Entries[0].Type == "i32", "typed round-trip");
|
||||
for (std::size_t index = 0; index < 3; ++index)
|
||||
require(reparsed.Entries[index].Expected == file.Entries[index].Expected && reparsed.Entries[index].Value == file.Entries[index].Value, "round-trip bytes");
|
||||
auto again = before;
|
||||
PatchTarget(again, "main.elf", reparsed.Entries);
|
||||
require(again == bytes, "re-applying the manifest gives different bytes");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
grammarAccepted();
|
||||
grammarRejected();
|
||||
integerRanges();
|
||||
boundsAndWidth();
|
||||
names();
|
||||
reservedNames();
|
||||
relocationOverlap();
|
||||
typedManifest();
|
||||
manifestConfig();
|
||||
ranges();
|
||||
headerRange();
|
||||
applyAndMismatch();
|
||||
overlapWithWriters();
|
||||
conversion();
|
||||
targets();
|
||||
verify();
|
||||
rangeFailureInVerify();
|
||||
manifest();
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import struct
|
||||
|
||||
from test_guest_host_libc import DYNAMIC, importing
|
||||
from test_guest_intel_trampolines import PLAIN_SITE, SITE, elf_loads, guest_fixture, main_fixture
|
||||
from test_guest_syscall_trap import GENERIC_STUB
|
||||
import test_jmprel_table_bounds
|
||||
from test_guest_none_relocations import JUMP_SLOT, RELATIVE, TARGET, module
|
||||
|
||||
|
||||
def patch_site(image):
|
||||
for _, flags, offset, mapped, _, file_size, _, _ in elf_loads(image):
|
||||
if flags & 1:
|
||||
return mapped + file_size - 1, image[offset + file_size - 1]
|
||||
raise AssertionError("No executable segment")
|
||||
|
||||
|
||||
def patch_text(address, expected, name="", target="eboot.bin"):
|
||||
lines = ["format = 1", "", "[[patch]]"]
|
||||
if name:
|
||||
lines.append(f'name = "{name}"')
|
||||
lines += [f'target = "{target}"', f"va = {address:#x}", f'expected = "{expected:02x}"', f'value = "{expected ^ 1:02x}"', 'description = "flip one bit"', ""]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
relinker = Path(sys.argv[1]).resolve()
|
||||
image = bytes(main_fixture())
|
||||
address, expected = patch_site(image)
|
||||
with tempfile.TemporaryDirectory(prefix="anyps5-game-patches-") as directory:
|
||||
work = Path(directory)
|
||||
|
||||
def run(*arguments):
|
||||
return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60)
|
||||
|
||||
def case(name):
|
||||
root = work / name
|
||||
(root / "in").mkdir(parents=True)
|
||||
(root / "out").mkdir()
|
||||
(root / "in" / "eboot.bin").write_bytes(image)
|
||||
return root, root / "in" / "eboot.bin", root / "out" / "app.elf"
|
||||
|
||||
root, source, output = case("plain")
|
||||
result = run("--skip-sce-module", str(source), str(output))
|
||||
assert result.returncode == 0, result.stderr
|
||||
baseline = output.read_bytes()
|
||||
assert not list(output.parent.glob("*.patches.toml")), "manifest written without patches"
|
||||
|
||||
(source.parent / "relinker-patches.toml").write_text(patch_text(address, expected, "flip_bit"))
|
||||
result = run("--skip-sce-module", "--no-patches", str(source), str(output))
|
||||
assert result.returncode == 0 and output.read_bytes() == baseline, result.stderr
|
||||
assert "Patches:" not in result.stdout and not list(output.parent.glob("*.patches.toml"))
|
||||
|
||||
result = run("--skip-sce-module", str(source), str(output))
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
patched = output.read_bytes()
|
||||
assert patched != baseline
|
||||
assert f"Patches: {source.parent / 'relinker-patches.toml'}\n" in result.stdout, result.stdout
|
||||
assert f"Patch: eboot.bin [flip_bit] va={address:#x} off=" in result.stdout, result.stdout
|
||||
assert f"{expected:02x} -> {expected ^ 1:02x} (flip one bit)\n" in result.stdout, result.stdout
|
||||
manifest = output.parent / "app.patches.toml"
|
||||
text = manifest.read_text()
|
||||
assert 'main_target = "eboot.bin"' in text and 'name = "flip_bit"' in text, text
|
||||
assert f"Patch manifest: {manifest}\n" in result.stdout
|
||||
assert f'config = "{source.parent / "relinker-patches.toml"}"\n' in text, text
|
||||
|
||||
quoted = work / "quoted"
|
||||
(quoted / "out").mkdir(parents=True)
|
||||
(quoted / "eboot.bin").write_bytes(image)
|
||||
(quoted / 'we"ird.toml').write_text(patch_text(address, expected))
|
||||
result = run("--skip-sce-module", "--patches", str(quoted / 'we"ird.toml'), str(quoted / "eboot.bin"), str(quoted / "out" / "app.elf"))
|
||||
assert result.returncode == 2 and "Patch manifest cannot hold the config" in result.stderr, (result.stdout, result.stderr)
|
||||
assert not list((quoted / "out").iterdir()), "output written despite the manifest error"
|
||||
|
||||
reduced = work / 'we"ird'
|
||||
(reduced / "out").mkdir(parents=True)
|
||||
(reduced / "eboot.bin").write_bytes(image)
|
||||
(reduced / "p.toml").write_text(patch_text(address, expected))
|
||||
result = run("--skip-sce-module", "--patches", str(reduced / "p.toml"), str(reduced / "eboot.bin"), str(reduced / "out" / "app.elf"))
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
assert 'config = "p.toml"\n' in (reduced / "out" / "app.patches.toml").read_text()
|
||||
|
||||
result = run("--verify-patches", str(manifest), str(source), "--skip-sce-module")
|
||||
assert result.returncode == 0 and "applicable: flip_bit eboot.bin" in result.stdout, (result.stdout, result.stderr)
|
||||
before = sorted(str(path) for path in work.rglob("*"))
|
||||
result = run("--verify-patches", str(manifest), str(output), "--verify-output")
|
||||
assert result.returncode == 0 and "applied: flip_bit eboot.bin" in result.stdout, (result.stdout, result.stderr)
|
||||
assert sorted(str(path) for path in work.rglob("*")) == before, "verify wrote files"
|
||||
renamed = work / "renamed"
|
||||
renamed.mkdir()
|
||||
(renamed / "eboot.bin").write_bytes(patched)
|
||||
result = run("--verify-patches", str(manifest), str(renamed / "eboot.bin"), "--skip-sce-module")
|
||||
assert result.returncode == 1 and "mismatch: flip_bit eboot.bin" in result.stdout, (result.stdout, result.stderr)
|
||||
|
||||
other = work / "reproduce"
|
||||
other.mkdir()
|
||||
(source.parent / "relinker-patches.toml").unlink()
|
||||
result = run("--skip-sce-module", "--patches", str(manifest), str(source), str(other / "app.elf"))
|
||||
assert result.returncode == 0 and (other / "app.elf").read_bytes() == patched, result.stderr
|
||||
|
||||
clash = work / "clash"
|
||||
(clash / "in").mkdir(parents=True)
|
||||
(clash / "in" / "eboot.bin").write_bytes(image)
|
||||
(clash / "in" / "app.patches.toml").write_text(patch_text(address, expected))
|
||||
result = run("--skip-sce-module", "--patches", str(clash / "in" / "app.patches.toml"), str(clash / "in" / "eboot.bin"), str(clash / "in" / "app.elf"))
|
||||
assert result.returncode == 2 and "would overwrite the patch file" in result.stderr, (result.stdout, result.stderr)
|
||||
assert not (clash / "in" / "app.elf").exists()
|
||||
|
||||
hand = work / "hand.toml"
|
||||
hand.write_text(patch_text(address, expected))
|
||||
result = run("--verify-patches", str(hand), str(output), "--verify-output")
|
||||
assert result.returncode == 2 and "no main_target" in result.stderr, result.stderr
|
||||
|
||||
root, source, output = case("wrong-bytes")
|
||||
(root / "p.toml").write_text(patch_text(address, expected ^ 0x55))
|
||||
result = run("--skip-sce-module", "--patches", str(root / "p.toml"), str(source), str(output))
|
||||
assert result.returncode == 2 and "expected" in result.stderr and "found" in result.stderr, result.stderr
|
||||
assert not list(output.parent.iterdir()), "output written despite a failed patch"
|
||||
result = run("--verify-patches", str(root / "p.toml"), str(source), "--skip-sce-module")
|
||||
assert result.returncode == 1 and "mismatch: eboot.bin" in result.stdout, result.stdout
|
||||
|
||||
root, source, output = case("unmatched")
|
||||
(root / "p.toml").write_text(patch_text(address, expected, target="missing.prx"))
|
||||
result = run("--skip-sce-module", "--patches", str(root / "p.toml"), str(source), str(output))
|
||||
assert result.returncode == 2 and "unmatched patch target: missing.prx" in result.stderr, result.stderr
|
||||
assert not list(output.parent.iterdir())
|
||||
result = run("--verify-patches", str(root / "p.toml"), str(source), "--skip-sce-module")
|
||||
assert result.returncode == 2 and "--verify-output" in result.stderr, result.stderr
|
||||
|
||||
root, source, output = case("extra")
|
||||
(root / "in" / "picker").mkdir()
|
||||
(root / "in" / "picker" / "picker.bin").write_bytes(image)
|
||||
(root / "p.toml").write_text(patch_text(address, expected, target="picker/picker.bin"))
|
||||
(root / "in" / "sce_module").mkdir()
|
||||
result = run("--executable", "picker/picker.bin", "--patches", str(root / "p.toml"), str(source), str(output))
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
manifest = output.parent / "app.patches.toml"
|
||||
result = run("--verify-patches", str(manifest), str(output), "--verify-output", "--executable", "picker/picker.bin")
|
||||
assert result.returncode == 0 and "applied: picker/picker.bin" in result.stdout, (result.stdout, result.stderr)
|
||||
(output.parent / "picker.picker.bin.elf").unlink()
|
||||
result = run("--verify-patches", str(manifest), str(output), "--verify-output", "--executable", "picker/picker.bin")
|
||||
assert result.returncode == 2 and "output not found" in result.stderr, result.stderr
|
||||
|
||||
relocation_overlap(relinker, work)
|
||||
writer_overlap(relinker, work)
|
||||
|
||||
|
||||
def relocation_overlap(relinker, work):
|
||||
def run(*arguments):
|
||||
return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60)
|
||||
|
||||
def patch_file(path, target, address, expected):
|
||||
path.write_text(f'format = 1\n\n[[patch]]\ntarget = "{target}"\nva = {address:#x}\nexpected = "{expected}"\nvalue = "ffff"\n')
|
||||
|
||||
for kind, rela, plt in (("rela", [RELATIVE], []), ("plt", [], [JUMP_SLOT])):
|
||||
image = bytes(module(rela, plt))
|
||||
for offset, overlaps in ((TARGET + 4, True), (TARGET + 8, False)):
|
||||
case = work / f"mod-{kind}-{offset:x}"
|
||||
(case / "in" / "sce_module").mkdir(parents=True)
|
||||
(case / "in" / "sce_module" / "module.prx").write_bytes(image)
|
||||
(case / "in" / "eboot.bin").write_bytes(main_fixture())
|
||||
(case / "out").mkdir()
|
||||
expected = image[0x2000 + (offset - 0x2000):0x2000 + (offset - 0x2000) + 2].hex()
|
||||
patch_file(case / "p.toml", "sce_module/module.prx", offset, expected)
|
||||
result = run("--patches", str(case / "p.toml"), str(case / "in" / "eboot.bin"), str(case / "out" / "app.elf"))
|
||||
if overlaps:
|
||||
assert result.returncode == 2 and "relocation target" in result.stderr, (kind, result.stdout, result.stderr)
|
||||
assert not list((case / "out").iterdir()), "output written despite the overlap"
|
||||
else:
|
||||
assert result.returncode == 0, (kind, result.stdout, result.stderr)
|
||||
|
||||
image = bytearray(importing())
|
||||
struct.pack_into("<qQ", image, DYNAMIC + 5 * 16, 8, 24)
|
||||
address = 0x4d00
|
||||
struct.pack_into("<QQq", image, 0x4700, address, 8, 0x4000)
|
||||
for offset, overlaps in ((address + 2, True), (address + 8, False)):
|
||||
case = work / f"exe-{offset:x}"
|
||||
(case / "out").mkdir(parents=True)
|
||||
(case / "eboot.bin").write_bytes(image)
|
||||
patch_file(case / "p.toml", "eboot.bin", offset, bytes(image[offset:offset + 2]).hex())
|
||||
result = run("--skip-sce-module", "--patches", str(case / "p.toml"), str(case / "eboot.bin"), str(case / "out" / "app.elf"))
|
||||
if overlaps:
|
||||
assert result.returncode == 2 and "RelaData relocation target" in result.stderr, (result.stdout, result.stderr)
|
||||
assert not list((case / "out").iterdir())
|
||||
else:
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
|
||||
|
||||
|
||||
def writer_overlap(relinker, work):
|
||||
def run(*arguments):
|
||||
return subprocess.run([str(relinker), *arguments], capture_output=True, text=True, timeout=60)
|
||||
|
||||
def attempt(name, module, options, address, expected, value="ff", target="sce_module/sample.prx"):
|
||||
case = work / name
|
||||
(case / "in" / "sce_module").mkdir(parents=True)
|
||||
(case / "in" / "sce_module" / "sample.prx").write_bytes(module)
|
||||
(case / "in" / "eboot.bin").write_bytes(main_fixture())
|
||||
(case / "out").mkdir()
|
||||
(case / "p.toml").write_text(f'format = 1\n\n[[patch]]\ntarget = "{target}"\nva = {address:#x}\nexpected = "{expected}"\nvalue = "{value}"\n')
|
||||
result = run(*options, "--patches", str(case / "p.toml"), str(case / "in" / "eboot.bin"), str(case / "out" / "app.elf"))
|
||||
return result, case / "out"
|
||||
|
||||
intel = bytes(guest_fixture(SITE))
|
||||
result, out = attempt("intel-hit", intel, ["--to-intel"], 0x1007, "08", "09")
|
||||
assert result.returncode == 2 and "overlaps Intel" in result.stderr, (result.stdout, result.stderr)
|
||||
assert not list(out.iterdir())
|
||||
result, out = attempt("intel-clear", intel, ["--to-intel"], 0x1020, "90")
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
|
||||
trap = bytearray(guest_fixture(PLAIN_SITE))
|
||||
trap[0x400:0x400 + len(GENERIC_STUB)] = GENERIC_STUB
|
||||
trap = bytes(trap)
|
||||
result, out = attempt("trap-hit", trap, ["--trap-sdk-syscall-stubs"], 0x1017, "0f05", "0f05")
|
||||
assert result.returncode == 2 and "overlaps syscall trap" in result.stderr, (result.stdout, result.stderr)
|
||||
assert not list(out.iterdir())
|
||||
result, out = attempt("trap-clear", trap, ["--trap-sdk-syscall-stubs"], 0x1000, "48", "48")
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
|
||||
image = bytes(test_jmprel_table_bounds.fixture(linux=True))
|
||||
for offset, overlaps in ((0x2ff, True), (0x2fe, False), (0x307, True), (0x308, False)):
|
||||
case = work / f"exe-plt-{offset:x}"
|
||||
(case / "out").mkdir(parents=True)
|
||||
(case / "eboot.bin").write_bytes(image)
|
||||
(case / "p.toml").write_text(f'format = 1\n\n[[patch]]\ntarget = "eboot.bin"\nva = {offset:#x}\nexpected = "{image[offset:offset + 2].hex()}"\nvalue = "ffff"\n')
|
||||
result = run("--skip-sce-module", "--patches", str(case / "p.toml"), str(case / "eboot.bin"), str(case / "out" / "app.elf"))
|
||||
if overlaps:
|
||||
assert result.returncode == 2 and "RelaPltData relocation target" in result.stderr, (offset, result.stdout, result.stderr)
|
||||
assert not list((case / "out").iterdir())
|
||||
else:
|
||||
assert result.returncode == 0, (offset, result.stdout, result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -45,6 +45,10 @@ flowchart LR
|
||||
- [`core/relinker/main.cpp`](../../core/relinker/main.cpp) runs the steps in this order. `--to-intel` is optional, see [USAGE.md](../user/USAGE.md).
|
||||
- Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("<nid>", func)` sets the NID directly when the name is unknown. Every other library keeps the names libc exports; it reads them from `libc.exports`, which `nid_patcher --write-export-list` rewrites only when that list changes. On Windows the link writes each import library with `SOURCE_DATE_EPOCH=0` and `ranlib -D` and replaces it only when its bytes change. A source edit that keeps the exports therefore relinks and re-patches only its own library.
|
||||
|
||||
## Game patches
|
||||
|
||||
[`GamePatch.cpp`](../../core/relinker/relinker/src/patching/GamePatch.cpp) parses the patch file, a strict subset of TOML. `Conversion::Convert` applies the entries of its own target to the executable bytes right after reading them, and to each bundled module before `GuestModuleBuilder` reads it, so every later stage sees patched bytes. Entry virtual addresses are translated through the `PT_LOAD` segments, and a range may not touch the ELF header or program header table, which the output writers rewrite. The patched file ranges are then checked against every in-place writer: the `--to-intel` reports and trampoline sites, `RelinkPatch` records and syscall trap ranges. The patched virtual address ranges are checked against the 8-byte targets of the `RelaData` and `RelaPltData` relocations, which the loader or the output writers overwrite. `main.cpp` builds the manifest text before it writes any output. `main.cpp` matches targets after the conversions, aggregates the applied records into `<output-stem>.patches.toml`, and runs `--verify-patches` through `GamePatch::VerifyPatches`, which reads only bytes.
|
||||
|
||||
## Windows deferred module initialization
|
||||
|
||||
Runtime-loaded Windows modules export `__aps5_guest_initialize`: the entry RVA, the initializer count and the RVAs of initializer pointer slots. [DynamicLoader.cpp](../../core/libs/prx/libkernel/Module/src/DynamicLoader.cpp) consumes the first two words in the mapped image after initialization, so another reference to that image does not run it again. Unloading the image discards this state; a later load reads the original table from the file, including when Windows reuses the same base address. Native references outside the guest handle table retain the same initialized image.
|
||||
|
||||
+57
-1
@@ -72,6 +72,10 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath`
|
||||
| `--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`. |
|
||||
| `--patches <file>` | Apply the byte patches of this file during conversion, see [game patches](#game-patches). Overrides `relinker-patches.toml` discovery. Conflicts with `--no-patches`. |
|
||||
| `--no-patches` | Do not apply patches, and do not load `relinker-patches.toml` from the input directory. |
|
||||
| `--verify-patches <file>` | Check a patch file against `<path>` without writing anything, see [game patches](#game-patches). Takes one path and no output path. |
|
||||
| `--verify-output` | With `--verify-patches`, treat `<path>` as a converted Linux output instead of an input executable. |
|
||||
| `--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. |
|
||||
@@ -84,6 +88,58 @@ The `--skip-sce-module`, `--exclude-sce-module <file>`, `--skip-syscall-check`,
|
||||
|
||||
`--trap-sdk-syscall-stubs` is also for debugging. It preserves a runtime failure if a syscall wrapper is called; it does not implement FreeBSD syscalls or make these applications generally safe to run.
|
||||
|
||||
## Game patches
|
||||
|
||||
A patch file changes bytes of the input executable or of a bundled module before conversion, for a title that hard-codes a value the host cannot meet. The relinker checks the bytes it replaces, so a patch for another version of the title fails instead of corrupting it. With no patch file the output is unchanged.
|
||||
|
||||
`relinker-patches.toml` in the directory of the input executable is loaded automatically and announced as `Patches: <path>`. `--patches <file>` names another file, and `--no-patches` loads none. Each applied patch is reported:
|
||||
|
||||
```
|
||||
Patch: <target> [<name>] va=0x<address> off=0x<offset> <old> -> <new> (<description>)
|
||||
```
|
||||
|
||||
The file is a subset of TOML; any other TOML syntax is an error that names the line. It is UTF-8, with blank lines, `#` comments outside strings, `[[patch]]` entries and `key = value` lines. Integers are decimal or `0x` hexadecimal, strings are double-quoted without escapes, and keys match `[A-Za-z0-9_]+`.
|
||||
|
||||
```toml
|
||||
format = 1
|
||||
|
||||
[[patch]]
|
||||
name = "pool_size"
|
||||
target = "eboot.elf"
|
||||
va = 0x1234
|
||||
type = "i32"
|
||||
expected = 0x10000000
|
||||
value = 0x18000000
|
||||
max = 0x7f000000
|
||||
align = 0x1000
|
||||
description = "pool size"
|
||||
```
|
||||
|
||||
`format = 1` is required. The top-level keys `relinker_version`, `relinker_commit`, `config`, `config_sha256`, `main_target`, `settings` and `settings_sha256` are accepted in any file; only `main_target` is used, by `--verify-output`. Any other top-level key is an error.
|
||||
|
||||
| Entry key | Meaning |
|
||||
|-----------|---------|
|
||||
| `name` | Optional. A setting name of `[a-z0-9_]`, unique within the file and not one of the reserved top-level key names (`format` and the seven keys above), used by a later `--set` option. Shown in the progress line, the manifest and the verify report. |
|
||||
| `target` | Required. The main executable's file name, or for `--executable` files and bundled modules the path relative to the module parent directory, for example `sce_module/libc.prx`. |
|
||||
| `va` | Required. The virtual address of the ELF as a disassembler shows it. |
|
||||
| `type` | Optional: `u8 u16 u32 u64 i8 i16 i32 i64`. Then `expected`, `value`, `min`, `max` and `align` are integers, range-checked and written little-endian. Without it, `expected` and `value` are hexadecimal byte strings of the same length. |
|
||||
| `expected` | Required. The bytes the input must hold. |
|
||||
| `value` | Required. The bytes to write. |
|
||||
| `min`, `max`, `align` | Optional, typed entries only; checked against `value`. |
|
||||
| `description` | Optional. Reported and copied to the manifest. |
|
||||
|
||||
x86 `mov r/m64, imm32` and `add`/`sub`/`cmp r/m64, imm32` sign-extend their immediate. To patch a 64-bit field through an imm32 use `type = "i32"`, and add `min` to keep negative values out.
|
||||
|
||||
Entries apply in file order. A patch range must lie in the file bytes of one `PT_LOAD` and outside the ELF header and program header table. The conversion fails before writing any output if bytes differ from `expected`, a range is not valid, two entries overlap, a target matches no file or more than one file, or a range overlaps bytes the relinker itself rewrites (`--to-intel` replacements and trampolines, relinker patches, syscall traps) or the 8-byte target of a relocation of the file.
|
||||
|
||||
When at least one patch is applied, `<output-stem>.patches.toml` is written beside the output. It lists every applied entry for every converted file, with `main_target` and `config` (the patch file path in forward-slash form, reduced to its file name when the path holds a `"`, backslash or control character; the conversion fails before writing any output when even that cannot be held); typed entries keep their `type`, integers and `min`, `max` and `align`, and `--patches` accepts it to reproduce the conversion. The manifest must not replace the patch file it was read from.
|
||||
|
||||
`--verify-patches <file> <path>` writes nothing and exits `0` when no entry is a mismatch. Without `--verify-output`, `<path>` is the input executable of any type, modules are found as in a conversion (module options and `--executable` apply), and each entry is `applicable` (the input holds `expected`) or `mismatch`. A target that matches nothing is an error that suggests `--verify-output`.
|
||||
|
||||
With `--verify-output`, `<path>` is a converted Linux ELF (`ET_DYN`); `ET_EXEC` output from `--linux-fixed-base`, PE and Mach-O outputs are refused. The file must have `main_target`, so verify the generated manifest, not a hand-written file. The entry whose target is `main_target` is checked against `<path>`, and entries for the `--executable` strings given are checked against the output file derived as in a conversion; an entry holding `value` is `applied`. Every other entry is `not-checked (module, or executable not named with --executable)`, including a mistyped module target. Two targets with the same name, such as an `--executable` equal to `main_target`, are an `ambiguous patch target` error. Module discovery options are rejected with `--verify-output`.
|
||||
|
||||
Verify mode rejects the conversion options (`--windows`, `--macos`, `--to-intel`, `--rpath`, `--patches`, `--no-patches` and the like) and a second path.
|
||||
|
||||
## Runtime layout
|
||||
|
||||
For an executable that assumes its image starts at `0x400000`, use `relinker --windows --windows-fixed-base 0x400000 source/input.elf app.exe`. This maps its lowest ELF load page there; the PE headers occupy the preceding 64 KiB. Use this option only when the executable requires an exact address. Without it, the Windows executable has no `DYNAMIC_BASE` and loads at its preferred base `0x140000000`; relocations are kept, so Windows relocates it if that range is taken. Modules keep `DYNAMIC_BASE`. This option does not change macOS output.
|
||||
@@ -273,7 +329,7 @@ With `APS5_STALL_WATCHDOG_SECS=<n>`, a queue that has waited `n` seconds without
|
||||
|
||||
## Exit codes
|
||||
|
||||
`0`: conversion succeeded or help was displayed. `1`: invalid arguments. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code.
|
||||
`0`: conversion succeeded or help was displayed. `1`: invalid arguments, or a `--verify-patches` entry that is a mismatch. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code.
|
||||
|
||||
## Import audit
|
||||
|
||||
|
||||
Reference in new issue
Block a user