From 319fa5779a610fa35d3080f3f44b926a96fae3d9 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Mon, 12 Oct 2026 05:54:06 +0600 Subject: [PATCH] feat(relinker): support fixed Windows guest image bases --- core/relinker/CMakeLists.txt | 3 + core/relinker/cli/include/Cli.hpp | 2 + core/relinker/cli/src/CliArgs.cpp | 19 ++- core/relinker/cli/tests/CliArgsTests.cpp | 17 ++ .../elfpatcher/windows/WindowsElfPatcher.hpp | 4 +- .../elfpatcher/windows/WindowsLoadImage.hpp | 4 +- .../elfpatcher/windows/WindowsPeWriter.hpp | 3 +- .../src/windows/WindowsLoadImage.cpp | 8 +- .../src/windows/WindowsPePatcher.cpp | 16 +- .../src/windows/WindowsPeWriter.cpp | 10 +- .../src/windows/WindowsTlsBuilder.cpp | 2 +- core/relinker/main.cpp | 1 + .../include/relinker/pipeline/Conversion.hpp | 1 + .../relinker/src/pipeline/Conversion.cpp | 2 +- .../relinker/tests/test_windows_fixed_base.py | 147 ++++++++++++++++++ docs/user/USAGE.md | 3 + 16 files changed, 224 insertions(+), 18 deletions(-) create mode 100644 core/relinker/relinker/tests/test_windows_fixed_base.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 6f1f81363..94816c2c1 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -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 $) + add_test(NAME windows_fixed_base COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_fixed_base.py $) + 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 $) add_test(NAME dynamic_segment COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 96df4f521..91586ff4e 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace Cli { @@ -22,6 +23,7 @@ struct Args { bool autorun = false; bool windowsDiagnostics = false; bool windowsGui = false; + std::optional windowsFixedBase; std::uint32_t unusedFilterLevel = 0; std::string inputPath; std::string outputPath; diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index fd896e80e..267cf0ce5 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -1,14 +1,16 @@ #include #include #include +#include #include #include #include +#include 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 ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" + return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--windows-fixed-base
] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--executable ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \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()); diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 5265776a3..433118159 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -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; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp index ac4be9dfe..8544e8e85 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp @@ -5,12 +5,13 @@ #include #include #include +#include namespace Elfpatcher::Windows { class WindowsPePatcher : public IElfPatcher { public: - explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}); + explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt); const std::string& GetDiagnostics() const; std::vector Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& trampolines) override; @@ -21,6 +22,7 @@ private: std::optional> icon; std::string diagnostics; std::filesystem::path iconName; + std::optional fixedBase; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp index 8bd9ff6ec..dc6d8cf6e 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsLoadImage.hpp @@ -8,11 +8,12 @@ namespace Elfpatcher::Windows { class WindowsLoadImage { public: - WindowsLoadImage(const std::vector& source, const std::vector& headers, bool requireEntry = true); + WindowsLoadImage(const std::vector& source, const std::vector& 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 BuildSections() const; @@ -22,6 +23,7 @@ private: std::vector data; std::vector pageFlags; std::uint64_t firstAddress; + std::uint64_t imageBase; std::uint32_t entryRva = 0; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp index 9692b150b..9c0b8f38f 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeWriter.hpp @@ -2,12 +2,13 @@ #define ELFPATCHER_WINDOWS_PEWRITER_HPP #include +#include namespace Elfpatcher::Windows { class WindowsPeWriter { public: - std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false) const; + std::vector Write(const std::vector& sections, std::uint32_t entryRva, const std::array& directories, bool windowsGui = false, std::optional fixedImageBase = std::nullopt) const; }; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp index f82ee3d93..ab78c51b1 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsLoadImage.cpp @@ -5,7 +5,7 @@ namespace Elfpatcher::Windows { -WindowsLoadImage::WindowsLoadImage(const std::vector& source, const std::vector& headers, const bool requireEntry) { +WindowsLoadImage::WindowsLoadImage(const std::vector& source, const std::vector& 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) { diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 0f91dbccc..562343c94 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -16,11 +16,11 @@ namespace Elfpatcher::Windows { namespace { -void writeGotStub(std::vector& sections, const std::uint32_t targetRva, const std::uint32_t stubRva) { +void writeGotStub(std::vector& 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& sections, const std::uint32_t targetRv } -WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)) { +WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional 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 WindowsPePatcher::Patch(const std::vector WindowsPePatcher::Patch(const std::vector{} : 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 WindowsPePatcher::Patch(const std::vector(image.GetImageBase()) : std::nullopt); report(10000); return result; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp index e91f5e59f..1e346dbed 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp @@ -5,7 +5,7 @@ namespace Elfpatcher::Windows { -std::vector WindowsPeWriter::Write(const std::vector& sections, const std::uint32_t entryRva, const std::array& directories, const bool windowsGui) const { +std::vector WindowsPeWriter::Write(const std::vector& sections, const std::uint32_t entryRva, const std::array& directories, const bool windowsGui, const std::optional 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 WindowsPeWriter::Write(const std::vector& s Io::WriteU16(result, peOffset + 4, 0x8664); Io::WriteU16(result, peOffset + 6, static_cast(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 WindowsPeWriter::Write(const std::vector& s } if (!foundEntry) throw Domain::RelinkerException("PE entry point is not inside executable section data", entryRva); + if (fixedImageBase && *fixedImageBase > 0x7fffffffffff - static_cast(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); diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 407698ffa..2a017013c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -402,7 +402,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& 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)); }; diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 556c4743f..c8c7adb8f 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -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)); diff --git a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp index d1b0cf021..4097ed184 100644 --- a/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/Conversion.hpp @@ -18,6 +18,7 @@ struct ConversionOptions { bool TrapSdkSyscallStubs = false; bool LazyBinding = false; bool WindowsGui = false; + std::optional WindowsFixedBase; bool WindowsDiagnostics = false; std::string RunPath = "$ORIGIN/libs"; std::set ExcludedModules; diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 5ff4f4d23..5d6b03824 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -102,7 +102,7 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti if (macos) { patcher = std::make_shared(); } else if (windows) { - patcher = std::make_shared(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(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( std::make_shared(), diff --git a/core/relinker/relinker/tests/test_windows_fixed_base.py b/core/relinker/relinker/tests/test_windows_fixed_base.py new file mode 100644 index 000000000..cd5d8cd5f --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_fixed_base.py @@ -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("` | 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 `, `--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