Files
AnyPS5/core/relinker/main.cpp
T
abderahmane-ai bd750b5954 feat(relinker): write a registry for each converted bundled module
--registry only described the imports of the executable, so a title whose
own modules import system functions (PSN.prx, libc.prx and others) could
not be audited completely. With --registry the relinker now also writes
<output-stem>.<module>.registry.json for every converted guest module.

An entry is written for each relocation against an undefined symbol that
no other bundled module exports, the same test the module builder uses to
decide that a dependency is satisfied by a sibling. Entries carry nid,
library and targetOffset; the symbol name no longer has the # suffix by the
time the module is read, so the NID is the bare 11 characters. The
executable's registry is unchanged.

The test converts two modules where one is provided by its sibling, for
Linux and Windows output, covers the DT_RELA and DT_JMPREL tables, checks
that the same module imports the sibling's function once the sibling is
absent, and that nothing is written without --registry. Registered in
CTest as guest_registry.
2026-10-09 22:44:09 +02:00

167 lines
9.1 KiB
C++

#include <Cli.hpp>
#include <domain/Types.hpp>
#include <io/FileReader.hpp>
#include <io/FileWriter.hpp>
#include <elfpatcher/linux/LinuxElfPatcher.hpp>
#include <elfpatcher/general/SegmentFilter.hpp>
#include <elfpatcher/general/EntryStubBuilder.hpp>
#include <elfpatcher/general/ProgramHeaderLayoutBuilder.hpp>
#include <elfpatcher/general/SectionHeaderTableBuilder.hpp>
#include <elfpatcher/windows/WindowsElfPatcher.hpp>
#include <io/ByteWriter.hpp>
#include <relinker/parsing/ElfReader.hpp>
#include <relinker/analysis/ValidationPolicy.hpp>
#include <relinker/analysis/SyscallScanner.hpp>
#include <relinker/analysis/CallSiteResolver.hpp>
#include <relinker/analysis/UnusedNidFilter.hpp>
#include <relinker/output/SysVDynamicSectionBuilder.hpp>
#include <relinker/output/CallRegistryWriter.hpp>
#include <relinker/pipeline/RelinkerPipeline.hpp>
#include <relinker/guest/GuestImage.hpp>
#include <codegen/IAmd64OnlyConverter.hpp>
#include <map>
#include <codegen/CodegenException.hpp>
#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
int main(const int argc, char* argv[]) {
Cli::Args args;
try {
args = Cli::ParseArgs(argc, argv);
} catch (const std::exception& e) {
std::cerr << "FAIL: " << e.what() << "\n";
return 1;
}
try {
auto extension = std::filesystem::path(args.outputPath).extension().string();
for (auto& character : extension) if (character >= 'A' && character <= 'Z') character = static_cast<char>(character + ('a' - 'A'));
if (!args.toWindows && extension == ".exe") std::cerr << "WARNING: Output filename ends with .exe, but --windows was not specified. The output will be a Linux ELF executable.\n";
Io::FileReader fileReader;
Io::FileWriter fileWriter;
auto sourceBytes = fileReader.Read(args.inputPath);
const std::string absPath = std::filesystem::absolute(args.outputPath).string();
std::vector<Codegen::TrampolineSite> trampolines;
if (args.toIntel) {
const auto codeSegments = Relinker::ElfReader(sourceBytes).ReadCodeSegments();
auto converted = Codegen::MakeAmd64OnlyConverter()->Convert(std::move(sourceBytes), codeSegments);
sourceBytes = std::move(converted.Bytes);
trampolines = std::move(converted.Trampolines);
std::map<std::string, std::size_t> stubsByName;
for (const auto& report : converted.Reports) {
if (report.Lowering == Codegen::Amd64OnlyLowering::Kept)
std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n";
else if (report.InstructionName == "VRSQRTPS" || report.InstructionName == "VRCPPS")
++stubsByName[report.InstructionName];
else
std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n";
}
for (const auto& [name, count] : stubsByName)
std::cout << "Intel substitution: " << name << " -> stub at " << count << " sites\n";
std::cout << "Intel conversion: " << converted.ReplacedCount << " in place, " << trampolines.size() << " stubs, " << converted.KeptCount << " kept\n";
}
auto elfReader = std::make_shared<Relinker::ElfReader>(sourceBytes);
const std::shared_ptr<Relinker::ISyscallScanner> syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner();
const auto pipeline = std::make_shared<Relinker::RelinkerPipeline>(
elfReader,
syscallScanner,
Relinker::MakeCallSiteResolver(),
std::make_shared<Relinker::ValidationPolicy>(),
std::make_shared<Relinker::SysVDynamicSectionBuilder>(),
args.unusedFilterLevel == 2 ? Relinker::MakeStrictUnusedNidFilter() : Relinker::MakeUnusedNidFilter(),
args.unusedFilterLevel
);
std::cout << "System: " << (args.toWindows ? "Windows" : "Linux") << "; unused-filter=" << args.unusedFilterLevel << "\n";
std::cout << "sce_module/sce_modules/prx processing: " << (args.skipSceModule ? "disabled (--skip-sce-module)" : "enabled") << '\n';
for (const auto& name : args.excludedSceModules) std::cout << "Guest module excluded: " << name << '\n';
auto result = pipeline->Relink(sourceBytes);
for (const auto& patch : result.Patches) {
if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset)
throw Domain::RelinkerException("Relinker patch exceeds source image", patch.Offset);
for (std::size_t index = 0; index < patch.Bytes.size(); ++index) sourceBytes[patch.Offset + index] = patch.Bytes[index];
}
std::vector<Relinker::GuestArtifact> guestArtifacts;
if (!args.skipSceModule) {
guestArtifacts = Relinker::GuestModuleBuilder().Build(args.inputPath, absPath, result.DynamicSection, args.toWindows, args.toIntel, *syscallScanner, args.lazyBinding, args.runPath, args.excludedSceModules);
}
if (args.writeRegistry) {
const std::filesystem::path outFsPath(absPath);
const std::string registryPath = (outFsPath.parent_path() / (outFsPath.stem().string() + ".registry.json")).string();
const auto registryWriter = std::make_shared<Relinker::CallRegistryWriter>();
fileWriter.Write(registryPath, registryWriter->WriteCallRegistry(result.RegistryEntries));
for (const auto& artifact : guestArtifacts) {
const auto modulePath = outFsPath.parent_path() / (outFsPath.stem().string() + "." + artifact.Path.filename().string() + ".registry.json");
fileWriter.Write(modulePath.string(), registryWriter->WriteModuleImports(artifact.Imports));
}
}
auto byteWriter = std::make_shared<Io::ByteWriter>();
std::shared_ptr<Elfpatcher::IElfPatcher> patcher;
if (args.toWindows) {
patcher = std::make_shared<Elfpatcher::Windows::WindowsPePatcher>(args.windowsGui, std::filesystem::path(args.inputPath).parent_path() / "sce_sys" / "icon0.png");
} else {
patcher = std::make_shared<Elfpatcher::Linux::LinuxElfPatcher>(
std::make_shared<Elfpatcher::EntryStubBuilder>(),
std::make_shared<Elfpatcher::ProgramHeaderLayoutBuilder>(
std::make_shared<Elfpatcher::SegmentFilter>(),
byteWriter
),
std::make_shared<Elfpatcher::SectionHeaderTableBuilder>(byteWriter),
byteWriter
);
}
std::vector<std::uint8_t> executableBytes;
try {
executableBytes = patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, args.runPath, args.lazyBinding, args.windowsDiagnostics, trampolines);
} catch (Domain::RelinkerException& error) {
error.InputPath = args.inputPath;
throw;
}
for (const auto& artifact : guestArtifacts) {
std::filesystem::create_directories(artifact.Path.parent_path());
fileWriter.Write(artifact.Path.string(), artifact.Bytes);
std::cout << "Guest module: " << artifact.Path.string() << '\n';
}
fileWriter.Write(absPath, executableBytes);
std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n';
std::cout << "Expected runtime layout (relative to the output executable):\n"
<< std::filesystem::path(absPath).filename().string() << "\n"
<< "libs/\n *.prx\napp0/\n <game resources>\n";
for (const auto& artifact : guestArtifacts)
std::cout << " " << artifact.Path.lexically_relative(std::filesystem::path(absPath).parent_path() / "app0").generic_string() << '\n';
std::cout << "Game resources and system libraries must be placed in this layout separately.\n";
if (args.runPath != "$ORIGIN/libs") std::cout << "Custom library search path (--rpath): " << args.runPath << '\n';
if (args.autorun) return Cli::Autorun(absPath, args.toWindows);
} catch (const Domain::RelinkerException& e) {
std::cerr << "FAIL: " << e.what();
if (e.FailureOffset != 0) std::cerr << " (offset 0x" << std::hex << e.FailureOffset << ")";
std::cerr << "\n";
if (!e.InputPath.empty()) std::cerr << "Input: " << e.InputPath << '\n';
return 2;
} catch (const Codegen::CodegenException& e) {
std::cerr << "FAIL: " << e.what();
if (e.FailureOffset != 0) std::cerr << " (offset 0x" << std::hex << e.FailureOffset << ")";
std::cerr << "\n";
return 2;
} catch (const std::exception& e) {
std::cerr << "FAIL: " << e.what() << "\n";
return 2;
}
return 0;
}