fix(relinker): load Linux executables at 0x400000 by default

This commit is contained in:
Rookie committed 2026-10-21 19:25:33 +01:00
1 parent 950388bfd2
commit 123ba70839
23 files changed
+191 -80

No files matched your search

@@ -14,6 +14,8 @@
namespace Elfpatcher::Linux {
inline constexpr std::uint64_t DefaultFixedBase = 0x400000;
class LinuxElfPatcher : public IElfPatcher {
public:
LinuxElfPatcher(
+3 -2
View File
@@ -35,6 +35,7 @@ struct ConvertedExecutable {
std::vector<std::uint8_t> Bytes;
std::string Diagnostics;
std::vector<Relinker::GamePatch::AppliedPatch> AppliedPatches;
std::optional<std::uint64_t> LinuxBase;
};
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) {
@@ -73,7 +74,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& 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), std::move(converted.AppliedPatches)};
return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics), std::move(converted.AppliedPatches), converted.LinuxBase};
}
class OutputIdentityDirectory {
@@ -332,7 +333,7 @@ int Run(const int argc, char* argv[]) {
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));
if (!manifestPath.empty()) manifestText = Relinker::GamePatch::WriteManifest(appliedPatches, mainTarget, Io::Utf8Path(patchFile), executables.front().LinuxBase);
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));
@@ -40,6 +40,7 @@ struct PatchEntry {
struct PatchFile {
std::vector<PatchEntry> Entries;
std::optional<std::string> MainTarget;
std::optional<std::uint64_t> LinuxBase;
};
struct PatchedRange {
@@ -114,7 +115,7 @@ void MatchTargets(const std::vector<PatchEntry>& entries, const std::vector<std:
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::string WriteManifest(const std::vector<AppliedPatch>& patches, const std::string& mainTarget, const std::string& configPath, std::optional<std::uint64_t> linuxBase = std::nullopt);
std::vector<VerifyResult> VerifyPatches(const PatchFile& file, const std::vector<VerifyTarget>& executables, const std::vector<VerifyTarget>& modules, bool outputMode);
@@ -46,6 +46,7 @@ struct ConversionOutput {
RelinkResult Analysis;
std::string WindowsDiagnostics;
std::vector<GamePatch::AppliedPatch> AppliedPatches;
std::optional<std::uint64_t> LinuxBase;
};
class Conversion {
@@ -33,8 +33,8 @@ constexpr std::array<IntegerType, 8> kIntegerTypes{{
{"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, 8> kIgnoredTopLevelKeys{"relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"};
constexpr std::array<std::string_view, 9> kReservedNames{"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "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) {
@@ -300,8 +300,8 @@ void RejectEntryOverlaps(const std::vector<PatchEntry>& entries, const std::vect
}
}
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;
bool IsElfOfType(const std::vector<std::uint8_t>& bytes, const std::uint16_t type) {
return bytes.size() >= 18 && bytes[0] == 0x7f && bytes[1] == 'E' && bytes[2] == 'L' && bytes[3] == 'F' && bytes[16] == (type & 0xff) && bytes[17] == (type >> 8);
}
VerifyResult VerifyOne(const PatchEntry& entry, const EntryRange& resolved, const std::vector<std::uint8_t>& bytes, const bool outputMode) {
@@ -410,6 +410,10 @@ PatchFile ParsePatchFile(const std::string_view text) {
if (!mainTarget->IsString || mainTarget->Text.empty()) FailLine(mainTarget->Line, "main_target must be a nonempty string", "main_target");
file.MainTarget = mainTarget->Text;
}
if (const auto* linuxBase = Find(top, "linux_base")) {
if (linuxBase->IsString || linuxBase->Number.Negative) FailLine(linuxBase->Line, "linux_base must be a non-negative integer", "linux_base");
file.LinuxBase = linuxBase->Number.Magnitude;
}
return file;
}
@@ -505,7 +509,7 @@ std::vector<AppliedPatch> AggregateManifest(const std::vector<std::vector<Applie
return merged;
}
std::string WriteManifest(const std::vector<AppliedPatch>& patches, const std::string& mainTarget, const std::string& configPath) {
std::string WriteManifest(const std::vector<AppliedPatch>& patches, const std::string& mainTarget, const std::string& configPath, const std::optional<std::uint64_t> linuxBase) {
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;
@@ -519,6 +523,7 @@ std::string WriteManifest(const std::vector<AppliedPatch>& patches, const std::s
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 (linuxBase) manifest << "linux_base = " << HexValue(*linuxBase) << '\n';
if (!config.empty()) manifest << "config = " << quoted(config, "config") << '\n';
for (const auto& patch : patches) {
manifest << "\n[[patch]]\n";
@@ -553,8 +558,12 @@ std::vector<VerifyResult> VerifyPatches(const PatchFile& file, const std::vector
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");
for (const auto& executable : executables) {
const bool fixed = IsElfOfType(executable.Bytes, Elfpatcher::ET_EXEC);
if (!fixed && !IsElfOfType(executable.Bytes, Elfpatcher::ET_DYN)) throw Domain::RelinkerException(executable.Name + " is not a Linux ELF output (PE and Mach-O outputs are not supported): verify the input instead");
if (fixed && !file.LinuxBase) throw Domain::RelinkerException(executable.Name + " is an ET_EXEC Linux ELF output, but the patch file has no linux_base: verify the manifest written by its conversion");
if (!fixed && file.LinuxBase) throw Domain::RelinkerException(executable.Name + " is an ET_DYN Linux ELF output, but the patch file has linux_base = " + HexValue(*file.LinuxBase) + ": verify the output of the conversion that wrote it");
}
} else {
std::vector<std::string> names;
for (const auto& executable : executables) names.push_back(executable.Name);
@@ -583,13 +592,19 @@ std::vector<VerifyResult> VerifyPatches(const PatchFile& file, const std::vector
}
continue;
}
const std::uint64_t base = outputMode && IsElfOfType(target->Bytes, Elfpatcher::ET_EXEC) ? *file.LinuxBase : 0;
std::vector<PatchEntry> own;
for (const auto other : indices) own.push_back(file.Entries[other]);
for (const auto other : indices) {
own.push_back(file.Entries[other]);
if (own.back().VirtualAddress > std::numeric_limits<std::uint64_t>::max() - base) throw Domain::RelinkerException(Describe(own.back()) + " lies outside the address space at linux_base = " + HexValue(base));
own.back().VirtualAddress += base;
}
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);
results[indices[position]].VirtualAddress = file.Entries[indices[position]].VirtualAddress;
done[indices[position]] = true;
}
}
@@ -125,6 +125,11 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti
auto byteWriter = std::make_shared<Io::ByteWriter>();
std::optional<std::uint64_t> linuxBase;
if (!windows && !macos)
linuxBase = options.LinuxFixedBase || options.LazyBinding ? options.LinuxFixedBase : std::optional<std::uint64_t>(Elfpatcher::Linux::DefaultFixedBase);
if (!windows && !macos && options.LazyBinding)
inputProgress.Message("Warning: with --lazy-binding the Linux executable stays position independent and is not placed at 0x400000; a title whose own sceLibcMspaceCreate checks the 40-bit address window stops at start-up\n");
std::shared_ptr<Elfpatcher::IElfPatcher> patcher;
if (macos) {
patcher = std::make_shared<Elfpatcher::MacOs::MacOsMachOPatcher>(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); });
@@ -139,7 +144,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti
),
std::make_shared<Elfpatcher::SectionHeaderTableBuilder>(byteWriter),
byteWriter,
options.LinuxFixedBase,
linuxBase,
options.TraceImports
);
}
@@ -157,7 +162,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), std::move(appliedPatches)};
return {std::move(executableBytes), std::move(guestArtifacts), std::move(result), std::move(diagnostics), std::move(appliedPatches), linuxBase};
} catch (Domain::RelinkerException& error) {
if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(input.InputPath);
throw;
@@ -132,14 +132,22 @@ void fixedBase() {
const Relinker::Conversion converter(progress);
auto source = input();
source.Modules.push_back({"virtual/title/sce_module/plugin.prx", fixture()});
const auto plain = converter.Convert(source, {});
const auto standard = converter.Convert(source, {});
Relinker::ConversionOptions lazy;
lazy.LazyBinding = true;
const auto independent = converter.Convert(input(), lazy);
Relinker::ConversionOptions options;
options.LinuxFixedBase = 0x400000000;
const auto fixed = converter.Convert(source, options);
require(Io::ReadU16(plain.Executable, 16) == 3 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC");
require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(plain.Executable, 24) + *options.LinuxFixedBase, "Fixed entry was not rebased above 4 GiB");
require(fixed.Modules.front().Bytes == plain.Modules.front().Bytes, "Executable fixed base changed a bundled module");
require(Io::ReadU16(standard.Executable, 16) == 2 && Io::ReadU16(fixed.Executable, 16) == 2, "Fixed base did not select ET_EXEC");
require(Io::ReadU16(independent.Executable, 16) == 3, "Lazy binding did not keep ET_DYN");
require(standard.LinuxBase == 0x400000 && fixed.LinuxBase == *options.LinuxFixedBase && !independent.LinuxBase, "Conversion reported the wrong Linux base");
require(Io::ReadU64(fixed.Executable, 24) == Io::ReadU64(standard.Executable, 24) + *options.LinuxFixedBase - 0x400000, "Fixed entry was not rebased above 4 GiB");
require(fixed.Modules.front().Bytes == standard.Modules.front().Bytes, "Executable fixed base changed a bundled module");
for (const auto target : {Relinker::TargetSystem::Windows, Relinker::TargetSystem::MacOs}) {
Relinker::ConversionOptions other;
other.Target = target;
require(!converter.Convert(source, other).LinuxBase, "A non-Linux conversion reported a Linux base");
options.Target = target;
reject([&] { converter.Convert(source, options); });
}
+11 -5
View File
@@ -3,6 +3,7 @@
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <limits>
#include <span>
#include <stdexcept>
#include <utility>
@@ -117,12 +118,17 @@ public:
const auto phOffset = Read<std::uint64_t>(elf, 32);
const auto phSize = Read<std::uint16_t>(elf, 54);
const auto phCount = Read<std::uint16_t>(elf, 56);
const auto page = static_cast<std::uint64_t>(sysconf(_SC_PAGESIZE));
linkBase = std::numeric_limits<std::uint64_t>::max();
std::uint64_t last = 0;
for (std::uint16_t index = 0; index < phCount; ++index) {
const auto header = phOffset + index * phSize;
if (Read<std::uint32_t>(elf, header) != 1) continue;
size = std::max<std::size_t>(size, Read<std::uint64_t>(elf, header + 16) + Read<std::uint64_t>(elf, header + 40));
linkBase = std::min(linkBase, Read<std::uint64_t>(elf, header + 16) / page * page);
last = std::max(last, Read<std::uint64_t>(elf, header + 16) + Read<std::uint64_t>(elf, header + 40));
}
if (size == 0) throw std::runtime_error("ELF has no PT_LOAD segment");
if (last == 0) throw std::runtime_error("ELF has no PT_LOAD segment");
size = static_cast<std::size_t>(last - linkBase);
base = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (base == MAP_FAILED) throw std::runtime_error("Cannot map executable memory for the ELF image");
for (std::uint16_t index = 0; index < phCount; ++index) {
@@ -131,15 +137,14 @@ public:
const auto offset = Read<std::uint64_t>(elf, header + 8);
const auto fileSize = Read<std::uint64_t>(elf, header + 32);
if (offset > elf.size() || fileSize > elf.size() - offset) throw std::runtime_error("PT_LOAD exceeds the ELF file");
std::memcpy(static_cast<std::uint8_t*>(base) + Read<std::uint64_t>(elf, header + 16), elf.data() + offset, fileSize);
std::memcpy(static_cast<std::uint8_t*>(base) + (Read<std::uint64_t>(elf, header + 16) - linkBase), elf.data() + offset, fileSize);
}
if (mprotect(base, size, PROT_NONE) != 0) throw std::runtime_error("Cannot protect the ELF image");
const auto page = static_cast<std::uint64_t>(sysconf(_SC_PAGESIZE));
for (std::uint16_t index = 0; index < phCount; ++index) {
const auto header = phOffset + index * phSize;
if (Read<std::uint32_t>(elf, header) != 1) continue;
const auto flags = Read<std::uint32_t>(elf, header + 4);
const auto vaddr = Read<std::uint64_t>(elf, header + 16);
const auto vaddr = Read<std::uint64_t>(elf, header + 16) - linkBase;
const auto start = vaddr / page * page;
const auto end = (vaddr + Read<std::uint64_t>(elf, header + 40) + page - 1) / page * page;
const int protection = ((flags & 4) != 0 ? PROT_READ : 0) | ((flags & 2) != 0 ? PROT_WRITE : 0) | ((flags & 1) != 0 ? PROT_EXEC : 0);
@@ -163,6 +168,7 @@ public:
private:
void* base = nullptr;
std::size_t size = 0;
std::uint64_t linkBase = 0;
};
} // namespace RelinkerTests
@@ -385,7 +385,20 @@ void verify() {
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", exec}}, {}, true); }, "has no linux_base");
const auto based = ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\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"));
require(based.LinuxBase == 0x400000, "linux_base was not read");
auto rebased = patchedBytes;
Io::WriteU16(rebased, 16, 2);
for (const std::size_t field : {64 + 16, 64 + 24, 120 + 16, 120 + 24}) Io::WriteU64(rebased, field, Io::ReadU64(rebased, field) + 0x400000);
output = VerifyPatches(based, {{"main.elf", rebased}}, {}, true);
require(output.size() == 2 && output[0].Status == VerifyStatus::Applied && output[1].Status == VerifyStatus::Applied, "fixed-base output applied");
require(output[0].VirtualAddress == 0x1000 && output[0].Offset == 0x1000 && output[1].Offset == 0x2000, "fixed-base output reported the rebased address");
output = VerifyPatches(based, {{"main.elf", exec}}, {}, true);
require(output[0].Status == VerifyStatus::Mismatch && output[0].Detail.find("PT_LOAD") != std::string::npos, "linux_base was not applied");
rejects([&] { VerifyPatches(based, outputs, {}, true); }, "has linux_base = 0x400000");
rejects([&] { VerifyPatches(ParsePatchFile(header("main_target = \"main.elf\"\nlinux_base = 0x400000\n[[patch]]\ntarget = \"main.elf\"\nva = 0xffffffffffffff00\nexpected = \"00\"\nvalue = \"01\"\n")), {{"main.elf", rebased}}, {}, true); }, "lies outside the address space at linux_base");
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");
@@ -424,10 +437,15 @@ void manifest() {
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");
require(text.find("linux_base") == std::string::npos && !parsed.LinuxBase, "a manifest without a base wrote linux_base");
const auto based = WriteManifest({plain}, "main.elf", "", 0x400000);
require(based.find("main_target = \"main.elf\"\nlinux_base = 0x400000\n") != std::string::npos && ParsePatchFile(based).LinuxBase == 0x400000, "linux_base did not round-trip");
rejectsText(header("linux_base = \"0x400000\"\n"), 2, "linux_base must be a non-negative integer");
rejectsText(header("linux_base = -1\n"), 2, "linux_base must be a non-negative integer");
}
void reservedNames() {
for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "settings", "settings_sha256"})
for (const char* reserved : {"format", "relinker_version", "relinker_commit", "config", "config_sha256", "main_target", "linux_base", "settings", "settings_sha256"})
rejects([&] { ParsePatchFile(namedEntry(reserved)); }, "line 3: name is reserved");
ParsePatchFile(header("settings = \"a\"\nsettings_sha256 = \"b\"\n"));
}
@@ -20,6 +20,7 @@ PT_TLS = 7
DTPMOD64, DTPOFF64, TPOFF64, TPOFF32 = 16, 17, 18, 23
TLS_MEMSZ, TLS_ALIGN = 0x24, 16
TLS_BLOCK = 0x30
DEFAULT_BASE = 0x400000
STRINGS = b"\0Tls\0Data\0ImportedTls\0"
TLS_NAME, DATA_NAME, IMPORT_NAME = 1, 5, 10
@@ -107,8 +108,9 @@ def main():
elf = output.read_bytes()
relocations = dynamic_relocations(elf)
tls = sorted(relocation for relocation in relocations if relocation[1] in (DTPMOD64, DTPOFF64, TPOFF64))
assert tls == [(0x1000, TPOFF64, "", 0, 4), (0x1008, DTPOFF64, "", 0, 8), (0x1010, TPOFF64, "", 0, 0x12),
(0x1018, DTPMOD64, "", 0, 0), (0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations
assert tls == [(DEFAULT_BASE + 0x1000, TPOFF64, "", 0, 4), (DEFAULT_BASE + 0x1008, DTPOFF64, "", 0, 8),
(DEFAULT_BASE + 0x1010, TPOFF64, "", 0, 0x12), (DEFAULT_BASE + 0x1018, DTPMOD64, "", 0, 0),
(DEFAULT_BASE + 0x1020, TPOFF64, "ImportedTls", 6, 0)], relocations
assert any(struct.unpack_from("<I", elf, 64 + index * 56)[0] == PT_TLS
for index in range(struct.unpack_from("<H", elf, 0x38)[0])), "PT_TLS must be kept"
@@ -66,6 +66,7 @@ def main():
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 'main_target = "eboot.bin"\nlinux_base = 0x400000\n' in text, text
assert f"Patch manifest: {manifest}\n" in result.stdout
assert f'config = "{source.parent.as_posix()}/relinker-patches.toml"\n' in text, text
@@ -10,6 +10,8 @@ PT_GNU_EH_FRAME = 0x6474E550
PT_GNU_STACK = 0x6474E551
PT_SCE_VERSION = 0x6FFFFF01
EH_FRAME_HEADER = (PT_GNU_EH_FRAME, 4, 0x4800, 0x800, 0x800, 16, 16, 4)
DEFAULT_BASE = 0x400000
EH_FRAME_OUTPUT = (*EH_FRAME_HEADER[:3], 0x800 + DEFAULT_BASE, 0x800 + DEFAULT_BASE, *EH_FRAME_HEADER[5:])
def fixture(header_count, with_frame=True):
@@ -93,7 +95,7 @@ def main():
assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr
headers = program_headers(elf)
frames = [header for header in headers if header[0] == PT_GNU_EH_FRAME]
assert frames == [EH_FRAME_HEADER], ("PT_GNU_EH_FRAME must be kept unchanged", headers)
assert frames == [EH_FRAME_OUTPUT], ("PT_GNU_EH_FRAME must only move by the base", headers)
offset, size = EH_FRAME_HEADER[2], EH_FRAME_HEADER[5]
assert elf[offset:offset + size] == image[offset:offset + size], "eh_frame_hdr bytes changed"
assert all(header[0] != PT_SCE_VERSION for header in headers), headers
@@ -103,7 +105,7 @@ def main():
image[table_end:table_end + 56] = bytes([0xa5]) * 56
result, elf = relink(relinker, directory, "full-slots", image)
headers = program_headers(elf)
assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == [EH_FRAME_HEADER], headers
assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == [EH_FRAME_OUTPUT], headers
assert len(headers) == 8, headers
assert struct.unpack_from("<Q", elf, 0x20)[0] >= len(image), "The grown program header table must be moved past the input"
assert "PT_GNU_EH_FRAME" not in result.stderr, result.stderr
@@ -125,7 +127,7 @@ def main():
result, elf = relink(relinker, directory, "few-slots-" + str(with_frame), image)
headers = program_headers(elf)
assert len(headers) == (8 if with_frame else 7), headers
assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_HEADER] if with_frame else []), headers
assert [header for header in headers if header[0] == PT_GNU_EH_FRAME] == ([EH_FRAME_OUTPUT] if with_frame else []), headers
assert struct.unpack_from("<Q", elf, 0x20)[0] >= len(image), "The grown program header table must be moved past the input"
assert elf[64 + 5 * 56:64 + 6 * 56] == image[64 + 5 * 56:64 + 6 * 56], "Bytes after the input program header table changed"
@@ -12,6 +12,7 @@ from signal_run import run_without_core_dump
from test_linux_load_alignment import PT_LOAD, PT_SCE_VERSION, fixture, loads
ENTRY = 0x10
DEFAULT_BASE = 0x400000
ARGV_CHECK = bytes.fromhex(
"833f02" "751e"
"48837f0800" "7417"
@@ -79,7 +80,7 @@ def main():
capture_output=True, text=True, timeout=20)
if error is None:
assert result.returncode == 0, (address, result.stdout, result.stderr)
assert entry_call_target(far.read_bytes()) == ENTRY, hex(address)
assert entry_call_target(far.read_bytes()) == DEFAULT_BASE + ENTRY, hex(address)
else:
assert result.returncode == 2 and error in result.stderr and not far.exists(), (address, result)
print("Linux entry argv test passed")
@@ -1,4 +1,4 @@
"""Check that --linux-fixed-base places the executable at the requested address and rebases every address it holds."""
"""Check that the Linux executable is placed at 0x400000 by default or at the --linux-fixed-base address, with every address it holds rebased, that --lazy-binding keeps it position independent and warns, and that the largest image the relinker accepts ends below the guest arena."""
from pathlib import Path
import platform
@@ -16,9 +16,13 @@ PT_LOAD = 1
PT_DYNAMIC = 2
PT_GNU_STACK = 0x6474E551
BASE = 0x400000
HIGH_BASE = 0x400000000
SHIFT = HIGH_BASE - BASE
ENTRY = 0x10
SLOT = 0x800
ADDRESS_TAGS = {3, 4, 5, 6, 7, 23}
GUEST_ARENA_START = 0x200000000
LAZY_WARNING = "with --lazy-binding the Linux executable stays position independent"
def check_code(base):
@@ -86,58 +90,83 @@ def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-fixed-base-") as directory:
work = Path(directory)
result, plain_path = relink(relinker, work, "plain", fixture())
result, fixed_path = relink(relinker, work, "default", fixture())
assert result.returncode == 0, (result.stdout, result.stderr)
result, fixed_path = relink(relinker, work, "fixed", fixture(), "--linux-fixed-base", hex(BASE))
assert LAZY_WARNING not in result.stdout + result.stderr, (result.stdout, result.stderr)
result, explicit_path = relink(relinker, work, "explicit", fixture(), "--linux-fixed-base", hex(BASE))
assert result.returncode == 0, (result.stdout, result.stderr)
plain = plain_path.read_bytes()
result, high_path = relink(relinker, work, "high", fixture(), "--linux-fixed-base", hex(HIGH_BASE))
assert result.returncode == 0, (result.stdout, result.stderr)
result, lazy_path = relink(relinker, work, "lazy", fixture(), "--lazy-binding")
assert result.returncode == 0, (result.stdout, result.stderr)
assert LAZY_WARNING in result.stdout + result.stderr, (result.stdout, result.stderr)
fixed = fixed_path.read_bytes()
high = high_path.read_bytes()
lazy = lazy_path.read_bytes()
assert struct.unpack_from("<H", plain, 16)[0] == 3, "a relink without the option is no longer ET_DYN"
assert struct.unpack_from("<H", fixed, 16)[0] == 2, "a fixed-base relink is not ET_EXEC"
assert struct.unpack_from("<Q", fixed, 24)[0] == struct.unpack_from("<Q", plain, 24)[0] + BASE, "entry not rebased"
assert fixed == explicit_path.read_bytes(), "a relink without the option is not placed at 0x400000"
assert struct.unpack_from("<H", fixed, 16)[0] == 2, "a default relink is not ET_EXEC"
assert struct.unpack_from("<H", high, 16)[0] == 2, "a fixed-base relink is not ET_EXEC"
assert struct.unpack_from("<H", lazy, 16)[0] == 3, "a lazy-binding relink is no longer ET_DYN"
assert relocations(lazy) == [(SLOT, 8, ENTRY)], relocations(lazy)
assert struct.unpack_from("<Q", high, 24)[0] == struct.unpack_from("<Q", fixed, 24)[0] + SHIFT, "entry not rebased"
plain_headers = headers(plain)
fixed_headers = headers(fixed)
assert len(plain_headers) == len(fixed_headers)
for before, after in zip(plain_headers, fixed_headers):
shift = 0 if before[0] in (PT_NULL, PT_GNU_STACK) else BASE
high_headers = headers(high)
assert len(fixed_headers) == len(high_headers)
for before, after in zip(fixed_headers, high_headers):
shift = 0 if before[0] in (PT_NULL, PT_GNU_STACK) else SHIFT
assert after == (*before[:3], before[3] + shift, before[4] + shift, *before[5:]), (before, after)
assert min(header[3] for header in fixed_headers if header[0] == PT_LOAD) == BASE, "image does not start at the base"
assert min(header[3] for header in high_headers if header[0] == PT_LOAD) == HIGH_BASE, "image does not start at the base"
plain_dynamic = dynamic_tags(plain)
fixed_dynamic = dynamic_tags(fixed)
assert len(plain_dynamic) == len(fixed_dynamic), (len(plain_dynamic), len(fixed_dynamic))
for (tag, value), (fixed_tag, fixed_value) in zip(plain_dynamic, fixed_dynamic):
assert tag == fixed_tag and fixed_value == value + (BASE if tag in ADDRESS_TAGS else 0), (tag, value, fixed_value)
high_dynamic = dynamic_tags(high)
assert len(fixed_dynamic) == len(high_dynamic), (len(fixed_dynamic), len(high_dynamic))
for (tag, value), (high_tag, high_value) in zip(fixed_dynamic, high_dynamic):
assert tag == high_tag and high_value == value + (SHIFT if tag in ADDRESS_TAGS else 0), (tag, value, high_value)
plain_relocations = relocations(plain)
assert plain_relocations == [(SLOT, 8, ENTRY)], plain_relocations
assert relocations(fixed) == [(SLOT + BASE, 8, ENTRY + BASE)], relocations(fixed)
assert relocations(high) == [(SLOT + HIGH_BASE, 8, ENTRY + HIGH_BASE)], relocations(high)
relocated = fixture(slots=5)
result, relocated_plain_path = relink(relinker, work, "relocated-plain", relocated)
result, relocated_fixed_path = relink(relinker, work, "relocated-default", relocated)
assert result.returncode == 0, (result.stdout, result.stderr)
result, relocated_fixed_path = relink(relinker, work, "relocated-fixed", relocated, "--linux-fixed-base", hex(BASE))
result, relocated_high_path = relink(relinker, work, "relocated-high", relocated, "--linux-fixed-base", hex(HIGH_BASE))
assert result.returncode == 0, (result.stdout, result.stderr)
relocated_plain = relocated_plain_path.read_bytes()
relocated_fixed = relocated_fixed_path.read_bytes()
relocated_high = relocated_high_path.read_bytes()
assert struct.unpack_from("<H", relocated_fixed, 0x38)[0] > struct.unpack_from("<H", relocated, 0x38)[0], "the output program header table did not grow past the input's slots"
assert struct.unpack_from("<Q", relocated_fixed, 0x20)[0] != 0x40, "the input table had slots for the output layout"
relocated_plain_headers = headers(relocated_plain)
relocated_fixed_headers = headers(relocated_fixed)
assert len(relocated_plain_headers) == len(relocated_fixed_headers)
for before, after in zip(relocated_plain_headers, relocated_fixed_headers):
shift = 0 if before[0] in (PT_NULL, PT_GNU_STACK) else BASE
relocated_high_headers = headers(relocated_high)
assert len(relocated_fixed_headers) == len(relocated_high_headers)
for before, after in zip(relocated_fixed_headers, relocated_high_headers):
shift = 0 if before[0] in (PT_NULL, PT_GNU_STACK) else SHIFT
assert after == (*before[:3], before[3] + shift, before[4] + shift, *before[5:]), (before, after)
assert min(header[3] for header in relocated_fixed_headers if header[0] == PT_LOAD) == BASE, "the relocated image does not start at the base"
relocated_entry = struct.unpack_from("<Q", relocated_fixed, 24)[0]
assert relocated_entry == struct.unpack_from("<Q", relocated_plain, 24)[0] + BASE, "entry not rebased"
assert struct.unpack_from("<Q", relocated_high, 24)[0] == relocated_entry + SHIFT, "entry not rebased"
entry_offset = mapped(relocated_fixed, relocated_entry)
assert entry_offset < len(relocated_fixed), ("the entry's mapping lies outside the output file", entry_offset)
covering = next(header for header in relocated_fixed_headers if header[0] == PT_LOAD and header[3] <= relocated_entry < header[3] + header[5])
assert covering[1] & 1, "the segment covering the entry is not executable"
large_path = None
for far, accepted in ((0x7F000000, True), (0x80000000, False), (GUEST_ARENA_START - BASE - 0x1000, False)):
spanning = fixture()
struct.pack_into("<IIQQQQQQ", spanning, 64 + 2 * 56, PT_LOAD, 6, 0x5000, far, far, 0, 0x2000, 0x1000)
for options in ((), ("--lazy-binding",)):
result, output = relink(relinker, work, f"span-{far:x}{''.join(options)}", spanning, *options)
if not accepted:
assert result.returncode == 2 and "Entry stub call exceeds rel32 range" in result.stderr and not output.exists(), (hex(far), options, result.returncode, result.stderr)
continue
assert result.returncode == 0, (hex(far), options, result.stdout, result.stderr)
if not options:
large_path = output
end = max(header[3] + header[6] for header in headers(output.read_bytes()) if header[0] == PT_LOAD)
assert BASE + far < end < GUEST_ARENA_START, ("the largest accepted image does not end below the guest arena", hex(end))
filled = bytearray(relocated)
filled[0x158:0x198] = b"\x41" * 0x40
result, _ = relink(relinker, work, "relocated-filled", bytes(filled), "--linux-fixed-base", hex(BASE))
@@ -150,6 +179,9 @@ def main():
relocated_fixed_path.chmod(0o755)
executed = run_without_core_dump([str(relocated_fixed_path)], capture_output=True, timeout=20)
assert executed.returncode == -signal.SIGTRAP, ("the relocated-table image did not run at the base with its relocation applied", executed.returncode)
large_path.chmod(0o755)
executed = run_without_core_dump([str(large_path)], capture_output=True, timeout=20)
assert executed.returncode == -signal.SIGTRAP, ("the image spanning 2 GiB did not run at the base", executed.returncode)
for name, image, base, error in (
("unaligned", fixture(alignment=0x20000), 0x410000, "is not a multiple of the PT_LOAD alignment 0x20000"),
@@ -13,7 +13,8 @@ from test_linux_load_alignment import fixture
PT_GNU_STACK = 0x6474E551
PT_GNU_RELRO = 0x6474E552
PT_SCE_VERSION = 0x6FFFFF01
BASE = 0x400000
DEFAULT_BASE = 0x400000
BASE = 0x800000
def image():
@@ -48,8 +49,8 @@ def main():
work = Path(directory)
source = work / "input.elf"
source.write_bytes(image())
for options, base, keeps_relro in (([], 0, True), (["--linux-fixed-base", hex(BASE)], BASE, True), (["--lazy-binding"], 0, False)):
output = work / ("fixed.elf" if base else "plain.elf")
for name, options, base, keeps_relro in (("default", [], DEFAULT_BASE, True), ("fixed", ["--linux-fixed-base", hex(BASE)], BASE, True), ("lazy", ["--lazy-binding"], 0, False)):
output = work / (name + ".elf")
result = subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)],
capture_output=True, text=True, timeout=20)
assert result.returncode == 0, (result.stdout, result.stderr)
@@ -72,7 +73,7 @@ def main():
assert result.returncode == 0, (result.stdout, result.stderr)
found = {header[0]: header for header in headers(output.read_bytes())}
assert found[PT_GNU_STACK] == (PT_GNU_STACK, 6, 0, 0, 0, 0, 0, 16), found
assert found[PT_GNU_RELRO][3:5] == (0x500, 0x500), found
assert found[PT_GNU_RELRO][3:5] == (0x500 + DEFAULT_BASE, 0x500 + DEFAULT_BASE), found
if sys.platform.startswith("linux"):
for options, mode in (([], "default"), (["--linux-fixed-base", hex(BASE)], "fixed")):
@@ -97,7 +98,7 @@ def main():
assert result.returncode == 0, (result.stdout, result.stderr)
found = {header[0]: header for header in headers(output.read_bytes())}
assert found[PT_GNU_STACK] == (PT_GNU_STACK, 6, 0, 0, 0, 0, 0, 16), found
assert found[PT_GNU_RELRO][3:5] == (0x4000, 0x4000), found
assert found[PT_GNU_RELRO][3:5] == (0x4000 + DEFAULT_BASE, 0x4000 + DEFAULT_BASE), found
output.chmod(output.stat().st_mode | 0o111)
execution = run_without_core_dump([str(output)], capture_output=True, text=True, timeout=20)
assert execution.returncode == -signal.SIGTRAP, (execution.returncode, execution.stdout, execution.stderr)
@@ -21,6 +21,7 @@ PT_OS_PROCPARAM = 0x61000001
PT_GNU_STACK = 0x6474E551
PT_SCE_VERSION = 0x6FFFFF01
PAGE = 0x1000
DEFAULT_BASE = 0x400000
TEXT = (PT_LOAD, 5, 0x4000, 0x0000, 0x1000, 0x1000, 0x1000)
RODATA = (PT_LOAD, 4, 0x5000, 0x1000, 0x0800, 0x0800, 0x1000)
@@ -102,13 +103,13 @@ def main():
ordered = convert("ordered-loads", multi_load_fixture([TEXT, RODATA, PROCPARAM, DATA]))
headers = check_order("ordered-loads", ordered.read_bytes())
assert (PT_OS_PROCPARAM, PROCPARAM[3]) in [(h[0], h[3]) for h in headers], headers
assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [TEXT[3], RODATA[3], DATA[3]], headers
assert (PT_OS_PROCPARAM, DEFAULT_BASE + PROCPARAM[3]) in [(h[0], h[3]) for h in headers], headers
assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [DEFAULT_BASE + h[3] for h in (TEXT, RODATA, DATA)], headers
run_expecting_trap("ordered-loads", ordered)
unordered = convert("unordered-loads", multi_load_fixture([DATA, PROCPARAM, TEXT, RODATA]))
headers = check_order("unordered-loads", unordered.read_bytes())
assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [TEXT[3], RODATA[3], DATA[3]], headers
assert [h[3] for h in headers if h[0] == PT_LOAD][:3] == [DEFAULT_BASE + h[3] for h in (TEXT, RODATA, DATA)], headers
assert kinds_between_loads(headers) == [PT_OS_PROCPARAM, PT_GNU_STACK], headers
run_expecting_trap("unordered-loads", unordered)
@@ -10,6 +10,13 @@ from test_dynamic_segment import fixture
from test_linux_dynamic_strings import dynamic_strings, file_offset
from test_linux_load_alignment import loads
DEFAULT_BASE = 0x400000
def rebased(relocation):
offset, info, addend = relocation
return (offset + DEFAULT_BASE, info, addend + DEFAULT_BASE if info & 0xffffffff == 8 else addend)
def image_with_relocations(relocations, sce, imported):
image = fixture()
@@ -75,7 +82,7 @@ def main():
if tags.get(8, 0):
start = file_offset(data, loads(data), tags[7])
actual = [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[8], 24)]
assert actual == [relocation for relocation in relocations if relocation in (imported, relative)]
assert actual == [rebased(relocation) for relocation in relocations if relocation in (imported, relative)]
if target == "windows" and os.name == "nt" and not has_import:
run = subprocess.run([str(output)], capture_output=True, timeout=20)
assert run.returncode == 42, (name, level, run.returncode, run.stderr)
@@ -96,7 +103,7 @@ def main():
if target == "linux":
tags, _ = dynamic_strings(data)
start = file_offset(data, loads(data), tags[7])
assert [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[8], 24)] == [relative]
assert [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[8], 24)] == [rebased(relative)]
if target == "windows" and os.name == "nt":
run = subprocess.run([str(output)], capture_output=True, timeout=20)
assert run.returncode == 42, (sce, level, run.returncode, run.stderr)
@@ -14,6 +14,7 @@ from test_linux_load_alignment import loads
GLOB_DAT_SLOT = 0x308
ABSOLUTE_SLOT = 0x310
ADDEND = 0x1234
DEFAULT_BASE = 0x400000
CHECK_SLOTS = bytes.fromhex(
"488b0501010000" "4885c0" "751a"
"488b0dfd000000" "4881f934120000" "750a"
@@ -68,8 +69,8 @@ def main():
registry = json.loads(output.with_suffix(".registry.json").read_text(encoding="utf-8"))
assert registry == [], (index, target, level, registry)
if target == "linux":
assert linux_imports(data) == [(GLOB_DAT_SLOT, 6, 0, b"", 0x20, 0, 0, 0),
(ABSOLUTE_SLOT, 1, ADDEND, b"", 0x20, 0, 0, 0)], (index, level)
assert linux_imports(data) == [(DEFAULT_BASE + GLOB_DAT_SLOT, 6, 0, b"", 0x20, 0, 0, 0),
(DEFAULT_BASE + ABSOLUTE_SLOT, 1, ADDEND, b"", 0x20, 0, 0, 0)], (index, level)
if target == "windows" and os.name == "nt":
run = subprocess.run([str(output)], capture_output=True, timeout=20)
assert run.returncode == 42, (index, level, run.returncode, run.stderr)
@@ -15,6 +15,7 @@ from test_linux_load_alignment import loads
from test_windows_import_modules import provider
DEFAULT_BASE = 0x400000
SCE_TAGS = {2: 0x6100002D, 3: 0x61000027, 5: 0x61000035, 6: 0x61000039,
7: 0x6100002F, 8: 0x61000031, 9: 0x61000033, 10: 0x61000037,
11: 0x6100003B, 20: 0x6100002B, 23: 0x61000029}
@@ -102,8 +103,8 @@ def main():
if relative or plt:
offset = file_offset(data, loads(data), dynamic[23 if plt else 7])
destination, info, addend = struct.unpack_from("<QQq", data, offset)
assert destination == 0x300 and info & 0xFFFFFFFF == (7 if plt else 8), case.name
assert addend == (0 if plt else 0x210), case.name
assert destination == 0x300 + DEFAULT_BASE and info & 0xFFFFFFFF == (7 if plt else 8), case.name
assert addend == (0 if plt else 0x210 + DEFAULT_BASE), case.name
if target == "windows" and os.name == "nt":
run = subprocess.run([str(output)], capture_output=True, timeout=20)
assert run.returncode == 42, (case.name, run.returncode, run.stderr)
@@ -16,6 +16,7 @@ from test_linux_load_alignment import loads
from test_macos_output import MachO
DEFAULT_BASE = 0x400000
SCE_TAGS = {2: 0x6100002D, 3: 0x61000027, 5: 0x61000035, 6: 0x61000039,
7: 0x6100002F, 8: 0x61000031, 9: 0x61000033, 10: 0x61000037,
11: 0x6100003B, 20: 0x6100002B, 23: 0x61000029}
@@ -73,6 +74,7 @@ def fixture(layout, sce, guest, misaligned=False, conflicting=False):
def check_output(data, target, guest, local, plt):
if target == "linux":
base = 0 if guest else DEFAULT_BASE
assert data.startswith(b"\x7fELF")
assert all(load[2] % 0x1000 == load[3] % 0x1000 for load in loads(data))
tags = dynamic_tags(data)
@@ -83,7 +85,8 @@ def check_output(data, target, guest, local, plt):
actual = [struct.unpack_from("<QQq", data, offset)
for offset in range(start, start + tags[size], 24)]
assert [(offset, info & 0xFFFFFFFF, addend) for offset, info, addend in actual] == [
(offset, info & 0xFFFFFFFF, addend) for offset, info, addend in expected], (actual, expected)
(offset + base, info & 0xFFFFFFFF, addend + base if info & 0xFFFFFFFF == 8 else addend)
for offset, info, addend in expected], (actual, expected)
if address == 23:
assert all(info >> 32 for _, info, _ in actual)
return
@@ -12,6 +12,7 @@ from test_linux_load_alignment import loads
PT_LOAD = 1
PT_DYNAMIC = 2
PT_SCE_RELRO = 0x61000010
DEFAULT_BASE = 0x400000
PT_SCE_VERSION = 0x6FFFFF01
@@ -77,11 +78,11 @@ def main():
assert result.returncode == 0, (name, target, result.stdout, result.stderr)
if target == "linux":
data = output.read_bytes()
relro = [load for load in loads(data) if load[3] <= 0x1000 < load[3] + load[6]]
assert len(relro) == 1 and relro[0][1] == 6 and relro[0][3] == 0x1000 and relro[0][5:7] == (0x1000, 0x1000), (name, relro)
relro = [load for load in loads(data) if load[3] <= DEFAULT_BASE + 0x1000 < load[3] + load[6]]
assert len(relro) == 1 and relro[0][1] == 6 and relro[0][3] == DEFAULT_BASE + 0x1000 and relro[0][5:7] == (0x1000, 0x1000), (name, relro)
tags, _ = dynamic_strings(data)
start = file_offset(data, loads(data), tags[7])
assert [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[8], 24)] == [(0x1000, 8, 0x210)], name
assert [struct.unpack_from("<QQq", data, offset) for offset in range(start, start + tags[8], 24)] == [(DEFAULT_BASE + 0x1000, 8, DEFAULT_BASE + 0x210)], name
if target == "windows" and os.name == "nt":
run = subprocess.run([str(output)], capture_output=True, timeout=20)
assert run.returncode == 42, (name, run.returncode, run.stderr)
+1
View File
@@ -11,6 +11,7 @@
- Converted Windows main executables [omit `DYNAMIC_BASE`](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) by default, not only with `--windows-fixed-base`: on the console the executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Relocations are kept, so if `0x140000000` is taken Windows relocates the image, possibly above that window. Verified only on Windows 10.
- [SDK memory-management syscall traps](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) under `--trap-sdk-syscall-stubs` recognize three exact instruction sequences from [ps5-payload-dev SDK libc/mman.c](https://github.com/ps5-payload-dev/sdk/blob/12cf3dda40/libc/mman.c), observed in Nuvio Maxd 0.5.15 (PPSA99288): syscall 74 and the two syscall 477 paths, with their argument preparation and carry-flag error handling. Matching requires a decoded instruction boundary and the complete sequence inside the scanned code range. Other SDK/compiler output remains rejected. The matched syscall becomes `UD2`; this does not implement mmap or mprotect and a call still faults.
- The [instruction decoder](../../../core/relinker/codegen/src/x86/X64InstructionDecoder.cpp) reads a 16-bit displacement for `66 E8`, `66 E9` and `66 0F 80`-`8F` without REX.W, as AMD processors, the console included, do (binutils `objdump -M amd64`; REX.W selects the 32-bit form). AMD also truncates the target of every near relative branch with that prefix, rel8, LOOP and JRCXZ forms included, to 16 bits. Instruction metadata marks these branches as truncated and reports no branch target for them, and stubs refuse to move them. Compilers do not emit these forms.
- Converted Linux main executables are [placed at `0x400000`](../../../core/relinker/relinker/src/pipeline/Conversion.cpp) by default, as `ET_EXEC` without ASLR, not only with `--linux-fixed-base`: the console executable lies inside the 40-bit window that the title's own `sceLibcMspaceCreate` checks (#3043). Where the console loads an executable that does not require an address is not measured; `0x400000` is the base that executables reading their GOT through `app0/imports.txt` assume (#4013). The relinker refuses an image that spans 2 GiB or more, so the executable ends below the guest arena at `0x200000000`. With `--lazy-binding` the executable stays position independent.
- `--to-intel` lowers MCOMMIT to STC plus NOP padding and RDPRU through a runtime-checked stub in the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp): AMD APM Vol. 3 defines RDPRU ECX 0 as MPERF and ECX 1 as APERF, neither readable on Intel hosts, so both selectors return the host TSC as a disclosed monotonic-counter approximation (their ratio always reads 1.0) while other selectors yield zero EDX:EAX. STC asserts CF=1 because under x86-TSO prior stores are already ordered and guest memory is host-DRAM-backed, leaving the commit-success case as the only observable one. Non-zero RDPRU selectors, the TSC-for-MPERF/APERF substitution, and any console path reporting MCOMMIT CF=0 are unverified.
- [Unscoped imports on Windows](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) - an import without a module that no searched library exports under its name is searched again under the NID of that name, because homebrew built with the [ps5-payload-dev SDK](https://github.com/ps5-payload-dev/sdk) imports by symbol name (Quake, from the websrv homebrew launcher; Mari0, #6239) while the libraries in `libs/` export NIDs. A dependency named with `.sprx`, `.suprx` or `.debug_prx` loads the `.prx` library of the same stem from `libs/`, and `libkernel_web` also loads `libkernel.prx`, as `libSceLibcInternal` already loads `libc.prx`: AnyPS5's `libkernel_web.prx` exports only two functions, where the console's `libkernel_web.sprx` is a libkernel variant. A name that matches a library's export as written still binds to that export first. Linux and macOS resolve these imports through the host loader by name and are unchanged.
- The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length.
+7 -7
View File
@@ -64,7 +64,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath`
| `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. |
| `--windows-fixed-base <address>` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. |
| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows and Linux output only; conflicts with `--lazy-binding`. |
| `--linux-fixed-base <address>` | Load the Linux executable at this fixed address, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. |
| `--linux-fixed-base <address>` | Load the Linux executable at this fixed address instead of the default `0x400000`, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR, as it is at the default address. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. |
| `--macos-fixed-base <address>` | Map the lowest ELF load page of the macOS executable at this fixed address, given in decimal or hexadecimal (`0x`). Requires `--macos`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and an image that ends in the macOS user address space. `__PAGEZERO` ends 64 KiB below the address and the Mach-O header occupies those 64 KiB. The executable has no ASLR; an address the process cannot map fails to load. Bundled modules keep their normal placement. |
| `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. |
| `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, RDSEED becomes RDRAND, CLWB a NOP, and RDPID and the ECX of RDTSCP return processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. |
@@ -85,7 +85,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath`
| `--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. |
| `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs and the recognized inline mmap/mprotect sequences from SDK libc/mman.c with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. |
| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. |
| `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. The Linux executable stays position independent (`ET_DYN`) instead of being placed at `0x400000`. |
Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. Use `relinker --help` to print the usage syntax to stdout and exit with code `0` without converting files. Invoking `relinker` without arguments prints the usage syntax to stderr and exits with code `1`.
@@ -120,11 +120,11 @@ 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.
`format = 1` is required. The top-level keys `relinker_version`, `relinker_commit`, `config`, `config_sha256`, `main_target`, `linux_base`, `settings` and `settings_sha256` are accepted in any file; only `main_target` and `linux_base` are 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. |
| `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 eight 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. |
@@ -137,11 +137,11 @@ x86 `mov r/m64, imm32` and `add`/`sub`/`cmp r/m64, imm32` sign-extend their imme
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.
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`, `linux_base` (the address a Linux `ET_EXEC` output is placed at) 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`.
With `--verify-output`, `<path>` is a converted Linux ELF; PE and Mach-O outputs are refused. The file must have `main_target`, so verify the generated manifest, not a hand-written file. Entries are checked at their `va` plus the manifest's `linux_base` in an `ET_EXEC` output, which requires `linux_base`; an `ET_DYN` output, from `--lazy-binding`, requires a manifest without it. 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.
@@ -149,7 +149,7 @@ Verify mode rejects the conversion options (`--windows`, `--macos`, `--to-intel`
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.
For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --linux-fixed-base 0x400000 source/input.elf app.elf` on Linux, or `relinker --macos --macos-fixed-base 0x400000 source/input.elf app` on macOS. Use these options only when the executable requires a fixed address. Without them, Linux output is a position-independent executable as before, and macOS output places the lowest ELF load page at `0x140010000`.
For an executable that assumes its image starts at `0x400000`, as Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) do when they read their `app0/imports.txt`, use `relinker --macos --macos-fixed-base 0x400000 source/input.elf app` on macOS. Use this option only when the executable requires a fixed address; without it, macOS output places the lowest ELF load page at `0x140010000`. Linux output places virtual address 0 at `0x400000` without an option, inside the 40-bit window that a title's own `sceLibcMspaceCreate` checks; `--linux-fixed-base` selects another address, and with `--lazy-binding` the executable stays position independent.
The same base is applied to executables selected with `--executable`; bundled PRX modules are not rebased.