feat(relinker): support fixed Windows guest image bases

This commit is contained in:
Kovalev Vladislav 2026-10-12 05:54:06 +06:00 committed by Adrià Franch
parent b1328fc0f9
commit 319fa5779a
16 changed files with 224 additions and 18 deletions

View File

@ -377,6 +377,9 @@ if(BUILD_TESTING)
if(Python3_Interpreter_FOUND)
add_test(NAME cli_help COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/test_help.py $<TARGET_FILE:relinker>)
add_test(NAME windows_fixed_base COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_fixed_base.py $<TARGET_FILE:relinker>)
set_tests_properties(windows_fixed_base PROPERTIES TIMEOUT 20)
add_test(NAME elf_offsets COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_offsets.py $<TARGET_FILE:relinker>)
add_test(NAME dynamic_segment COMMAND ${Python3_EXECUTABLE}

View File

@ -5,6 +5,7 @@
#include <string>
#include <vector>
#include <cstdint>
#include <optional>
namespace Cli {
@ -22,6 +23,7 @@ struct Args {
bool autorun = false;
bool windowsDiagnostics = false;
bool windowsGui = false;
std::optional<std::uint64_t> windowsFixedBase;
std::uint32_t unusedFilterLevel = 0;
std::string inputPath;
std::string outputPath;

View File

@ -1,14 +1,16 @@
#include <Cli.hpp>
#include <io/NativePath.hpp>
#include <filesystem>
#include <charconv>
#include <iostream>
#include <stdexcept>
#include <string>
#include <string_view>
namespace Cli {
const char* Usage() {
return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--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] [--windows-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"
"Example: relinker input.elf output.elf";
}
@ -21,6 +23,18 @@ Args ParseArgs(int argc, char* argv[]) {
const std::string arg = argv[i];
if (arg == "--help") {
args.showHelp = true;
} else if (arg == "--windows-fixed-base") {
if (args.windowsFixedBase) throw std::runtime_error("--windows-fixed-base cannot be repeated");
if (i + 1 >= argc || std::string(argv[i + 1]).empty() || std::string(argv[i + 1]).starts_with("--"))
throw std::runtime_error("--windows-fixed-base requires an address");
std::string_view value(argv[++i]);
const bool hexadecimal = value.starts_with("0x") || value.starts_with("0X");
if (hexadecimal) value.remove_prefix(2);
std::uint64_t address = 0;
const auto parsed = std::from_chars(value.data(), value.data() + value.size(), address, hexadecimal ? 16 : 10);
if (parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size() || address < 0x20000 || address > 0x7fffffff0000 || (address & 0xffff) != 0)
throw std::runtime_error("--windows-fixed-base requires a 64 KiB-aligned address from 0x20000 through 0x7fffffff0000");
args.windowsFixedBase = address;
} else if (arg == "--skip-syscall-check") {
args.skipSyscallCheck = true;
} else if (arg == "--trap-sdk-syscall-stubs") {
@ -108,6 +122,9 @@ Args ParseArgs(int argc, char* argv[]) {
if (args.windowsGui && !args.toWindows)
throw std::runtime_error("--windows-gui requires --windows");
if (args.windowsFixedBase && !args.toWindows)
throw std::runtime_error("--windows-fixed-base requires --windows");
if (!args.showHelp && (args.inputPath.empty() || args.outputPath.empty()))
throw std::runtime_error(Usage());

View File

@ -206,6 +206,22 @@ void missingPositionals() {
expectUsage({"--rpath", "/libs"});
}
void windowsFixedBase() {
require(!parse({"--windows", "input.elf", "output.exe"}).windowsFixedBase, "fixed base was enabled by default");
for (const auto& value : {"0x400000", "0X400000", "4194304"}) {
const auto args = parse({"--windows-fixed-base", value, "--windows", "input.elf", "output.exe"});
require(args.windowsFixedBase == 0x400000, "fixed base was parsed incorrectly");
}
require(parse({"--windows", "--windows-fixed-base", "0x400000000", "input.elf", "output.exe"}).windowsFixedBase == 0x400000000, "fixed base was truncated to 32 bits");
for (const auto& value : {"0", "0x10000", "0x400001", "0x", "-4194304", "+4194304", " 4194304", "0x400000junk", "18446744073709551616", "0x800000000000"})
expectThrows({"--windows", "--windows-fixed-base", value, "input.elf", "output.exe"}, "--windows-fixed-base requires a 64 KiB-aligned address from 0x20000 through 0x7fffffff0000");
expectThrows({"--windows-fixed-base"}, "--windows-fixed-base requires an address");
expectThrows({"--windows-fixed-base", "--windows"}, "--windows-fixed-base requires an address");
expectThrows({"--windows-fixed-base", "0x400000", "input.elf", "output.exe"}, "--windows-fixed-base requires --windows");
expectThrows({"--macos", "--windows-fixed-base", "0x400000", "input.elf", "output.exe"}, "--windows-fixed-base requires --windows");
expectThrows({"--windows", "--windows-fixed-base", "0x400000", "--windows-fixed-base", "0x800000", "input.elf", "output.exe"}, "--windows-fixed-base cannot be repeated");
}
}
int main() {
@ -233,6 +249,7 @@ int main() {
windowsDiagnosticsRequiresWindows();
windowsGuiRequiresWindows();
missingPositionals();
windowsFixedBase();
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;

View File

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

View File

@ -8,11 +8,12 @@ namespace Elfpatcher::Windows {
class WindowsLoadImage {
public:
WindowsLoadImage(const std::vector<std::uint8_t>& source, const std::vector<Domain::ProgramHeader>& headers, bool requireEntry = true);
WindowsLoadImage(const std::vector<std::uint8_t>& source, const std::vector<Domain::ProgramHeader>& headers, bool requireEntry = true, std::uint64_t imageBase = ImageBase);
std::uint32_t GetRva(std::uint64_t address, std::uint64_t size = 1) const;
std::uint32_t GetEndRva() const;
std::uint32_t GetEntryRva() const;
std::uint64_t GetRelocatedAddress(std::uint64_t address) const;
std::uint64_t GetImageBase() const;
void WritePointer(std::uint64_t address, std::uint64_t value);
void RequireWritable(std::uint64_t address, std::uint64_t size) const;
std::vector<PeSection> BuildSections() const;
@ -22,6 +23,7 @@ private:
std::vector<std::uint8_t> data;
std::vector<std::uint32_t> pageFlags;
std::uint64_t firstAddress;
std::uint64_t imageBase;
std::uint32_t entryRva = 0;
};

View File

@ -2,12 +2,13 @@
#define ELFPATCHER_WINDOWS_PEWRITER_HPP
#include <elfpatcher/windows/WindowsPeFormat.hpp>
#include <optional>
namespace Elfpatcher::Windows {
class WindowsPeWriter {
public:
std::vector<std::uint8_t> Write(const std::vector<PeSection>& sections, std::uint32_t entryRva, const std::array<PeDirectory, 16>& directories, bool windowsGui = false) const;
std::vector<std::uint8_t> Write(const std::vector<PeSection>& sections, std::uint32_t entryRva, const std::array<PeDirectory, 16>& directories, bool windowsGui = false, std::optional<std::uint64_t> fixedImageBase = std::nullopt) const;
};
}

View File

@ -5,7 +5,7 @@
namespace Elfpatcher::Windows {
WindowsLoadImage::WindowsLoadImage(const std::vector<std::uint8_t>& source, const std::vector<Domain::ProgramHeader>& headers, const bool requireEntry) {
WindowsLoadImage::WindowsLoadImage(const std::vector<std::uint8_t>& source, const std::vector<Domain::ProgramHeader>& headers, const bool requireEntry, const std::uint64_t imageBase) : imageBase(imageBase) {
if (source.size() < 64 || Io::ReadU32(source, 0) != 0x464c457f || source[4] != 2 || source[5] != 1 || source[6] != 1 || Io::ReadU16(source, 18) != 62)
throw Domain::RelinkerException("Windows output requires a little-endian ELF64 x86-64 image");
const auto type = Io::ReadU16(source, 16);
@ -78,7 +78,11 @@ std::uint32_t WindowsLoadImage::GetEntryRva() const {
std::uint64_t WindowsLoadImage::GetRelocatedAddress(const std::uint64_t address) const {
if (address < firstAddress || address - firstAddress > data.size())
throw Domain::RelinkerException("Relative relocation addend is outside the ELF image", address);
return ImageBase + LoadRva + address - firstAddress;
return imageBase + LoadRva + address - firstAddress;
}
std::uint64_t WindowsLoadImage::GetImageBase() const {
return imageBase;
}
void WindowsLoadImage::WritePointer(const std::uint64_t address, const std::uint64_t value) {

View File

@ -16,11 +16,11 @@ namespace Elfpatcher::Windows {
namespace {
void writeGotStub(std::vector<PeSection>& sections, const std::uint32_t targetRva, const std::uint32_t stubRva) {
void writeGotStub(std::vector<PeSection>& sections, const std::uint32_t targetRva, const std::uint32_t stubRva, const std::uint64_t imageBase) {
for (auto& section : sections) {
if (section.Data.size() < 8 || targetRva < section.Rva || targetRva - section.Rva > section.Data.size() - 8)
continue;
Io::WriteU64(section.Data, targetRva - section.Rva, ImageBase + stubRva);
Io::WriteU64(section.Data, targetRva - section.Rva, imageBase + stubRva);
return;
}
throw Domain::RelinkerException("Lazy import GOT slot is not contained in any section", targetRva);
@ -28,7 +28,9 @@ void writeGotStub(std::vector<PeSection>& sections, const std::uint32_t targetRv
}
WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional<std::vector<std::uint8_t>> icon, std::filesystem::path iconName, std::function<void(std::uint64_t, std::uint64_t)> progress) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)) {
WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional<std::vector<std::uint8_t>> icon, std::filesystem::path iconName, std::function<void(std::uint64_t, std::uint64_t)> progress, const std::optional<std::uint64_t> fixedBase) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase) {
if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0))
throw Domain::RelinkerException("Invalid fixed Windows guest image base");
}
const std::string& WindowsPePatcher::GetDiagnostics() const { return diagnostics; }
@ -37,7 +39,7 @@ std::vector<std::uint8_t> WindowsPePatcher::Patch(const std::vector<std::uint8_t
const auto report = [&](std::uint64_t completed) { if (progress) progress(completed, 10000); };
report(0);
diagnostics.clear();
WindowsLoadImage image(sourceElf, originalHeaders);
WindowsLoadImage image(sourceElf, originalHeaders, true, fixedBase ? *fixedBase - LoadRva : ImageBase);
if (originalPltGotVaddr != 0)
image.GetRva(originalPltGotVaddr, 8);
report(500);
@ -94,10 +96,10 @@ std::vector<std::uint8_t> WindowsPePatcher::Patch(const std::vector<std::uint8_t
sections.push_back(std::move(entry.Data));
sections.push_back(std::move(entry.Code));
for (const auto& lazyStub : entry.LazyStubs) {
writeGotStub(sections, lazyStub.TargetRva, lazyStub.StubRva);
writeGotStub(sections, lazyStub.TargetRva, lazyStub.StubRva, image.GetImageBase());
relocations.BaseRelocations.push_back(lazyStub.TargetRva);
}
auto relocationData = relocationBuilder.BuildBaseRelocations(relocations.BaseRelocations);
auto relocationData = fixedBase ? std::vector<std::uint8_t>{} : relocationBuilder.BuildBaseRelocations(relocations.BaseRelocations);
if (!relocationData.empty()) {
directories[5] = {nextRva, CheckedRva(relocationData.size())};
sections.push_back({".reloc", nextRva, SectionRead | 0x02000040u, std::move(relocationData)});
@ -105,7 +107,7 @@ std::vector<std::uint8_t> WindowsPePatcher::Patch(const std::vector<std::uint8_t
}
directories[2] = icon ? WindowsIconResourceBuilder().Build(*icon, iconName, sections, nextRva) : PeDirectory{};
report(9500);
auto result = WindowsPeWriter().Write(sections, entryRva, directories, windowsGui);
auto result = WindowsPeWriter().Write(sections, entryRva, directories, windowsGui, fixedBase ? std::optional<std::uint64_t>(image.GetImageBase()) : std::nullopt);
report(10000);
return result;
}

View File

@ -5,7 +5,7 @@
namespace Elfpatcher::Windows {
std::vector<std::uint8_t> WindowsPeWriter::Write(const std::vector<PeSection>& sections, const std::uint32_t entryRva, const std::array<PeDirectory, 16>& directories, const bool windowsGui) const {
std::vector<std::uint8_t> WindowsPeWriter::Write(const std::vector<PeSection>& sections, const std::uint32_t entryRva, const std::array<PeDirectory, 16>& directories, const bool windowsGui, const std::optional<std::uint64_t> fixedImageBase) const {
constexpr std::size_t peOffset = 0x80;
constexpr std::size_t optionalOffset = peOffset + 24;
constexpr std::size_t sectionTable = optionalOffset + 240;
@ -18,10 +18,12 @@ std::vector<std::uint8_t> WindowsPeWriter::Write(const std::vector<PeSection>& s
Io::WriteU16(result, peOffset + 4, 0x8664);
Io::WriteU16(result, peOffset + 6, static_cast<std::uint16_t>(sections.size()));
Io::WriteU16(result, peOffset + 20, 240);
Io::WriteU16(result, peOffset + 22, 0x22);
if (fixedImageBase && (directories[5].Size != 0 || *fixedImageBase < 0x10000 || (*fixedImageBase & 0xffff) != 0))
throw Domain::RelinkerException("Invalid fixed Windows image base or base relocations");
Io::WriteU16(result, peOffset + 22, fixedImageBase ? 0x23 : 0x22);
Io::WriteU16(result, optionalOffset, 0x20b);
Io::WriteU32(result, optionalOffset + 16, entryRva);
Io::WriteU64(result, optionalOffset + 24, ImageBase);
Io::WriteU64(result, optionalOffset + 24, fixedImageBase.value_or(ImageBase));
Io::WriteU32(result, optionalOffset + 32, SectionAlignment);
Io::WriteU32(result, optionalOffset + 36, FileAlignment);
Io::WriteU16(result, optionalOffset + 40, 6);
@ -70,6 +72,8 @@ std::vector<std::uint8_t> WindowsPeWriter::Write(const std::vector<PeSection>& s
}
if (!foundEntry)
throw Domain::RelinkerException("PE entry point is not inside executable section data", entryRva);
if (fixedImageBase && *fixedImageBase > 0x7fffffffffff - static_cast<std::uint64_t>(endRva))
throw Domain::RelinkerException("Fixed Windows image exceeds the user address space");
Io::WriteU32(result, optionalOffset + 4, codeSize);
Io::WriteU32(result, optionalOffset + 8, dataSize);
Io::WriteU32(result, optionalOffset + 56, endRva);

View File

@ -402,7 +402,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector<std::uint8_t>& source, co
const auto templateBytes = WindowsTlsTemplateBuilder().Build(*tls, image, sections, templateRva, relocations);
std::copy(templateBytes.begin(), templateBytes.end(), data.Data.begin() + templateOffset);
const auto writeAddress = [&](const std::size_t offset, const std::uint32_t rva) {
Io::WriteU64(data.Data, offset, ImageBase + rva);
Io::WriteU64(data.Data, offset, image.GetImageBase() + rva);
relocations.push_back(CheckedRva(data.Rva + offset));
};

View File

@ -57,6 +57,7 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath,
options.LazyBinding = args.lazyBinding;
options.RunPath = args.runPath;
options.WindowsGui = args.windowsGui;
options.WindowsFixedBase = mainExecutable ? args.windowsFixedBase : std::nullopt;
options.WindowsDiagnostics = args.windowsDiagnostics && mainExecutable;
options.ExcludedModules = args.excludedSceModules;
progress.IncludeFile(Io::Utf8Path(input.InputPath));

View File

@ -18,6 +18,7 @@ struct ConversionOptions {
bool TrapSdkSyscallStubs = false;
bool LazyBinding = false;
bool WindowsGui = false;
std::optional<std::uint64_t> WindowsFixedBase;
bool WindowsDiagnostics = false;
std::string RunPath = "$ORIGIN/libs";
std::set<std::string> ExcludedModules;

View File

@ -102,7 +102,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti
if (macos) {
patcher = std::make_shared<Elfpatcher::MacOs::MacOsMachOPatcher>();
} else if (windows) {
patcher = std::make_shared<Elfpatcher::Windows::WindowsPePatcher>(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); });
patcher = std::make_shared<Elfpatcher::Windows::WindowsPePatcher>(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase);
} else {
patcher = std::make_shared<Elfpatcher::Linux::LinuxElfPatcher>(
std::make_shared<Elfpatcher::EntryStubBuilder>(),

View File

@ -0,0 +1,147 @@
import os
from pathlib import Path
import struct
import subprocess
import sys
import tempfile
from test_optional_plt import fixture
from test_windows_import_modules import executable, provider
def pe_headers(data):
pe = struct.unpack_from("<I", data, 0x3c)[0]
optional = pe + 24
return pe, optional
def file_offset(data, rva):
pe, optional = pe_headers(data)
table = optional + struct.unpack_from("<H", data, pe + 20)[0]
for index in range(struct.unpack_from("<H", data, pe + 6)[0]):
size, start, raw_size, raw = struct.unpack_from("<IIII", data, table + index * 40 + 8)
if start <= rva and rva - start < min(size, raw_size):
return raw + rva - start
raise AssertionError(f"RVA {rva:#x} has no file data")
def absolute_jump(base):
return bytes.fromhex("48 a1") + struct.pack("<Q", base + 0x300) + bytes.fromhex("ff e0")
def run(output, expected=42):
if os.name == "nt":
result = subprocess.run([str(output)], capture_output=True, timeout=5)
assert result.returncode == expected, (output.name, result.returncode, result.stdout, result.stderr)
def check_occupied_base(output):
if os.name != "nt":
return
try:
result = subprocess.run([str(output)], capture_output=True, timeout=5)
except OSError as error:
assert error.winerror == 487, error
else:
assert result.returncode & 0xffffffff == 0xc0000018, (result.returncode, result.stdout, result.stderr)
def main():
relinker = Path(sys.argv[1]).resolve()
with tempfile.TemporaryDirectory(prefix="anyps5-fixed-base-") as directory:
work = Path(directory)
def convert(name, image, base=None, options=(), modules=False):
case = work / name
case.mkdir(exist_ok=True)
source = case / "input.elf"
output = case / "output.exe"
source.write_bytes(image)
arguments = [str(relinker), "--windows"]
if not modules:
arguments.append("--skip-sce-module")
if base is not None:
arguments += ["--windows-fixed-base", hex(base)]
result = subprocess.run([*arguments, *options, str(source), str(output)], capture_output=True, text=True, timeout=5)
assert result.returncode == 0, (name, result.returncode, result.stdout, result.stderr)
data = output.read_bytes()
pe, optional = pe_headers(data)
assert struct.unpack_from("<Q", data, optional + 24)[0] == (0x140000000 if base is None else base - 0x10000)
assert bool(struct.unpack_from("<H", data, pe + 22)[0] & 1) == (base is not None)
characteristics = struct.unpack_from("<H", data, optional + 70)[0]
assert characteristics & 0x100
assert bool(characteristics & 0x40) == (base is None)
relocations = struct.unpack_from("<II", data, optional + 112 + 5 * 8)
assert bool(relocations[1]) == (base is None)
return output
default = convert("default", fixture())
run(default)
image = fixture()
struct.pack_into("<H", image, 16, 0xfe10)
code = bytes.fromhex("b8 00 03 00 00 48 83 b8 00 00 40 00 00 74 06 b8 2a 00 00 00 c3 31 c0 c3")
image[0x200:0x200 + len(code)] = code
image[0x240:0x246] = bytes.fromhex("b8 2a 00 00 00 c3")
struct.pack_into("<q", image, 0x710, 0x240)
run(convert("sce-dynexec-imports-offset", image, 0x400000))
for base in (0x400000, 0x800000, 0x400000000):
image = fixture()
code = absolute_jump(base)
image[0x200:0x200 + len(code)] = code
output = convert(f"absolute-{base:x}", image, base)
data = output.read_bytes()
assert struct.unpack_from("<Q", data, file_offset(data, 0x10300))[0] == base + 0x210
for _ in range(3):
run(output)
image = fixture()
code = bytes.fromhex("64 48 8b 04 25 f0 ff ff ff ff e0")
image[0x200:0x200 + len(code)] = code
struct.pack_into("<H", image, 0x38, 3)
struct.pack_into("<IIQQQQQQ", image, 176, 7, 4, 0x800, 0x800, 0x800, 8, 16, 8)
struct.pack_into("<Q", image, 0x400 + 5 * 16 + 8, 48)
struct.pack_into("<QQq", image, 0x718, 0x800, 8, 0x210)
tls = convert("tls", image, 0x400000)
run(tls)
data = tls.read_bytes()
_, optional = pe_headers(data)
tls_rva = struct.unpack_from("<I", data, optional + 112 + 9 * 8)[0]
start, end, index, callbacks = struct.unpack_from("<QQQQ", data, file_offset(data, tls_rva))
size = struct.unpack_from("<I", data, optional + 56)[0]
assert all(0x3f0000 <= value < 0x3f0000 + size for value in (start, end, index, callbacks))
assert struct.unpack_from("<Q", data, file_offset(data, start - 0x3f0000))[0] == 0x400210
case = work / "guest-import"
(case / "prx").mkdir(parents=True)
(case / "prx" / "provider.prx").write_bytes(provider(42))
image = executable("provider.prx")
code = bytes.fromhex("48 a1") + struct.pack("<Q", 0x400a20) + bytes.fromhex("ff d0 c3")
image[0x4000:0x4000 + len(code)] = code
imported = convert("guest-import", image, 0x400000, modules=True)
run(imported)
guest = case / "app0" / "prx" / "provider.prx.guest.prx"
guest_bytes = guest.read_bytes()
_, guest_optional = pe_headers(guest_bytes)
assert struct.unpack_from("<Q", guest_bytes, guest_optional + 24)[0] == 0x140000000
for lazy in (False, True):
output = convert(f"native-import-{lazy}", image, 0x400000, ("--lazy-binding",) if lazy else ())
(output.parent / "libs").mkdir()
(output.parent / "libs" / "provider.prx").write_bytes(guest_bytes)
run(output)
collision = convert("shared-user-data-collision", fixture(), 0x7ffe0000)
check_occupied_base(collision)
source = work / "bounds.elf"
output = work / "bounds.exe"
image = fixture()
struct.pack_into("<Q", image, 64 + 40, 0x20000)
source.write_bytes(image)
result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", "--windows-fixed-base", "0x7fffffff0000", str(source), str(output)], capture_output=True, text=True, timeout=5)
assert result.returncode == 2 and "user address space" in result.stderr and not output.exists(), result
print("Windows fixed base: default, absolute GOT, 64-bit base, TLS, guest/native/lazy imports, collision and bounds passed")
if __name__ == "__main__":
main()

View File

@ -48,6 +48,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath`
| `--macos` | Produce an x86-64 Mach-O executable, with bundled modules as Mach-O libraries in `app0/sce_module`. Conflicts with `--windows`. |
| `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. |
| `--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. |
| `--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: RDSEED becomes RDRAND, CLWB a NOP, and RDPID returns processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. |
| `unused-filter=0` | Keep all imported NID references. |
@ -73,6 +74,8 @@ The `--skip-sce-module`, `--exclude-sce-module <file>`, `--skip-syscall-check`,
## 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 a fixed address. Without it, Windows placement and relocation behavior are unchanged. This option does not change Linux or macOS output.
Paths are relative to the output executable:
```text