diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index ab124744fb3..a9dd1b8138e 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -53,6 +53,8 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp @@ -141,6 +143,8 @@ if(WIN32) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ) target_include_directories(amd64_only_windows_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libc/src/specifics/windows) if(BUILD_TESTING) @@ -177,6 +181,19 @@ if(BUILD_TESTING) add_test(NAME call_site_index COMMAND call_site_index_tests) set_tests_properties(call_site_index PROPERTIES TIMEOUT 20) + add_test_executable(sdk_mman_syscall_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/SdkMmanSyscallTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/DecodedInstruction.cpp) + target_include_directories(sdk_mman_syscall_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include) + add_test(NAME sdk_mman_syscall COMMAND sdk_mman_syscall_tests) + set_tests_properties(sdk_mman_syscall PROPERTIES TIMEOUT 20) + add_test_executable(autorun_exit_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/AutorunExitTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp @@ -297,6 +314,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ClzeroLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalOperands.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ReciprocalLowering.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SegmentFilter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/EntryStubBuilder.cpp @@ -312,6 +331,18 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/io/include ) add_test(NAME amd64_only_converter COMMAND amd64_only_converter_tests) + add_test_executable(call_site_resolver_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteResolverTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(call_site_resolver_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ) + add_test(NAME call_site_resolver COMMAND call_site_resolver_tests) add_test_executable(decoder_length_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/DecoderLengthTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp @@ -334,11 +365,14 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_rela_size.py $) add_test(NAME none_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_none_relocations.py $) + add_test(NAME guest_none_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_none_relocations.py $) + set_tests_properties(guest_none_relocations PROPERTIES TIMEOUT 20) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_empty_tls.py $) - set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads rejected) + set(tlsCoverageGroups metadata padding register_loads displacement_bounds alu_execution register_address_loads moved_branch rejected) foreach(register IN ITEMS 0 1 2 3 5 6 7 8 9 10 11 12 13 14 15) list(APPEND tlsCoverageGroups displacement_r${register}) endforeach() @@ -374,6 +408,9 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_gui.py $) add_test(NAME windows_working_directory COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_working_directory.py $) + add_test(NAME windows_runpath COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_runpath.py $) + set_tests_properties(windows_runpath PROPERTIES TIMEOUT 30) add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $) add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE} @@ -419,24 +456,33 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) add_test(NAME guest_module_identity COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_identity.py $) + add_test(NAME guest_export_lookup COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_export_lookup.py $) + set_tests_properties(guest_export_lookup PROPERTIES TIMEOUT 30) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py $) add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $) + add_test(NAME weak_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_weak_imports.py $) add_test(NAME input_magic COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_magic.py $) add_test(NAME input_preservation COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_preservation.py $) add_test(NAME input_file COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_file.py $) + add_test(NAME null_symbol_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_null_symbol_relocations.py $) add_test(NAME input_nonascii COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_nonascii.py $) add_test(NAME string_table_bounds COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_string_table_bounds.py $) add_test(NAME sce_code_extent COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_extent.py $) + add_test(NAME sce_relro COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_relro.py $) add_test(NAME call_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $) add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE} @@ -499,6 +545,20 @@ if(BUILD_TESTING) ) add_test(NAME entry_stub COMMAND entry_stub_tests) + add_test_executable(control_flow_graph_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ControlFlowGraphTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(control_flow_graph_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ) + add_test(NAME control_flow_graph COMMAND control_flow_graph_tests) + set_tests_properties(control_flow_graph PROPERTIES TIMEOUT 20) + add_test_executable(strict_nid_filter_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/StrictNidFilterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index eef10daad7b..21d055d6b93 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -71,6 +71,8 @@ Args ParseArgs(int argc, char* argv[]) { args.windowsGui = true; } else if (arg.rfind("--", 0) == 0 || arg == "unused-filter") { throw std::runtime_error("unknown option: " + arg); + } else if (arg.empty()) { + throw std::runtime_error("input and output paths must be nonempty"); } else if (args.inputPath.empty()) { args.inputPath = arg; } else if (args.outputPath.empty()) { diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index ec27e771aa6..3cc339e55d3 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -163,6 +163,13 @@ void unknownOption() { expectThrows({"--", "input.elf", "output.elf"}, "unknown option: --"); } +void emptyPositionals() { + for (const auto& arguments : std::vector>{ + {"", "input.elf", "output.elf"}, {"input.elf", "", "output.elf"}, + {"", "output.elf"}, {"input.elf", ""}}) + expectThrows(arguments, "input and output paths must be nonempty"); +} + void tooManyPositionals() { expectThrows({"input.elf", "output.elf", "extra.elf"}, "unexpected argument: extra.elf"); } @@ -211,6 +218,7 @@ int main() { unusedFilterRejectsDuplicates(); unusedFilterBareWordIsUnknown(); unknownOption(); + emptyPositionals(); tooManyPositionals(); windowsDiagnosticsRequiresWindows(); windowsGuiRequiresWindows(); diff --git a/core/relinker/codegen/include/codegen/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index e41f6cd538f..43e5a7110b6 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -65,6 +65,7 @@ struct DecodedInstructionInfo { std::uint8_t RexPrefix; ControlFlowKind FlowKind; bool HasRipRelativeDisp; + bool AddressSize32; std::size_t RipRelativeDispOffset; bool HasModRm; std::uint8_t ModRmByte; diff --git a/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp b/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp index ca82119d748..e133c533f14 100644 --- a/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp +++ b/core/relinker/codegen/include/codegen/IAmd64OnlyConverter.hpp @@ -14,7 +14,7 @@ class IAmd64OnlyConverter { [[nodiscard]] virtual ConvertResult Convert( std::vector fileBytes, - const std::vector& codeSegments + const std::vector& segments ) const = 0; }; diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index 67e81c11a7d..7c94063bc7b 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -42,6 +42,7 @@ inline constexpr Entry kRdseed = {"RDSEED", nullptr, 0}; inline constexpr Entry kRdpid = {"RDPID", nullptr, 0}; inline constexpr Entry kClwb = {"CLWB", nullptr, 0}; inline constexpr Entry kCpuid = {"CPUID", nullptr, 0}; +inline constexpr Entry kWideStore = {"256-bit store", nullptr, 0}; inline constexpr Entry kExtrq = {"EXTRQ", nullptr, 0}; inline constexpr Entry kInsertq = {"INSERTQ", nullptr, 0}; inline constexpr Entry kExtrqRegisterForm = {"EXTRQ register form", nullptr, 0}; diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp index bcb5df81629..e2f59464242 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp @@ -35,6 +35,7 @@ struct Amd64OnlyMatch { std::size_t ReturnBranchOffset; bool Optional = false; std::vector Relocations = {}; + bool RelocatableWindow = false; }; struct Amd64OnlySubstitutionReport { diff --git a/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp b/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp index df17eb1753e..9986e79286a 100644 --- a/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp +++ b/core/relinker/codegen/include/codegen/x86/IAmd64OnlyInstructionMatcher.hpp @@ -2,6 +2,7 @@ #define CODEGEN_X86_IAMD64ONLYINSTRUCTIONMATCHER_HPP #include +#include #include #include #include @@ -23,6 +24,11 @@ class IAmd64OnlyInstructionMatcher { std::span> instructions, std::span trailing ) const = 0; + + virtual void EmitOutOfLine( + StubBodyBuilder& body, + std::span instruction + ) const = 0; }; std::unique_ptr MakeAmd64OnlyInstructionMatcher(Amd64OnlyTarget target = Amd64OnlyTarget::Intel); diff --git a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp index 212eeea546c..f2ff3c3652c 100644 --- a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp +++ b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp @@ -1,6 +1,7 @@ #ifndef CODEGEN_X86_STUBBODYBUILDER_HPP #define CODEGEN_X86_STUBBODYBUILDER_HPP +#include #include #include #include @@ -53,6 +54,10 @@ class StubBodyBuilder { void Restore(std::uint8_t reg); void Raw(std::span bytes); void Move(std::span instruction, std::optional ripDisplacementOffset); + void RipRelative(std::span instruction, std::size_t displacementOffset, std::int64_t target); + void MoveControlFlow(std::span instruction, const DecodedInstructionInfo& info); + void JumpTo(std::int64_t siteTarget); + void Seek(std::int64_t siteOffset); void Advance(std::size_t length); [[nodiscard]] LoweredBody Finish(); @@ -63,12 +68,15 @@ class StubBodyBuilder { std::size_t ConstantIndex; }; + void _rel32(std::initializer_list opcode, std::int64_t siteTarget); + void _pushReturnAddress(std::int64_t siteTarget); + std::vector _bytes; std::vector _fixups; std::vector _constants; std::vector _relocations; std::size_t _stackDepth = 0; - std::size_t _siteOffset = 0; + std::int64_t _siteOffset = 0; }; } diff --git a/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp b/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp new file mode 100644 index 00000000000..7e27e279669 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/WideStoreLowering.hpp @@ -0,0 +1,16 @@ +#ifndef CODEGEN_X86_WIDESTORELOWERING_HPP +#define CODEGEN_X86_WIDESTORELOWERING_HPP + +#include +#include + +namespace Codegen { + +class WideStoreLowering { +public: + void EmitOutOfLine(StubBodyBuilder& body, const WideStoreOperands& operands) const; +}; + +} + +#endif diff --git a/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp b/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp new file mode 100644 index 00000000000..1716af37be5 --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/WideStoreOperands.hpp @@ -0,0 +1,26 @@ +#ifndef CODEGEN_X86_WIDESTOREOPERANDS_HPP +#define CODEGEN_X86_WIDESTOREOPERANDS_HPP + +#include +#include +#include +#include + +namespace Codegen { + +struct WideStoreHalf { + std::vector Bytes; + std::optional RipDisplacementOffset; + std::int64_t RipTarget = 0; +}; + +struct WideStoreOperands { + WideStoreHalf Low; + WideStoreHalf High; +}; + +[[nodiscard]] std::optional DecodeVexWideStore(const std::uint8_t* data, std::size_t length); + +} + +#endif diff --git a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp index 889d0c447f8..7a9a14a364e 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -170,6 +170,7 @@ inline constexpr std::uint8_t TwoByteGrp6 = 0x00; inline constexpr std::uint8_t TwoByteLar = 0x02; inline constexpr std::uint8_t TwoByteLsl = 0x03; inline constexpr std::uint8_t TwoBytePrefetchw = 0x0D; +inline constexpr std::uint8_t TwoByte3DNow = 0x0F; inline constexpr std::uint8_t TwoByteMovCrDrMin = 0x20; inline constexpr std::uint8_t TwoByteMovCrDrMax = 0x23; inline constexpr std::uint8_t TwoBytePopcnt = 0xB8; diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index 20163046468..6990cf40fb1 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -3,9 +3,14 @@ #include #include #include +#include #include +#include #include +#include +#include #include +#include #include #include #include @@ -24,13 +29,191 @@ auto _atFileOffset(const Domain::FileByteOffset base, const TOperation& operatio } } +constexpr std::int64_t kShortJumpSize = 2; +constexpr std::uint32_t kLoadSegment = 1; +constexpr std::uint32_t kExecutableSegment = 1; +constexpr std::uint8_t kLea = 0x8D; +constexpr std::uint64_t kIslandSearchBytes = 0x100; + +struct Placement { + std::vector Sites; + std::vector Claimed; + std::vector Consumed; + std::size_t ReplacementLength = 0; +}; + +class WindowPlacer { +public: + WindowPlacer( + std::vector& seg, + const std::vector& matches, + const Domain::ProgramHeader& ph, + const std::set& branchTargets, + const std::set& pending, + const std::set& claimed, + const IAmd64OnlyInstructionMatcher& matcher + ) : _seg(seg), _matches(matches), _ph(ph), _branchTargets(branchTargets), _pending(pending), _claimed(claimed), _matcher(matcher) {} + + [[nodiscard]] std::optional Window(const std::size_t index) const { + if (index + 1 < _matches.size() && (_pending.contains(index + 1) || _movable(index + 1, true))) { + if (auto placement = _relocate(index, index, index + 1)) + return placement; + } + if (index > 0 && _movable(index - 1, false)) + return _relocate(index, index - 1, index); + return std::nullopt; + } + + [[nodiscard]] std::optional Island(const std::size_t index) { + const auto store = _address(index); + for (std::size_t distance = 1;; ++distance) { + bool inReach = false; + for (const bool before : {true, false}) { + if (before ? distance > index : index + distance >= _matches.size()) + continue; + const auto start = before ? index - distance : index + distance; + if (_address(start) + kIslandSearchBytes < store || _address(start) > store + kIslandSearchBytes) + continue; + inReach = true; + if (auto placement = _islandAt(index, start)) + return placement; + } + if (!inReach) + return std::nullopt; + } + } + +private: + std::vector& _seg; + const std::vector& _matches; + const Domain::ProgramHeader& _ph; + const std::set& _branchTargets; + const std::set& _pending; + const std::set& _claimed; + const IAmd64OnlyInstructionMatcher& _matcher; + X64InstructionDecoder _decoder; + + [[nodiscard]] std::uint64_t _address(const std::size_t index) const { return _ph.MappedAddress + _matches[index].Offset; } + [[nodiscard]] std::uint64_t _end(const std::size_t index) const { return _address(index) + _matches[index].Length; } + [[nodiscard]] std::span _bytes(const std::size_t index) const { return {_seg.data() + _matches[index].Offset, _matches[index].Length}; } + [[nodiscard]] DecodedInstructionInfo _info(const std::size_t index) const { return _decoder.DecodeInstruction(_seg.data() + _matches[index].Offset, _matches[index].Length); } + + [[nodiscard]] bool _entered(const std::uint64_t start, const std::uint64_t end) const { + const auto hit = _branchTargets.upper_bound(start); + return hit != _branchTargets.end() && *hit < end; + } + + [[nodiscard]] bool _blocksFollowing(const std::size_t index) const { + const auto kind = _info(index).FlowKind; + return kind != ControlFlowKind::Sequential && kind != ControlFlowKind::ConditionalBranch; + } + + [[nodiscard]] bool _movable(const std::size_t index, const bool last) const { + return !_claimed.contains(index) && !_pending.contains(index) && (last || !_blocksFollowing(index)); + } + + [[nodiscard]] TrampolineSite _site(const std::uint64_t start, const std::size_t length, LoweredBody body) const { + const auto offset = static_cast(start - _ph.MappedAddress); + return { + static_cast(_ph.Offset + offset), + static_cast(start), + length, + std::vector(_seg.begin() + static_cast(offset), _seg.begin() + static_cast(offset + length)), + std::move(body.Bytes), + body.ReturnBranchOffset, + std::move(body.Relocations) + }; + } + + [[nodiscard]] std::optional _relocate(const std::size_t index, const std::size_t first, const std::size_t last) const { + if (_entered(_address(first), _end(last))) + return std::nullopt; + StubBodyBuilder body; + Placement placement; + try { + for (auto k = first; k <= last; ++k) { + if (_pending.contains(k)) { + _matcher.EmitOutOfLine(body, _bytes(k)); + if (k != index) + placement.Consumed.push_back(k); + } else { + body.MoveControlFlow(_bytes(k), _info(k)); + } + placement.Claimed.push_back(k); + } + } catch (const CodegenException&) { + return std::nullopt; + } + placement.Sites.push_back(_site(_address(first), static_cast(_end(last) - _address(first)), body.Finish())); + placement.ReplacementLength = placement.Sites.back().Body.size(); + return placement; + } + + [[nodiscard]] std::optional _islandAt(const std::size_t index, const std::size_t start) { + using Amd64OnlySubstitutionTable::kJmpRel32; + std::size_t head = 0; + for (auto last = start; last < _matches.size(); ++last) { + if (!_movable(last, true) || _entered(_address(start), _end(last))) + return std::nullopt; + if (head == 0 && _end(last) - _address(start) >= kJmpRel32.Size) + head = static_cast(_end(last) - _address(start)); + const auto island = _address(start) + head; + const auto reach = static_cast(island) - static_cast(_address(index) + kShortJumpSize); + if (head != 0 && (reach < std::numeric_limits::min() || reach > std::numeric_limits::max())) + return std::nullopt; + if (head != 0 && _end(last) >= island + kJmpRel32.Size) { + try { + return _buildIsland(index, start, last, head); + } catch (const CodegenException&) { + return std::nullopt; + } + } + if (_blocksFollowing(last)) + return std::nullopt; + } + return std::nullopt; + } + + [[nodiscard]] Placement _buildIsland(const std::size_t index, const std::size_t start, const std::size_t last, const std::size_t head) { + using Amd64OnlySubstitutionTable::kJmpRel32; + const auto windowStart = _address(start); + const auto windowEnd = _end(last); + const auto island = windowStart + head; + const auto store = _address(index); + Placement placement; + + StubBodyBuilder moved; + for (auto k = start; k <= last; ++k) { + moved.MoveControlFlow(_bytes(k), _info(k)); + placement.Claimed.push_back(k); + } + if (!_blocksFollowing(last)) + moved.JumpTo(static_cast(windowEnd - windowStart)); + placement.Sites.push_back(_site(windowStart, head, moved.Finish())); + + StubBodyBuilder lowered; + lowered.Seek(static_cast(store) - static_cast(island)); + _matcher.EmitOutOfLine(lowered, _bytes(index)); + lowered.JumpTo(static_cast(_end(index)) - static_cast(island)); + placement.Sites.push_back(_site(island, kJmpRel32.Size, lowered.Finish())); + placement.ReplacementLength = placement.Sites.back().Body.size(); + placement.Claimed.push_back(index); + + const auto offset = _matches[index].Offset; + std::fill_n(_seg.begin() + static_cast(offset), _matches[index].Length, Amd64OnlySubstitutionTable::kTrapFill); + _seg[offset] = X64OpcodeConstants::OneByteJmpRel8; + _seg[offset + 1] = static_cast(static_cast(static_cast(island) - static_cast(store + kShortJumpSize))); + return placement; + } +}; + class Amd64OnlyConverter : public IAmd64OnlyConverter { public: explicit Amd64OnlyConverter(const Amd64OnlyTarget target) : _matcher(MakeAmd64OnlyInstructionMatcher(target)) {} [[nodiscard]] ConvertResult Convert( std::vector fileBytes, - const std::vector& codeSegments + const std::vector& segments ) const override; private: @@ -46,30 +229,59 @@ class Amd64OnlyConverter : public IAmd64OnlyConverter { void _convertSegment( std::vector& fileBytes, const Domain::ProgramHeader& ph, + const std::vector& segments, ConvertResult& result ) const; [[nodiscard]] static std::set _collectBranchTargets( const std::vector& seg, const std::vector& matches, - const Domain::ProgramHeader& ph + const Domain::ProgramHeader& ph, + const std::vector& fileBytes, + const std::vector& segments ); }; std::set Amd64OnlyConverter::_collectBranchTargets( const std::vector& seg, const std::vector& matches, - const Domain::ProgramHeader& ph + const Domain::ProgramHeader& ph, + const std::vector& fileBytes, + const std::vector& segments ) { + const auto startsInstruction = [&](const std::uint64_t address) { + const auto offset = address - ph.MappedAddress; + const auto match = std::lower_bound(matches.begin(), matches.end(), offset, [](const InstructionMatch& item, const std::uint64_t value) { return item.Offset < value; }); + return match != matches.end() && match->Offset == offset; + }; const X64InstructionDecoder decoder; std::set targets; for (const auto& match : matches) { const auto info = decoder.DecodeInstruction(seg.data() + match.Offset, match.Length); - if (!info.HasBranchTarget || info.HasRipRelativeDisp) - continue; - const auto target = static_cast(ph.MappedAddress + match.Offset + info.Length) + info.BranchDisp; - if (target >= 0) - targets.insert(static_cast(target)); + const auto next = static_cast(ph.MappedAddress + match.Offset + info.Length); + if (info.HasBranchTarget && !info.HasRipRelativeDisp) { + const auto target = next + info.BranchDisp; + if (target >= 0) + targets.insert(static_cast(target)); + } else if (info.HasRipRelativeDisp && info.Opcode == kLea) { + std::int32_t displacement; + std::memcpy(&displacement, seg.data() + match.Offset + info.RipRelativeDispOffset, sizeof(displacement)); + const auto table = static_cast(next + displacement); + for (const auto& data : segments) { + if (data.Type != kLoadSegment || table < data.MappedAddress || table - data.MappedAddress >= data.FileSize) + continue; + if (data.Offset > fileBytes.size() || data.FileSize > fileBytes.size() - data.Offset) + throw CodegenException("Segment exceeds the file", data.Offset); + for (auto entry = table; entry - data.MappedAddress + sizeof(std::int32_t) <= data.FileSize; entry += sizeof(std::int32_t)) { + std::int32_t relative; + std::memcpy(&relative, fileBytes.data() + data.Offset + (entry - data.MappedAddress), sizeof(relative)); + const auto target = table + static_cast(static_cast(relative)); + if (!startsInstruction(target)) + break; + targets.insert(target); + } + } + } } return targets; } @@ -77,6 +289,7 @@ std::set Amd64OnlyConverter::_collectBranchTargets( void Amd64OnlyConverter::_convertSegment( std::vector& fileBytes, const Domain::ProgramHeader& ph, + const std::vector& segments, ConvertResult& result ) const { const auto segOffset = static_cast(ph.Offset); @@ -104,8 +317,12 @@ void Amd64OnlyConverter::_convertSegment( const bool needsBranchTargets = std::any_of(pending.begin(), pending.end(), [](const Pending& item) { return item.Substitution.Lowering == Amd64OnlyLowering::Trampoline; }); - const auto branchTargets = needsBranchTargets ? _collectBranchTargets(seg, matches, ph) : std::set{}; + const auto branchTargets = needsBranchTargets ? _collectBranchTargets(seg, matches, ph, fileBytes, segments) : std::set{}; + std::set pendingIndices; + for (const auto& item : pending) + pendingIndices.insert(item.Index); + std::set claimed; std::set consumed; for (const auto& item : pending) { if (consumed.contains(item.Index)) @@ -126,6 +343,20 @@ void Amd64OnlyConverter::_convertSegment( break; } case Amd64OnlyLowering::Trampoline: { + if (substitution.RelocatableWindow && match.Length < Amd64OnlySubstitutionTable::kJmpRel32.Size) { + WindowPlacer placer(seg, matches, ph, branchTargets, pendingIndices, claimed, *_matcher); + auto placement = placer.Window(item.Index); + if (!placement) + placement = placer.Island(item.Index); + if (!placement) + throw CodegenException("No room for a jump to the stub of " + substitution.InstructionName + " within its neighbours or a nearby island", fileOffset); + for (auto& site : placement->Sites) + result.Trampolines.push_back(std::move(site)); + claimed.insert(placement->Claimed.begin(), placement->Claimed.end()); + consumed.insert(placement->Consumed.begin(), placement->Consumed.end()); + replacementLength = placement->ReplacementLength; + break; + } std::vector taken; try { std::size_t siteLength = match.Length; @@ -172,6 +403,8 @@ void Amd64OnlyConverter::_convertSegment( }); replacementLength = stub.StubBody.size(); consumed.insert(taken.begin(), taken.end()); + for (auto k = item.Index; k < matches.size() && matches[k].Offset < match.Offset + siteLength; ++k) + claimed.insert(k); } catch (const CodegenException&) { if (!substitution.Optional) throw; @@ -196,11 +429,13 @@ void Amd64OnlyConverter::_convertSegment( ConvertResult Amd64OnlyConverter::Convert( std::vector fileBytes, - const std::vector& codeSegments + const std::vector& segments ) const { ConvertResult result{}; - for (const auto& ph : codeSegments) - _convertSegment(fileBytes, ph, result); + for (const auto& ph : segments) { + if (ph.Type == kLoadSegment && (ph.Flags & kExecutableSegment) != 0) + _convertSegment(fileBytes, ph, segments, result); + } result.Bytes = std::move(fileBytes); return result; } diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 3c549f7b458..e0306bc068d 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -73,8 +75,8 @@ const Entry& _sha1Entry(const Sha1Operands& operands) { return kSha1rnds4; } -Amd64OnlyMatch _trampoline(std::string name, const std::size_t length, LoweredBody body, const bool optional = false) { - return Amd64OnlyMatch{std::move(name), length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, optional, std::move(body.Relocations)}; +Amd64OnlyMatch _trampoline(std::string name, const std::size_t length, LoweredBody body, const bool optional = false, const bool window = false) { + return Amd64OnlyMatch{std::move(name), length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, optional, std::move(body.Relocations), window}; } void _moveTrailing(StubBodyBuilder& body, std::span trailing) { @@ -143,6 +145,11 @@ class Amd64OnlyInstructionMatcher : public IAmd64OnlyInstructionMatcher { std::span trailing ) const override; + void EmitOutOfLine( + StubBodyBuilder& body, + std::span instruction + ) const override; + private: Amd64OnlyTarget _target; Sse4aLowering _lowering; @@ -150,6 +157,7 @@ class Amd64OnlyInstructionMatcher : public IAmd64OnlyInstructionMatcher { Sha1Lowering _sha1Lowering; ClzeroLowering _clzeroLowering; ReciprocalLowering _reciprocalLowering; + WideStoreLowering _wideStoreLowering; [[nodiscard]] Amd64OnlyMatch _matchMovnts(const DecodedInstruction& instr, const Entry& entry) const; [[nodiscard]] Amd64OnlyMatch _matchSse4a(const DecodedInstruction& instr, const Entry& entry, const Entry& registerFormEntry, std::span trailing) const; @@ -157,6 +165,7 @@ class Amd64OnlyInstructionMatcher : public IAmd64OnlyInstructionMatcher { [[nodiscard]] Amd64OnlyMatch _matchSha1(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] Amd64OnlyMatch _matchClzero(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] std::optional _matchRosetta(const DecodedInstruction& instr, std::span trailing) const; + [[nodiscard]] const char* _emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const; }; Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const { @@ -211,9 +220,56 @@ std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const D return _trampoline(kRdpid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdpidReplacement(instr)); }), true); if (instr.IsCpuid()) return _trampoline(kCpuid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(RosettaCpuid::StubBody()); }), true); + if (const auto wide = DecodeVexWideStore(instr.Data, instr.Length)) + return _trampoline(kWideStore.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _wideStoreLowering.EmitOutOfLine(body, *wide); }), false, true); return std::nullopt; } +const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const { + if (instr.IsExtrq() || instr.IsInsertq()) { + const auto operands = DecodeSse4a(instr.Data, instr.Length); + _lowering.EmitOutOfLine(body, operands); + return _sse4aEntry(operands).Name; + } + if (instr.IsSha256()) { + const auto operands = DecodeSha256(instr.Data, instr.Length); + _sha256Lowering.EmitOutOfLine(body, operands); + return _sha256Entry(operands).Name; + } + if (instr.IsSha1()) { + const auto operands = DecodeSha1(instr.Data, instr.Length); + _sha1Lowering.EmitOutOfLine(body, operands); + return _sha1Entry(operands).Name; + } + if (instr.IsClzero()) { + _clzeroLowering.EmitOutOfLine(body, DecodeClzero(instr.Data, instr.Length)); + return kClzero.Name; + } + if (const auto reciprocal = DecodeReciprocal(instr.Data, instr.Length)) { + _reciprocalLowering.EmitOutOfLine(body, *reciprocal); + return _reciprocalEntry(*reciprocal).Name; + } + if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) { + body.Raw(_rdpidReplacement(instr)); + return kRdpid.Name; + } + if (_target == Amd64OnlyTarget::Rosetta && instr.IsCpuid()) { + body.Raw(RosettaCpuid::StubBody()); + return kCpuid.Name; + } + if (const auto wide = _target == Amd64OnlyTarget::Rosetta ? DecodeVexWideStore(instr.Data, instr.Length) : std::nullopt) { + _wideStoreLowering.EmitOutOfLine(body, *wide); + return kWideStore.Name; + } + return nullptr; +} + +void Amd64OnlyInstructionMatcher::EmitOutOfLine(StubBodyBuilder& body, const std::span instruction) const { + if (_emitOutOfLine(body, DecodedInstruction{instruction.data(), instruction.size()}) == nullptr) + throw CodegenException("AMD-only instruction has no out-of-line lowering"); + body.Advance(instruction.size()); +} + std::optional Amd64OnlyInstructionMatcher::MatchSequence( std::span> instructions, std::span trailing @@ -224,41 +280,11 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( const char* name = nullptr; for (const auto& instruction : instructions) { const DecodedInstruction instr{instruction.data(), instruction.size()}; - if (instr.IsExtrq() || instr.IsInsertq()) { - const auto operands = DecodeSse4a(instr.Data, instr.Length); - if (name == nullptr) - name = _sse4aEntry(operands).Name; - _lowering.EmitOutOfLine(body, operands); - } else if (instr.IsSha256()) { - const auto operands = DecodeSha256(instr.Data, instr.Length); - if (name == nullptr) - name = _sha256Entry(operands).Name; - _sha256Lowering.EmitOutOfLine(body, operands); - } else if (instr.IsSha1()) { - const auto operands = DecodeSha1(instr.Data, instr.Length); - if (name == nullptr) - name = _sha1Entry(operands).Name; - _sha1Lowering.EmitOutOfLine(body, operands); - } else if (instr.IsClzero()) { - const auto operands = DecodeClzero(instr.Data, instr.Length); - if (name == nullptr) - name = kClzero.Name; - _clzeroLowering.EmitOutOfLine(body, operands); - } else if (const auto reciprocal = DecodeReciprocal(instr.Data, instr.Length)) { - if (name == nullptr) - name = _reciprocalEntry(*reciprocal).Name; - _reciprocalLowering.EmitOutOfLine(body, *reciprocal); - } else if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdpid()) { - if (name == nullptr) - name = kRdpid.Name; - body.Raw(_rdpidReplacement(instr)); - } else if (_target == Amd64OnlyTarget::Rosetta && instr.IsCpuid()) { - if (name == nullptr) - name = kCpuid.Name; - body.Raw(RosettaCpuid::StubBody()); - } else { + const auto emitted = _emitOutOfLine(body, instr); + if (emitted == nullptr) return std::nullopt; - } + if (name == nullptr) + name = emitted; body.Advance(instr.Length); } _moveTrailing(body, trailing); diff --git a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp index a1b6e44b617..e68805215ff 100644 --- a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp +++ b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace Codegen { @@ -150,7 +151,7 @@ void StubBodyBuilder::Load(const std::uint8_t reg, const MemoryOperand& operand) if (operand.Rm == ModRmRmSibPresent) _bytes.push_back(operand.Sib); if (operand.RipRelative) { - _relocations.push_back({_bytes.size(), _bytes.size() + Disp32Size, static_cast(_siteOffset + operand.InstructionLength) + operand.Displacement}); + _relocations.push_back({_bytes.size(), _bytes.size() + Disp32Size, _siteOffset + static_cast(operand.InstructionLength) + operand.Displacement}); _bytes.insert(_bytes.end(), Disp32Size, 0); return; } @@ -189,13 +190,82 @@ void StubBodyBuilder::Move(const std::span instruction, cons value |= static_cast(instruction[*ripDisplacementOffset + index]) << (index * 8); _bytes[start + *ripDisplacementOffset + index] = 0; } - _relocations.push_back({start + *ripDisplacementOffset, start + instruction.size(), static_cast(_siteOffset + instruction.size()) + static_cast(value)}); + _relocations.push_back({start + *ripDisplacementOffset, start + instruction.size(), _siteOffset + static_cast(instruction.size()) + static_cast(value)}); } Advance(instruction.size()); } +void StubBodyBuilder::RipRelative(const std::span instruction, const std::size_t displacementOffset, const std::int64_t target) { + using namespace X64OpcodeConstants; + if (displacementOffset > instruction.size() || instruction.size() - displacementOffset < Disp32Size) + throw CodegenException("RIP-relative displacement lies outside its instruction"); + const auto start = _bytes.size(); + Raw(instruction); + std::fill_n(_bytes.begin() + static_cast(start + displacementOffset), Disp32Size, std::uint8_t{0}); + _relocations.push_back({start + displacementOffset, start + instruction.size(), _siteOffset + target}); +} + +void StubBodyBuilder::MoveControlFlow(const std::span instruction, const DecodedInstructionInfo& info) { + using namespace X64OpcodeConstants; + const auto end = _siteOffset + static_cast(instruction.size()); + if (info.FlowKind == ControlFlowKind::IndirectCall) + throw CodegenException("A register or memory call cannot move into a stub"); + if (info.FlowKind == ControlFlowKind::Call && info.HasRipRelativeDisp) { + _pushReturnAddress(end); + std::vector jump(instruction.begin(), instruction.end()); + auto& modrm = jump[info.RipRelativeDispOffset - 1]; + modrm = static_cast((modrm & ~(ModRmRegMask << ModRmRegShift)) | (4u << ModRmRegShift)); + Move(jump, info.RipRelativeDispOffset); + return; + } + if (!info.HasBranchTarget || info.HasRipRelativeDisp) { + Move(instruction, info.HasRipRelativeDisp ? std::optional(info.RipRelativeDispOffset) : std::nullopt); + return; + } + for (std::size_t index = 0; index < info.OpcodeOffset; ++index) { + const auto prefix = instruction[index]; + if (prefix != PrefixSegCs && prefix != PrefixSegDs && prefix != PrefixRepne && prefix != PrefixAddressSize && (prefix & 0xF0) != kRexBase) + throw CodegenException("A relative branch with this prefix cannot move into a stub"); + } + const auto target = end + info.BranchDisp; + if (info.IsTwoByteOpcode) { + _rel32({TwoByteOpcodeEscape, info.Opcode}, target); + } else if (info.Opcode == OneByteCallRel32) { + _pushReturnAddress(end); + _rel32({OneByteJmpRel32}, target); + } else if (info.Opcode == OneByteJmpRel32 || info.Opcode == OneByteJmpRel8) { + _rel32({OneByteJmpRel32}, target); + } else if (info.Opcode >= OneByteJccRel8Min && info.Opcode <= OneByteJccRel8Max) { + _rel32({TwoByteOpcodeEscape, static_cast(TwoByteJccRel32Min | (info.Opcode & 0x0F))}, target); + } else { + throw CodegenException("A LOOP or JRCXZ branch cannot move into a stub"); + } + Advance(instruction.size()); +} + +void StubBodyBuilder::JumpTo(const std::int64_t siteTarget) { + _rel32({X64OpcodeConstants::OneByteJmpRel32}, siteTarget); +} + +void StubBodyBuilder::Seek(const std::int64_t siteOffset) { + _siteOffset = siteOffset; +} + void StubBodyBuilder::Advance(const std::size_t length) { - _siteOffset += length; + _siteOffset += static_cast(length); +} + +void StubBodyBuilder::_rel32(const std::initializer_list opcode, const std::int64_t siteTarget) { + _bytes.insert(_bytes.end(), opcode); + const auto displacement = _bytes.size(); + _bytes.insert(_bytes.end(), X64OpcodeConstants::Disp32Size, std::uint8_t{0}); + _relocations.push_back({displacement, _bytes.size(), siteTarget}); +} + +void StubBodyBuilder::_pushReturnAddress(const std::int64_t siteTarget) { + _bytes.insert(_bytes.end(), {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50}); + _rel32({0x48, 0x8D, 0x05}, siteTarget); + _bytes.insert(_bytes.end(), {0x48, 0x89, 0x44, 0x24, 0x08, 0x58}); } LoweredBody StubBodyBuilder::Finish() { diff --git a/core/relinker/codegen/src/x86/WideStoreLowering.cpp b/core/relinker/codegen/src/x86/WideStoreLowering.cpp new file mode 100644 index 00000000000..40e22c9a376 --- /dev/null +++ b/core/relinker/codegen/src/x86/WideStoreLowering.cpp @@ -0,0 +1,21 @@ +#include + +namespace Codegen { + +namespace { + +void _emitHalf(StubBodyBuilder& body, const WideStoreHalf& half) { + if (half.RipDisplacementOffset) + body.RipRelative(half.Bytes, *half.RipDisplacementOffset, half.RipTarget); + else + body.Raw(half.Bytes); +} + +} + +void WideStoreLowering::EmitOutOfLine(StubBodyBuilder& body, const WideStoreOperands& operands) const { + _emitHalf(body, operands.Low); + _emitHalf(body, operands.High); +} + +} diff --git a/core/relinker/codegen/src/x86/WideStoreOperands.cpp b/core/relinker/codegen/src/x86/WideStoreOperands.cpp new file mode 100644 index 00000000000..478c4108edb --- /dev/null +++ b/core/relinker/codegen/src/x86/WideStoreOperands.cpp @@ -0,0 +1,133 @@ +#include +#include +#include +#include + +namespace Codegen { + +namespace { + +constexpr std::uint8_t kVexMap0F = 0x01; +constexpr std::uint8_t kVexLength256 = 0x04; +constexpr std::uint8_t kVexUnusedRegister = 0x0F; +constexpr std::uint8_t kPpNone = 0x00; +constexpr std::uint8_t kPp66 = 0x01; +constexpr std::uint8_t kPpF3 = 0x02; +constexpr std::uint8_t kVextractf128 = 0x19; +constexpr std::uint8_t kVextractf128Payload = 0x7D; +constexpr std::uint8_t kUpperHalf = 0x01; +constexpr std::int64_t kHalfBytes = 16; + +bool _unalignedStore(const std::uint8_t pp, const std::uint8_t opcode) { + return ((pp == kPpNone || pp == kPp66) && opcode == 0x11) || (pp == kPpF3 && opcode == 0x7F); +} + +bool _legacyPrefixAllowed(const std::uint8_t prefix) { + using namespace X64OpcodeConstants; + return prefix == PrefixSegCs || prefix == PrefixSegSs || prefix == PrefixSegDs || prefix == PrefixSegEs || prefix == PrefixSegFs || prefix == PrefixSegGs || prefix == PrefixAddressSize; +} + +void _appendU32(std::vector& bytes, const std::uint32_t value) { + for (std::size_t index = 0; index < X64OpcodeConstants::Disp32Size; ++index) + bytes.push_back(static_cast(value >> (index * 8))); +} + +} + +std::optional DecodeVexWideStore(const std::uint8_t* data, const std::size_t length) { + using namespace X64OpcodeConstants; + std::size_t pos = 0; + while (pos < length && _legacyPrefixAllowed(data[pos])) + ++pos; + const auto vex = pos; + if (vex + 2 > length) + return std::nullopt; + std::uint8_t inverted = 0; + std::uint8_t payload = 0; + if (data[vex] == OneByteVex2) { + inverted = static_cast((data[vex + 1] & 0x80) | 0x60); + payload = data[vex + 1]; + pos = vex + Vex2PrefixLength; + } else if (data[vex] == OneByteVex3) { + if (vex + Vex3PrefixLength > length || (data[vex + 1] & Vex3MapMask) != kVexMap0F) + return std::nullopt; + inverted = static_cast(data[vex + 1] & 0xE0); + payload = data[vex + 2]; + pos = vex + Vex3PrefixLength; + } else { + return std::nullopt; + } + if ((payload & kVexLength256) == 0 || ((payload >> 3) & 0x0F) != kVexUnusedRegister) + return std::nullopt; + if (pos + 2 > length || !_unalignedStore(static_cast(payload & 0x03), data[pos])) + return std::nullopt; + const auto modRmOffset = pos + 1; + const auto modrm = data[modRmOffset]; + const auto mod = static_cast((modrm >> ModRmModShift) & ModRmModMask); + const auto rm = static_cast(modrm & ModRmRmMask); + if (mod == ModRmModRegister) + return std::nullopt; + auto cursor = modRmOffset + 1; + std::optional sib; + if (rm == ModRmRmSibPresent) { + if (cursor >= length) + return std::nullopt; + sib = data[cursor++]; + } + const bool ripRelative = mod == ModRmModIndirect && rm == ModRmRmRipRelative; + const bool absolute = mod == ModRmModIndirect && sib.has_value() && (*sib & SibBaseMask) == SibBaseDisp32; + const std::size_t displacementSize = mod == ModRmModDisp8 ? Disp8Size : (mod == ModRmModDisp32 || ripRelative || absolute) ? Disp32Size : 0; + if (cursor + displacementSize != length) + return std::nullopt; + std::int64_t displacement = 0; + if (displacementSize == Disp8Size) { + displacement = static_cast(data[cursor]); + } else if (displacementSize == Disp32Size) { + std::uint32_t value = 0; + for (std::size_t index = 0; index < Disp32Size; ++index) + value |= static_cast(data[cursor + index]) << (index * 8); + displacement = static_cast(value); + } + + WideStoreOperands operands; + operands.Low.Bytes.assign(data, data + length); + operands.Low.Bytes[data[vex] == OneByteVex2 ? vex + 1 : vex + 2] &= static_cast(~kVexLength256); + if (ripRelative) { + operands.Low.RipDisplacementOffset = cursor; + operands.Low.RipTarget = static_cast(length) + displacement; + } + + auto& high = operands.High.Bytes; + high.assign(data, data + vex); + high.insert(high.end(), {OneByteVex3, static_cast(inverted | Vex3Map0F3A), kVextractf128Payload, kVextractf128}); + const auto reg = static_cast(modrm & (ModRmRegMask << ModRmRegShift)); + std::int64_t highDisplacement = displacement + kHalfBytes; + std::uint8_t highMod = mod; + std::size_t highDisplacementSize = displacementSize; + if (!ripRelative && !absolute) { + if (mod == ModRmModIndirect || (mod == ModRmModDisp8 && highDisplacement > std::numeric_limits::max())) { + highMod = highDisplacement > std::numeric_limits::max() ? ModRmModDisp32 : ModRmModDisp8; + highDisplacementSize = highMod == ModRmModDisp8 ? Disp8Size : Disp32Size; + } + } + if (!ripRelative && highDisplacement > std::numeric_limits::max()) + throw CodegenException("256-bit store displacement leaves no room for its upper half"); + high.push_back(static_cast((highMod << ModRmModShift) | reg | rm)); + if (sib) + high.push_back(*sib); + const auto highDisplacementOffset = high.size(); + if (highDisplacementSize == Disp8Size) + high.push_back(static_cast(static_cast(highDisplacement))); + else if (ripRelative) + _appendU32(high, 0); + else if (highDisplacementSize == Disp32Size) + _appendU32(high, static_cast(static_cast(highDisplacement))); + high.push_back(kUpperHalf); + if (ripRelative) { + operands.High.RipDisplacementOffset = highDisplacementOffset; + operands.High.RipTarget = static_cast(length) + displacement + kHalfBytes; + } + return operands; +} + +} diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index b894332bdf0..f905d2df437 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -242,7 +242,7 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t hasModRm = true; } else if (opcode >= TwoByteJccRel32Min && opcode <= TwoByteJccRel32Max) { immediateSize = ImmSize32; - } else if (opcode == TwoByteMovImm8ModRm) { + } else if (opcode == TwoByteMovImm8ModRm || opcode == TwoByte3DNow) { hasModRm = true; immediateSize = ImmSize8; } else if (opcode >= TwoByteShiftImm8Min && opcode <= TwoByteShiftImm8Max) { @@ -369,6 +369,8 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( b == PrefixOperandSize || b == PrefixAddressSize) { if (b >= PrefixSegFs && b <= PrefixSegGs) info.SegmentPrefix = b; + if (b == PrefixAddressSize) + info.AddressSize32 = true; info.RexPrefix = 0; ++pos; continue; @@ -556,7 +558,7 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( op == TwoByteImulRmModRm || op == TwoByteGrp7 || op == TwoByteGrp15 || op == TwoByteXadd || op == TwoByteGrp9 || op == TwoByteNopModRm || - op == TwoByteEndbr || op == TwoByteMovImm8ModRm || + op == TwoByteEndbr || op == TwoByteMovImm8ModRm || op == TwoByte3DNow || op == ThreeByteEscape38 || op == ThreeByteEscape3A || (op >= TwoByteShiftImm8Min && op <= TwoByteShiftImm8Max) || op == TwoByteShldImm8 || op == TwoByteShrdImm8 || diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 719788893ef..8c4e4e00907 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -670,6 +671,258 @@ void rosettaSubstitutions() { require(rdpid->ReturnBranchOffset == 6 && Bytes(rdpid->StubBody.begin(), rdpid->StubBody.begin() + 7) == Bytes{0x41, 0xBB, 0, 0, 0, 0, 0xE9}, "RDPID r11 stub does not load 0 before returning"); } +void wideStoreSplit() { + struct Case { + Bytes Original; + Bytes Low; + Bytes High; + const char* Name; + }; + const std::vector cases = { + {{0xC5, 0xFC, 0x11, 0x07}, {0xC5, 0xF8, 0x11, 0x07}, {0xC4, 0xE3, 0x7D, 0x19, 0x47, 0x10, 0x01}, "vmovups (%rdi)"}, + {{0xC4, 0x41, 0x7E, 0x7F, 0x4C, 0x87, 0x70}, {0xC4, 0x41, 0x7A, 0x7F, 0x4C, 0x87, 0x70}, {0xC4, 0x43, 0x7D, 0x19, 0x8C, 0x87, 0x80, 0x00, 0x00, 0x00, 0x01}, "vmovdqu 0x70(%r15,%rax,4)"}, + {{0xC5, 0xFC, 0x11, 0x4C, 0x24, 0xF8}, {0xC5, 0xF8, 0x11, 0x4C, 0x24, 0xF8}, {0xC4, 0xE3, 0x7D, 0x19, 0x4C, 0x24, 0x08, 0x01}, "vmovups -8(%rsp)"}, + {{0xC5, 0xFE, 0x7F, 0x93, 0x78, 0x56, 0x34, 0x12}, {0xC5, 0xFA, 0x7F, 0x93, 0x78, 0x56, 0x34, 0x12}, {0xC4, 0xE3, 0x7D, 0x19, 0x93, 0x88, 0x56, 0x34, 0x12, 0x01}, "vmovdqu 0x12345678(%rbx)"}, + {{0x64, 0xC5, 0xFC, 0x11, 0x04, 0x25, 0x40, 0x00, 0x00, 0x00}, {0x64, 0xC5, 0xF8, 0x11, 0x04, 0x25, 0x40, 0x00, 0x00, 0x00}, {0x64, 0xC4, 0xE3, 0x7D, 0x19, 0x04, 0x25, 0x50, 0x00, 0x00, 0x00, 0x01}, "vmovups %fs:0x40"}, + {{0xC4, 0x41, 0x7C, 0x11, 0x65, 0x00}, {0xC4, 0x41, 0x78, 0x11, 0x65, 0x00}, {0xC4, 0x43, 0x7D, 0x19, 0x65, 0x10, 0x01}, "vmovups (%r13)"}, + {{0xC4, 0xC1, 0x7C, 0x11, 0x47, 0x68}, {0xC4, 0xC1, 0x78, 0x11, 0x47, 0x68}, {0xC4, 0xC3, 0x7D, 0x19, 0x47, 0x78, 0x01}, "vmovups 0x68(%r15)"}}; + for (const auto& item : cases) { + const auto split = Codegen::DecodeVexWideStore(item.Original.data(), item.Original.size()); + require(split && split->Low.Bytes == item.Low && split->High.Bytes == item.High && !split->Low.RipDisplacementOffset && !split->High.RipDisplacementOffset, (std::string("256-bit store was not split into its 128-bit halves: ") + item.Name).c_str()); + } + const Bytes rip = {0xC5, 0xFD, 0x11, 0x1D, 0x00, 0x01, 0x00, 0x00}; + const auto ripSplit = Codegen::DecodeVexWideStore(rip.data(), rip.size()); + require(ripSplit && ripSplit->Low.Bytes == Bytes{0xC5, 0xF9, 0x11, 0x1D, 0x00, 0x01, 0x00, 0x00} && ripSplit->High.Bytes == Bytes{0xC4, 0xE3, 0x7D, 0x19, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x01}, "RIP-relative 256-bit store was not split"); + require(ripSplit->Low.RipDisplacementOffset == 4 && ripSplit->Low.RipTarget == 8 + 0x100 && ripSplit->High.RipDisplacementOffset == 5 && ripSplit->High.RipTarget == 8 + 0x110, "RIP-relative halves do not address the original operand and the one 16 bytes above it"); + for (const auto& other : {Bytes{0xC5, 0xF8, 0x11, 0x07}, Bytes{0xC5, 0xFC, 0x11, 0xC1}, Bytes{0xC5, 0xFC, 0x10, 0x07}, Bytes{0xC4, 0xE2, 0x75, 0x2E, 0x07}, Bytes{0xC5, 0xF4, 0x11, 0x07}, Bytes{0xC4, 0xE3, 0x7D, 0x19, 0x07, 0x01}, Bytes{0xC5, 0xFC, 0x11, 0x04}, Bytes{0xC5, 0xFC, 0x11, 0x47}}) + require(!Codegen::DecodeVexWideStore(other.data(), other.size()).has_value(), "An instruction other than a plain 256-bit store was split"); + for (const auto& aligned : {Bytes{0xC5, 0xFC, 0x29, 0x4C, 0x24, 0xF8}, Bytes{0xC5, 0xFD, 0xE7, 0x93, 0x78, 0x56, 0x34, 0x12}, Bytes{0xC5, 0xFD, 0x7F, 0x1F}, Bytes{0xC5, 0xFC, 0x2B, 0x27}}) + require(!Codegen::DecodeVexWideStore(aligned.data(), aligned.size()).has_value(), "An aligned 256-bit store, which cannot cross a page, was split"); + const Bytes farRip = {0xC5, 0xFC, 0x11, 0x05, 0xF8, 0xFF, 0xFF, 0x7F}; + const auto farRipSplit = Codegen::DecodeVexWideStore(farRip.data(), farRip.size()); + require(farRipSplit && farRipSplit->High.RipTarget == std::int64_t{8} + 0x7FFFFFF8 + 0x10, "A RIP-relative 256-bit store near the end of its displacement range was not split"); + const Bytes farBase = {0xC5, 0xFC, 0x11, 0x80, 0xF8, 0xFF, 0xFF, 0x7F}; + requireFailure([&] { (void)Codegen::DecodeVexWideStore(farBase.data(), farBase.size()); }, "A 256-bit store whose upper half has no 32-bit displacement was kept"); + requireFailure([] { Codegen::StubBodyBuilder body; body.RipRelative(Bytes{0x90}, 0, 0); }, "A RIP-relative displacement outside its instruction was relocated"); + const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes store = {0xC4, 0xC1, 0x7C, 0x11, 0x47, 0x68}; + const auto lowered = rosetta->Match(store.data(), store.size()); + require(lowered && lowered->Lowering == Codegen::Amd64OnlyLowering::Trampoline && !lowered->Optional && lowered->RelocatableWindow && lowered->InstructionName == "256-bit store", "A 256-bit store was not lowered through a required stub that may move its neighbours for Rosetta"); + Bytes expectedBody = cases.back().Low; + expectedBody.insert(expectedBody.end(), cases.back().High.begin(), cases.back().High.end()); + require(Bytes(lowered->StubBody.begin(), lowered->StubBody.begin() + static_cast(lowered->ReturnBranchOffset)) == expectedBody, "The 256-bit store stub is not the two halves"); + const std::vector> pair = {std::span(cases[0].Original), std::span(rip)}; + const auto sequence = rosetta->MatchSequence(pair, {}); + require(sequence && sequence->Relocations.size() == 2 && sequence->Relocations[0].SiteTarget == 4 + 8 + 0x100 && sequence->Relocations[1].SiteTarget == 4 + 8 + 0x110, "A RIP-relative 256-bit store after another is not relocated from its place in the site"); + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->Match(store.data(), store.size()).has_value(), "A 256-bit store was split for an Intel host"); + const std::vector> single = {std::span(store)}; + require(!Codegen::MakeAmd64OnlyInstructionMatcher()->MatchSequence(single, {}).has_value(), "A 256-bit store was split in a sequence for an Intel host"); +} + +void converterWideStoreWindows() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes store = {0xC5, 0xFC, 0x11, 0x07}; + Bytes halves = {0xC5, 0xF8, 0x11, 0x07, 0xC4, 0xE3, 0x7D, 0x19, 0x47, 0x10, 0x01}; + const auto convert = [&](const Bytes& text) { + Bytes file(0x300 + text.size(), 0xCC); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + return converter->Convert(file, {segmentHeader(text.size())}); + }; + const auto body = [](const Codegen::TrampolineSite& site) { + return Bytes(site.Body.begin(), site.Body.begin() + static_cast(site.ReturnBranchOffset)); + }; + const auto withStore = [&](Bytes before, const Bytes& after) { + before.insert(before.end(), store.begin(), store.end()); + before.insert(before.end(), after.begin(), after.end()); + return before; + }; + + const auto returned = convert(withStore({}, {0xC3})); + Bytes expected = halves; + expected.push_back(0xC3); + require(returned.Trampolines.size() == 1 && returned.KeptCount == 0 && returned.Reports.size() == 1 && returned.Reports[0].InstructionName == "256-bit store", "A 256-bit store followed by a return was not moved"); + require(returned.Trampolines[0].Offset == 0x200 && returned.Trampolines[0].Length == 5 && body(returned.Trampolines[0]) == expected, "A 256-bit store followed by a return did not end its stub with the return"); + + const auto called = convert(withStore({}, {0xE8, 0x10, 0x00, 0x00, 0x00})); + require(called.Trampolines.size() == 1 && called.Trampolines[0].Offset == 0x200 && called.Trampolines[0].Length == 9, "A 256-bit store followed by a call did not take the call"); + const auto& call = called.Trampolines[0]; + expected = halves; + expected.insert(expected.end(), {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50, 0x48, 0x8D, 0x05, 0, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x08, 0x58, 0xE9, 0, 0, 0, 0}); + require(body(call) == expected, "A moved call does not push its original return address and jump"); + require(call.Relocations.size() == 2 && call.Relocations[0].DisplacementOffset == halves.size() + 9 && call.Relocations[0].SiteTarget == 9 && call.Relocations[1].DisplacementOffset == halves.size() + 20 && call.Relocations[1].SiteTarget == 9 + 0x10, "A moved call returns to or calls the wrong address"); + + const auto branched = convert(withStore({}, {0x75, 0x10, 0x90})); + require(branched.Trampolines.size() == 1 && branched.Trampolines[0].Length == 6, "A 256-bit store followed by a short conditional branch did not take the branch"); + const auto& jcc = branched.Trampolines[0]; + expected = halves; + expected.insert(expected.end(), {0x0F, 0x85, 0, 0, 0, 0}); + require(body(jcc) == expected && jcc.Relocations.size() == 1 && jcc.Relocations[0].SiteTarget == 6 + 0x10, "A moved short conditional branch was not re-encoded with a 32-bit displacement to its target"); + + const auto jumped = convert(withStore({}, {0xEB, 0x10, 0x90})); + expected = halves; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(jumped.Trampolines.size() == 1 && body(jumped.Trampolines[0]) == expected && jumped.Trampolines[0].Relocations.size() == 1 && jumped.Trampolines[0].Relocations[0].SiteTarget == 6 + 0x10, "A moved short jump was not re-encoded with a 32-bit displacement to its target"); + + const auto longBranched = convert(withStore({}, {0x0F, 0x85, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), {0x0F, 0x85, 0, 0, 0, 0}); + require(longBranched.Trampolines.size() == 1 && longBranched.Trampolines[0].Length == 10 && body(longBranched.Trampolines[0]) == expected && longBranched.Trampolines[0].Relocations.size() == 1 && longBranched.Trampolines[0].Relocations[0].SiteTarget == 10 + 0x10, "A moved conditional branch with a 32-bit displacement does not reach its target"); + + const auto entered = convert(withStore({0x48, 0x83, 0xC1, 0x01}, {0xC3, 0xEB, 0xFD})); + expected = {0x48, 0x83, 0xC1, 0x01}; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(entered.Trampolines.size() == 1 && entered.Trampolines[0].Offset == 0x200 && entered.Trampolines[0].Length == 8 && body(entered.Trampolines[0]) == expected, "A 256-bit store before a branch target did not take room from the instruction before it"); + + Bytes adds; + for (int index = 0; index < 4; ++index) + adds.insert(adds.end(), {0x48, 0x83, 0xC1, 0x01}); + Bytes after = {0xC3, 0xEB, 0xFD}; + after.insert(after.end(), adds.begin(), adds.end()); + const auto island = convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, after)); + require(island.Trampolines.size() == 2 && island.KeptCount == 0, "A 256-bit store with no movable neighbours was not lowered through an island"); + const auto& moved = island.Trampolines[0]; + const auto& lowered = island.Trampolines[1]; + expected = adds; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(moved.Offset == 0x20C && moved.Length == 8 && body(moved) == expected && moved.Relocations.size() == 1 && moved.Relocations[0].SiteTarget == 16, "The instructions under the island were not moved and continued after it"); + expected = halves; + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(lowered.Offset == 0x214 && lowered.Length == 5 && body(lowered) == expected && lowered.Relocations.size() == 1 && lowered.Relocations[0].SiteTarget == -11, "The island does not store both halves and return after the original store"); + require(Bytes(island.Bytes.begin() + 0x205, island.Bytes.begin() + 0x209) == Bytes{0xEB, 0x0D, 0xCC, 0xCC}, "The 256-bit store was not replaced by a short jump to its island"); + + requireFailure([&] { (void)convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, {0xC3, 0xEB, 0xFD})); }, "A 256-bit store with no room for a stub jump was kept"); + + const auto paired = convert(withStore({}, {0xC5, 0xFC, 0x11, 0x0E, 0xC3})); + expected = halves; + expected.insert(expected.end(), {0xC5, 0xF8, 0x11, 0x0E, 0xC4, 0xE3, 0x7D, 0x19, 0x4E, 0x10, 0x01}); + require(paired.Trampolines.size() == 1 && paired.Trampolines[0].Length == 8 && body(paired.Trampolines[0]) == expected && paired.Reports.size() == 1, "A 256-bit store followed by another was not lowered with it in one stub"); + + const Bytes push = {0x48, 0x8D, 0x64, 0x24, 0xF8, 0x50, 0x48, 0x8D, 0x05, 0, 0, 0, 0, 0x48, 0x89, 0x44, 0x24, 0x08, 0x58}; + const auto ripCalled = convert(withStore({}, {0xFF, 0x15, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), push.begin(), push.end()); + expected.insert(expected.end(), {0xFF, 0x25, 0, 0, 0, 0}); + const auto& ripCall = ripCalled.Trampolines[0]; + require(ripCalled.Trampolines.size() == 1 && ripCall.Length == 10 && body(ripCall) == expected, "A moved call through a RIP-relative pointer does not push its original return address and jump through the pointer"); + require(ripCall.Relocations.size() == 2 && ripCall.Relocations[0].SiteTarget == 10 && ripCall.Relocations[1].DisplacementOffset == halves.size() + 21 && ripCall.Relocations[1].SiteTarget == 10 + 0x10, "A moved call through a RIP-relative pointer returns to or reads the wrong address"); + + const auto ripJumped = convert(withStore({}, {0xFF, 0x25, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), {0xFF, 0x25, 0, 0, 0, 0}); + require(ripJumped.Trampolines.size() == 1 && body(ripJumped.Trampolines[0]) == expected && ripJumped.Trampolines[0].Relocations.size() == 1 && ripJumped.Trampolines[0].Relocations[0].SiteTarget == 10 + 0x10, "A moved jump through a RIP-relative pointer reads the wrong address"); + + const auto relaxed = convert(withStore({}, {0x67, 0xE8, 0x10, 0x00, 0x00, 0x00})); + expected = halves; + expected.insert(expected.end(), push.begin(), push.end()); + expected.insert(expected.end(), {0xE9, 0, 0, 0, 0}); + require(relaxed.Trampolines.size() == 1 && relaxed.Trampolines[0].Length == 10 && body(relaxed.Trampolines[0]) == expected && relaxed.Trampolines[0].Relocations.size() == 2 && relaxed.Trampolines[0].Relocations[1].SiteTarget == 10 + 0x10, "A call with an address-size prefix was not moved like a call"); + + const Bytes add = {0x48, 0x83, 0xC1, 0x01}; + for (const auto& follower : {Bytes{0xFF, 0xD0}, Bytes{0xE3, 0x10}, Bytes{0x66, 0x75, 0x10}, Bytes{0x66, 0x0F, 0xAE, 0x37}}) { + const auto preceded = convert(withStore(add, follower)); + expected = add; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(!preceded.Trampolines.empty() && preceded.Trampolines[0].Offset == 0x200 && preceded.Trampolines[0].Length == 8 && body(preceded.Trampolines[0]) == expected, "A 256-bit store moved a following instruction that cannot leave its place instead of the one before it"); + } + + Bytes throwing = {0xC3, 0xEB, 0xFD}; + for (int index = 0; index < 3; ++index) + throwing.insert(throwing.end(), add.begin(), add.end()); + throwing.insert(throwing.end(), {0xFF, 0xD0}); + throwing.insert(throwing.end(), adds.begin(), adds.end()); + const auto farther = convert(withStore({0xE8, 0x00, 0x00, 0x00, 0x00}, throwing)); + require(farther.Trampolines.size() == 2 && farther.Trampolines[0].Offset == 0x21A && farther.Trampolines[1].Offset == 0x222, "An island that would move a register call was not passed over for the next one"); + require(Bytes(farther.Bytes.begin() + 0x205, farther.Bytes.begin() + 0x207) == Bytes{0xEB, 0x1B}, "The 256-bit store does not jump to the island after the register call"); + + Bytes reentered = {0xFF, 0xD0, 0xEB, 0xF8}; + reentered.insert(reentered.end(), adds.begin(), adds.end()); + const auto beyondStore = convert(withStore(Bytes(adds.begin(), adds.begin() + 8), reentered)); + require(beyondStore.Trampolines.size() == 2 && beyondStore.Trampolines[0].Offset == 0x210 && beyondStore.Trampolines[1].Offset == 0x218, "An island before a 256-bit store that is a branch target took the store with it"); + require(Bytes(beyondStore.Bytes.begin() + 0x208, beyondStore.Bytes.begin() + 0x20A) == Bytes{0xEB, 0x0E}, "The 256-bit store that is a branch target does not jump to the island after it"); + + const Bytes farCall = {0xE8, 0x40, 0x00, 0x00, 0x00}; + Bytes claimedFirst = {0xFF, 0xD0}; + claimedFirst.insert(claimedFirst.end(), adds.begin(), adds.end()); + const auto afterSequence = convert(withStore({0xC5, 0xF8, 0x53, 0xC1, 0x48, 0x83, 0xC1, 0x01}, claimedFirst)); + require(afterSequence.Trampolines.size() == 3 && afterSequence.Trampolines[0].Offset == 0x200 && afterSequence.Trampolines[0].Length == 8 && afterSequence.Trampolines[1].Offset == 0x20E && afterSequence.Trampolines[2].Offset == 0x216, "A 256-bit store moved an instruction already moved into the stub of the instruction before it"); + + Bytes pendingRun = {0xFF, 0xD0, 0x48, 0x83, 0xC1, 0x01, 0xC5, 0xFC, 0x11, 0x47, 0x10}; + pendingRun.insert(pendingRun.end(), adds.begin(), adds.end()); + const auto pendingIsland = convert(withStore(farCall, pendingRun)); + require(pendingIsland.Trampolines.size() == 3 && pendingIsland.Trampolines[0].Offset == 0x214 && pendingIsland.Trampolines[1].Offset == 0x21C && pendingIsland.Trampolines[2].Offset == 0x20F && pendingIsland.Trampolines[2].Length == 5, "An island moved the call before a 256-bit store or another store that still needs its own stub"); + + Bytes enteredRun = {0xFF, 0xD0}; + for (int index = 0; index < 6; ++index) + enteredRun.insert(enteredRun.end(), add.begin(), add.end()); + enteredRun.insert(enteredRun.end(), {0xEB, 0xEE}); + const auto enteredIsland = convert(withStore(farCall, enteredRun)); + require(enteredIsland.Trampolines.size() == 2 && enteredIsland.Trampolines[0].Offset == 0x213 && enteredIsland.Trampolines[1].Offset == 0x21B, "An island was placed over a branch target"); + + Bytes returning = {0xFF, 0xD0}; + for (int index = 0; index < 3; ++index) + returning.insert(returning.end(), add.begin(), add.end()); + returning.push_back(0xC3); + const auto returnIsland = convert(withStore(farCall, returning)); + expected = Bytes(adds.begin(), adds.begin() + 12); + expected.push_back(0xC3); + require(returnIsland.Trampolines.size() == 2 && returnIsland.Trampolines[0].Offset == 0x20B && body(returnIsland.Trampolines[0]) == expected, "Instructions moved for an island that end in a return jump back after it"); + + const auto afterBranch = convert(withStore({0x75, 0x10}, {0xFF, 0xD0})); + expected = {0x0F, 0x85, 0, 0, 0, 0}; + expected.insert(expected.end(), halves.begin(), halves.end()); + require(afterBranch.Trampolines.size() == 1 && afterBranch.Trampolines[0].Offset == 0x200 && afterBranch.Trampolines[0].Length == 6 && body(afterBranch.Trampolines[0]) == expected && afterBranch.Trampolines[0].Relocations[0].SiteTarget == 2 + 0x10, "A 256-bit store after a conditional branch did not take the branch"); + + Bytes far = adds; + far.resize(8); + for (int index = 0; index < 60; ++index) + far.insert(far.end(), {0xFF, 0xD0}); + far.insert(far.end(), adds.begin(), adds.end()); + for (int index = 0; index < 70; ++index) + far.insert(far.end(), {0xFF, 0xD0}); + Bytes tail; + for (int index = 0; index < 11; ++index) + tail.insert(tail.end(), {0xFF, 0xD0}); + tail.insert(tail.end(), {0x90, 0x90}); + requireFailure([&] { (void)convert(withStore(far, tail)); }, "A 256-bit store whose movable neighbours are all beyond a short jump was kept"); +} + +void converterJumpTableTargets() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes leaTable = {0x48, 0x8D, 0x0D, 0xF9, 0x1F, 0x00, 0x00}; + const Bytes dispatch = {0x48, 0x63, 0x04, 0x81, 0x48, 0x01, 0xC8, 0xFF, 0xE0, 0x48, 0x83, 0xC1, 0x01, 0xC5, 0xFC, 0x11, 0x07, 0x48, 0x83, 0xC2, 0x01, 0xC3}; + const Bytes store = {0xC5, 0xFC, 0x11, 0x07}; + const Domain::ProgramHeader code{1, 5, 0x200, 0x1000, 0, 29, 29, 16}; + const Domain::ProgramHeader data{1, 4, 0x300, 0x3000, 0, 0x40, 0x40, 16}; + const auto table = [](const std::uint64_t base, const std::vector& targets) { + Bytes bytes; + for (const auto target : targets) { + const auto relative = static_cast(target - base); + for (std::size_t byte = 0; byte < 4; ++byte) + bytes.push_back(static_cast(relative >> (byte * 8))); + } + return bytes; + }; + const auto convert = [&](const Bytes& first, const std::vector>& placed, const std::vector& headers) { + Bytes file(0x400, 0xCC); + std::copy(first.begin(), first.end(), file.begin() + 0x200); + std::copy(dispatch.begin(), dispatch.end(), file.begin() + 0x207); + for (const auto& [offset, bytes] : placed) + std::copy(bytes.begin(), bytes.end(), file.begin() + static_cast(offset)); + return converter->Convert(file, headers); + }; + const auto placedAt = [](const Codegen::ConvertResult& result, const Domain::FileByteOffset offset) { + return result.Trampolines.size() == 1 && result.Trampolines[0].Offset == offset && result.Trampolines[0].Length == 8; + }; + + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1018})}, {0x330, store}}, {code, data}), 0x210), "A 256-bit store moved the instruction after it although a jump table enters that instruction"); + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1019, 0x1018})}}, {code, data}), 0x214), "A jump table was read past an entry that does not start an instruction"); + require(placedAt(convert({0x48, 0x8B, 0x0D, 0xF9, 0x1F, 0x00, 0x00}, {{0x300, table(0x3000, {0x1010, 0x1018})}}, {code, data}), 0x214), "A jump table was read at an address loaded rather than formed by LEA"); + require(placedAt(convert({0x48, 0x8D, 0x0D, 0xF7, 0x1F, 0x00, 0x00}, {{0x2FE, table(0x2FFE, {0x1018})}}, {code, data}), 0x214), "A jump table was read at an address outside every segment"); + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010})}, {0x380, table(0x3000, {0x1018})}}, {code, data, {0x61000000, 4, 0x380, 0x3000, 0, 0x10, 0x10, 16}}), 0x214), "A jump table was read from a segment that is not loaded"); + require(placedAt(convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1018})}, {0x3A0, store}}, {code, data, {2, 5, 0x3A0, 0x2000, 0, 4, 4, 16}}), 0x210), "A segment that is not loaded was converted"); + requireFailure([&] { (void)convert(leaTable, {{0x300, table(0x3000, {0x1010, 0x1018})}}, {code, {1, 4, 0x300, 0x3000, 0, 0x1000, 0x1000, 16}}); }, "A jump table was read from a segment that exceeds the file"); +} + void converterRosetta() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); Bytes file(0x300, 0xCC); @@ -776,7 +1029,7 @@ void converterReciprocal() { void converterStrayRex() { const auto converter = Codegen::MakeAmd64OnlyConverter(); const auto convert = [&](const Bytes& text) { - Bytes file(0x300, 0xCC); + Bytes file(0x300 + text.size(), 0xCC); std::copy(text.begin(), text.end(), file.begin() + 0x200); return std::pair{file, converter->Convert(file, {segmentHeader(text.size())})}; }; @@ -1419,6 +1672,9 @@ int main() { converterClzero(); rosettaSubstitutions(); converterRosetta(); + wideStoreSplit(); + converterWideStoreWindows(); + converterJumpTableTargets(); converterReciprocal(); converterStrayRex(); rewriterStrayRex(); diff --git a/core/relinker/codegen/tests/DecoderLengthCases.hpp b/core/relinker/codegen/tests/DecoderLengthCases.hpp index 561f3a64e08..10747c9a61b 100644 --- a/core/relinker/codegen/tests/DecoderLengthCases.hpp +++ b/core/relinker/codegen/tests/DecoderLengthCases.hpp @@ -33,6 +33,12 @@ inline constexpr DecoderLengthCase kDecoderLengthCases[] = { {"0f0b", 2}, {"0f0d846511223344", 8}, {"0f0e", 2}, + {"0f0fc196", 4}, + {"0f0f08b7", 4}, + {"0f0f4f80b4", 5}, + {"0f0f846511223344b4", 9}, + {"0f0f0d11223344a6", 8}, + {"410f0f1c24bf", 6}, {"0f10846511223344", 8}, {"0f11846511223344", 8}, {"0f12846511223344", 8}, diff --git a/core/relinker/codegen/tests/DecoderLengthTests.cpp b/core/relinker/codegen/tests/DecoderLengthTests.cpp index d119b3b3200..e141e1539e6 100644 --- a/core/relinker/codegen/tests/DecoderLengthTests.cpp +++ b/core/relinker/codegen/tests/DecoderLengthTests.cpp @@ -77,6 +77,10 @@ int main() { } catch (const Codegen::CodegenException&) { } } + const auto threeDNow = parse("0f0f0d11223344a6"); + const auto threeDNowInfo = decoder.DecodeInstruction(threeDNow.data(), threeDNow.size()); + require(threeDNowInfo.HasModRm && threeDNowInfo.ModRmByte == 0x0d, "3DNow! ModRM was not reported"); + require(threeDNowInfo.HasRipRelativeDisp && threeDNowInfo.RipRelativeDispOffset == 3, "3DNow! RIP-relative displacement was not reported"); std::cout << "Decoder length tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp index ecc49bd3999..37bab4c73e1 100644 --- a/core/relinker/domain/include/domain/ImportModule.hpp +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -3,15 +3,21 @@ #include #include +#include #include #include namespace Domain { +inline constexpr std::array ModuleSuffixes = {".prx", ".sprx", ".suprx", ".debug_prx"}; + +inline std::string_view ModuleSuffix(const std::string_view name) { + const auto suffix = std::find_if(ModuleSuffixes.begin(), ModuleSuffixes.end(), [&](const auto candidate) { return name.ends_with(candidate); }); + return suffix == ModuleSuffixes.end() ? std::string_view{} : *suffix; +} + inline std::string ImportModuleName(std::string name) { - if (name.ends_with(".prx")) name.resize(name.size() - 4); - else if (name.ends_with(".sprx")) name.resize(name.size() - 5); - else if (name.ends_with(".suprx")) name.resize(name.size() - 6); + name.resize(name.size() - ModuleSuffix(name).size()); if (name.ends_with("-module")) name.resize(name.size() - 7); std::replace(name.begin(), name.end(), '.', '_'); return name; @@ -44,7 +50,7 @@ inline std::string ImportModule(const std::string& symbol, const std::map dependencies = {}) { auto name = DeclaredImportModule(symbol, modules); if (name.empty()) return {}; - if (!name.ends_with(".prx") && !name.ends_with(".sprx") && !name.ends_with(".suprx")) name += ".prx"; + if (ModuleSuffix(name).empty()) name += ".prx"; if (std::find(dependencies.begin(), dependencies.end(), name) != dependencies.end()) return name; std::string matched; for (const auto& dependency : dependencies) { diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index 35ec257472f..4e4cad6e5de 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -61,6 +61,7 @@ struct NidReference { FileByteOffset RelocationTableOffset; VirtualAddress RelocationAddress; std::int64_t Addend; + bool Weak = false; }; struct CallSiteInfo { diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index 615c7c7742d..40802530ee0 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -141,6 +141,7 @@ inline constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; inline constexpr std::uint8_t STB_GLOBAL = 1; inline constexpr std::uint8_t STB_WEAK = 2; +inline constexpr std::uint8_t STT_NOTYPE = 0; inline constexpr std::uint8_t STT_FUNC = 2; inline constexpr std::uint8_t STV_DEFAULT = 0; inline constexpr std::uint16_t SHN_UNDEF = 0; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index bef348f4a98..e20f23b1dcc 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -35,6 +35,7 @@ struct PeImport { std::int32_t TargetModule = -1; std::uint32_t RelocationType = 1; std::string Library; + bool Weak = false; }; struct PeRelocations { diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 83281042fdf..dcd704baba8 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -26,6 +26,8 @@ std::string normalizeRunPath(std::string path) { path = "."; if (path.starts_with("$ORIGIN\\")) path.erase(0, 8); + if (path.empty()) + path = "."; if (path.find('$') != std::string::npos) throw Domain::RelinkerException("Unsupported variable in Windows run path"); if (path.back() != '\\') @@ -447,6 +449,21 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto resolved = code.Branch({0x0f, 0x85}); code.Emit({0x48, 0x83, 0xc7, 4, 0xff, 0xcd}); code.Rip({0x0f, 0x85}, search); + if (imports[index].Weak) { + code.Emit({0x31, 0xc0}); + const auto writeWeak = code.GetRva(); + if (imports[index].Addend != 0) { + code.Emit({0x48, 0xba}); + code.U64(imports[index].Addend); + code.Emit({0x48, 0x01, 0xd0}); + } + guestStartup.WriteImport(code, imports[index], handles); + const auto weakDone = code.Branch({0xe9}); + code.PatchBranch(resolved, code.GetRva()); + code.Rip({0xe9}, writeWeak); + code.PatchBranch(weakDone, code.GetRva()); + continue; + } if (lazyBinding) { lazyUnresolvedImports.push_back(index); const auto skipGotWrite = code.Branch({0xe9}); diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index b1f71e3f3de..0d4efd25b1b 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -38,13 +39,18 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom if (Io::ReadU16(dynamicSection.DynSymData, symbolOffset + 6) != 0) throw Domain::RelinkerException("Defined symbols require explicit relocation support", target); const auto name = ReadString(dynamicSection.DynStrData, Io::ReadU32(dynamicSection.DynSymData, symbolOffset)); - if (name.empty()) + if (name.empty() && dynamicSection.DynSymData[symbolOffset + 4] >> 4 != STB_WEAK) throw Domain::RelinkerException("Empty import symbol name", target); if (type != 1 && addend != 0) throw Domain::RelinkerException("GLOB_DAT/JUMP_SLOT relocation has a nonzero addend", target); image.RequireWritable(target, 8); + if (name.empty()) { + image.WritePointer(target, addend); + continue; + } image.WritePointer(target, 0); result.Imports.push_back({name, rva, addend}); + result.Imports.back().Weak = (dynamicSection.DynSymData[symbolOffset + 4] >> 4) == STB_WEAK; if (const auto module = dynamicSection.ImportModules.find(target); module != dynamicSection.ImportModules.end()) result.Imports.back().Library = module->second; } diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index df1ce0fea79..e37c631dccb 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -23,6 +23,8 @@ struct MovedInstruction { std::uint32_t Rva; std::vector Bytes; std::optional RipDisplacement; + std::optional BranchTarget; + std::size_t BranchDisplacementOffset = 0; }; struct TlsAccess { @@ -145,9 +147,28 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (length >= header.FileSize - offset) throw Domain::RelinkerException("Short guest TLS instruction ends its segment", header.Offset + offset); const auto next = decoder.DecodeInstruction(bytes + length, header.FileSize - offset - length); - if (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0) + const bool relativeBranch = + (next.FlowKind == Codegen::ControlFlowKind::ConditionalBranch && (next.IsTwoByteOpcode ? next.Opcode >= 0x80 && next.Opcode <= 0x8f : next.Opcode >= 0x70 && next.Opcode <= 0x7f)) || + (next.FlowKind == Codegen::ControlFlowKind::UnconditionalJump && !next.HasRipRelativeDisp && !next.IsTwoByteOpcode && (next.Opcode == 0xe9 || next.Opcode == 0xeb)); + if (!relativeBranch && (next.FlowKind != Codegen::ControlFlowKind::Sequential || next.HasBranchTarget || next.SegmentPrefix != 0)) throw Domain::RelinkerException("Short guest TLS instruction is followed by an instruction that cannot move", header.Offset + offset + length); - moved.push_back({CheckedRva(rva + length), std::vector(bytes + length, bytes + length + next.Length), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt}); + std::vector movedBytes(bytes + length, bytes + length + next.Length); + std::optional branchTarget; + std::size_t branchDisplacementOffset = 0; + if (relativeBranch) { + const auto target = static_cast(rva) + static_cast(length + next.Length) + next.BranchDisp; + if (target < 0 || target > std::numeric_limits::max()) + throw Domain::RelinkerException("Short guest TLS instruction is followed by a branch beyond the address space", header.Offset + offset + length); + branchTarget = static_cast(target); + if (!next.IsTwoByteOpcode && next.Opcode != 0xe9) { + movedBytes = next.Opcode == 0xeb ? std::vector{0xe9} : std::vector{0x0f, static_cast(0x80 | (next.Opcode & 0xf))}; + branchDisplacementOffset = movedBytes.size(); + movedBytes.insert(movedBytes.end(), 4, 0); + } else { + branchDisplacementOffset = next.Length - 4; + } + } + moved.push_back({CheckedRva(rva + length), std::move(movedBytes), next.HasRipRelativeDisp ? std::optional(next.RipRelativeDispOffset) : std::nullopt, branchTarget, branchDisplacementOffset}); length += next.Length; } accesses.push_back({rva, header.Offset + offset, length, false, 0, addressRegister, 0, isAlu ? bytes[position] : std::uint8_t{0}, *address, wide, std::move(moved)}); @@ -357,6 +378,12 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co throw Domain::RelinkerException("Moved RIP-relative instruction exceeds rel32 range from its TLS stub", access.FileOffset); Io::WriteU32(bytes, *instruction.RipDisplacement, static_cast(static_cast(displacement))); } + if (instruction.BranchTarget) { + const auto displacement = static_cast(*instruction.BranchTarget) - (static_cast(code.GetRva()) + static_cast(bytes.size())); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Moved branch exceeds rel32 range from its TLS stub", access.FileOffset); + Io::WriteU32(bytes, instruction.BranchDisplacementOffset, static_cast(static_cast(displacement))); + } for (const auto byte : bytes) code.Emit({byte}); } diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 0e747853d8e..f67db4aa08c 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -66,15 +66,16 @@ int Run(const int argc, char* argv[]) { std::vector trampolines; if (args.toIntel) { - const auto codeSegments = Relinker::ElfReader(sourceBytes).ReadCodeSegments(); - auto converted = Codegen::MakeAmd64OnlyConverter(args.toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(sourceBytes), codeSegments); + const auto segments = Relinker::ElfReader(sourceBytes).ReadProgramHeaders(); + auto converted = Codegen::MakeAmd64OnlyConverter(args.toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(sourceBytes), segments); sourceBytes = std::move(converted.Bytes); trampolines = std::move(converted.Trampolines); std::map stubsByName; + const auto counted = [](const std::string& name) { return name == "VRSQRTPS" || name == "VRCPPS" || name == "256-bit store"; }; for (const auto& report : converted.Reports) { if (report.Lowering == Codegen::Amd64OnlyLowering::Kept) std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) kept: no room for a jump\n"; - else if (report.InstructionName == "VRSQRTPS" || report.InstructionName == "VRCPPS") + else if (counted(report.InstructionName)) ++stubsByName[report.InstructionName]; else std::cout << "Intel substitution: " << report.InstructionName << " at 0x" << std::hex << report.Offset << std::dec << " (" << report.OriginalLength << " bytes) -> " << (report.Lowering == Codegen::Amd64OnlyLowering::InPlace ? "in place " : "stub ") << report.ReplacementLength << " bytes\n"; @@ -127,19 +128,16 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(desktopEntry->IconPath, "Icon"); checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); } + if (args.windowsDiagnostics) + checkOutputInputs(std::filesystem::absolute("windows-diagnostics-imports.txt"), "Windows import diagnostics"); + std::vector registryPaths; if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); - std::vector registryPaths{ - outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json") - }; + registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + ".registry.json")); for (const auto& artifact : guestArtifacts) registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json")); for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); - const auto registryWriter = std::make_shared(); - fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); - for (std::size_t index = 0; index < guestArtifacts.size(); ++index) - fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); } auto byteWriter = std::make_shared(); @@ -174,6 +172,12 @@ int Run(const int argc, char* argv[]) { std::cout << "Guest module: " << Io::Utf8Path(artifact.Path) << '\n'; } fileWriter.Write(absPath, executableBytes); + if (args.writeRegistry) { + const auto registryWriter = std::make_shared(); + fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); + for (std::size_t index = 0; index < guestArtifacts.size(); ++index) + fileWriter.Write(Io::Utf8Path(registryPaths[index + 1]), registryWriter->WriteModuleImports(guestArtifacts[index].Imports)); + } if (desktopEntry) std::cout << "Desktop entry: " << Io::Utf8Path(desktopEntryWriter.Write(*desktopEntry)) << '\n'; std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; std::cout << "Expected runtime layout (relative to the output executable):\n" diff --git a/core/relinker/relinker/include/relinker/domain/IElfReader.hpp b/core/relinker/relinker/include/relinker/domain/IElfReader.hpp index 90ce34de307..f528b394511 100644 --- a/core/relinker/relinker/include/relinker/domain/IElfReader.hpp +++ b/core/relinker/relinker/include/relinker/domain/IElfReader.hpp @@ -18,7 +18,6 @@ class IElfReader { [[nodiscard]] virtual FileByteOffset TranslateVirtualAddress(VirtualAddress address) const = 0; [[nodiscard]] virtual std::vector ReadSection(const SectionHeader& header) const = 0; [[nodiscard]] virtual std::vector ReadSegment(const ProgramHeader& header) const = 0; - [[nodiscard]] virtual std::vector ReadCodeSegments() const = 0; [[nodiscard]] virtual std::uint64_t GetFileSize() const = 0; [[nodiscard]] virtual const std::vector& GetRawBytes() const = 0; }; diff --git a/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp b/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp index 1ed4ca8895a..b0cb4d0f761 100644 --- a/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp +++ b/core/relinker/relinker/include/relinker/parsing/ElfReader.hpp @@ -17,7 +17,6 @@ class ElfReader : public IElfReader { FileByteOffset TranslateVirtualAddress(VirtualAddress address) const override; std::vector ReadSection(const SectionHeader& header) const override; std::vector ReadSegment(const ProgramHeader& header) const override; - std::vector ReadCodeSegments() const override; std::uint64_t GetFileSize() const override; const std::vector& GetRawBytes() const override; diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 0c698568e61..fbd29c405f8 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -28,7 +28,8 @@ bool isRipRelativeFF(const std::vector& text, const Codegen::Instr std::uint8_t modrm = text[cur + 1]; std::uint8_t mod = (modrm >> ModRmModShift) & ModRmModMask; std::uint8_t rm = modrm & ModRmRmMask; - return mod == ModRmModIndirect && rm == ModRmRmRipRelative; + const auto operation = (modrm >> ModRmRegShift) & ModRmRegMask; + return mod == ModRmModIndirect && rm == ModRmRmRipRelative && (operation >= 2 && operation <= 6); } break; } diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 49b4a86b2dc..5222f8a409d 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -236,7 +236,8 @@ CodeInstructionSet CodeInstructionCollector::Collect(const std::vector> 32; const auto addend = Io::ReadU64(bytes, offset + 16); if (type == 8) { - if (index != 0 || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); + const auto zeroSectionSymbol = index != 0 && index < symbolCount && (bytes[symbols + index * 24 + 4] & 15) == 3 && Io::ReadU64(bytes, symbols + index * 24 + 8) == 0; + if ((index != 0 && !zeroSectionSymbol) || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); addRoot(addend); } else if (type == 1 || type == 6 || type == 7) { if (index == 0 || index >= symbolCount) throw Domain::RelinkerException("Code analysis: invalid relocation symbol", slot); diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index a7e80bcf0c8..f83eb1eead4 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -50,6 +50,45 @@ enum class ForbiddenInstruction { Return, }; +static constexpr std::array SDK_MPROTECT_INLINE{ + 0x55, 0x48, 0x89, 0xE5, 0x53, 0x50, 0x89, 0xD1, 0x49, 0x89, 0xF0, 0x48, 0x89, 0xFE, + 0xF6, 0xC1, 0x04, 0x75, 0x31, 0x48, 0x63, 0xD1, 0xB8, 0x4A, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xF7, 0x4C, 0x89, 0xC6, 0x45, 0x31, 0xD2, 0x45, 0x31, 0xC0, 0x45, 0x31, 0xC9, + 0x0F, 0x05, 0x0F, 0x93, 0xC1, 0x48, 0x89, 0xC3, 0x48, 0xF7, 0xDB, 0x84, 0xC9, + 0x48, 0x0F, 0x45, 0xD8, 0x48, 0x85, 0xDB, +}; + +static constexpr std::array SDK_MMAP_INLINE{ + 0x49, 0x63, 0xD6, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, 0xB8, 0xDD, 0x01, 0x00, 0x00, + 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x48, 0x89, 0xC1, 0x0F, 0x93, 0xC2, 0x48, 0xF7, 0xD8, + 0x84, 0xD2, 0x48, 0x0F, 0x45, 0xC1, 0x48, 0x85, 0xC0, +}; + +static constexpr std::array SDK_MMAP_EXEC_INLINE{ + 0x44, 0x89, 0xF0, 0x83, 0xE0, 0xFB, 0x48, 0x63, 0xD0, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, + 0xB8, 0xDD, 0x01, 0x00, 0x00, 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x0F, 0x93, 0xC1, + 0x49, 0x89, 0xC7, 0x49, 0xF7, 0xDF, 0x84, 0xC9, 0x4C, 0x0F, 0x45, 0xF8, 0x4D, 0x85, 0xFF, +}; + +template +static bool MatchesSdkInlineStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, + const std::array& pattern, std::size_t beforeSyscall, + const std::vector& matches, FileByteOffset codeOffset) { + if (syscallOffset < beforeSyscall || syscallOffset > limit) return false; + const auto start = syscallOffset - beforeSyscall; + if (pattern.size() > limit - start || !std::equal(pattern.begin(), pattern.end(), code.begin() + static_cast(start))) return false; + const auto instruction = std::lower_bound(matches.begin(), matches.end(), codeOffset + start, + [](const Codegen::InstructionMatch& match, FileByteOffset offset) { return match.Offset < offset; }); + return instruction != matches.end() && instruction->Offset == codeOffset + start; +} + +static bool IsSdkMmanSyscallStub(const std::vector& code, std::size_t syscallOffset, std::size_t limit, + const std::vector& matches, FileByteOffset codeOffset) { + return MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MPROTECT_INLINE, 42, matches, codeOffset) + || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_INLINE, 17, matches, codeOffset) + || MatchesSdkInlineStub(code, syscallOffset, limit, SDK_MMAP_EXEC_INLINE, 23, matches, codeOffset); +} + static ForbiddenInstruction ClassifyForbiddenInstruction( const std::vector& codeSection, const std::size_t opcodeOffset, @@ -158,7 +197,8 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls( if (kind == ForbiddenInstruction::None) continue; - if (_trapSdkStubs && kind == ForbiddenInstruction::Invocation && IsSdkSyscallStub(codeSection, opcode)) { + if (_trapSdkStubs && kind == ForbiddenInstruction::Invocation && + (IsSdkSyscallStub(codeSection, opcode) || IsSdkMmanSyscallStub(codeSection, opcode, limit, matches, codeSectionOffset))) { trapped.push_back(opcode); std::cout << "Trapping SDK syscall instruction at code offset 0x" << std::hex << match.Offset << std::dec << '\n'; continue; diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp index 10629ddefb6..1c64337a30f 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/ControlFlowGraph.cpp @@ -80,87 +80,87 @@ void buildGraph( VirtualAddress va = worklist.front(); worklist.pop(); - if (va < textVaddr || va >= textVaddr + static_cast(text.size())) - throw RelinkerException("CFG: jump target outside text segment", va); - - std::size_t bufOff = static_cast(va - textVaddr); - std::size_t available = text.size() - bufOff; - if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); - - Codegen::DecodedInstructionInfo info; - if (speculative) { - try { + while (va >= textVaddr && va - textVaddr < text.size() && !reachable.contains(va)) { + std::size_t bufOff = static_cast(va - textVaddr); + std::size_t available = text.size() - bufOff; + if (available == 0) throw RelinkerException("CFG: zero available bytes at target", va); + + Codegen::DecodedInstructionInfo info; + if (speculative) { + try { + info = decoder.DecodeInstruction(text.data() + bufOff, available); + } catch (const Codegen::CodegenException&) { + break; + } + } else { info = decoder.DecodeInstruction(text.data() + bufOff, available); - } catch (const Codegen::CodegenException&) { - continue; } - } else { - info = decoder.DecodeInstruction(text.data() + bufOff, available); - } - reachable.insert(va); + reachable.insert(va); - VirtualAddress nextVaddr = va + static_cast(info.Length); + VirtualAddress nextVaddr = va + static_cast(info.Length); - if (followCodeAddresses && info.HasRipRelativeDisp && text[bufOff + info.OpcodeOffset] == 0x8d && (info.RexPrefix & 8)) { - std::int32_t displacement = 0; - std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - enqueue(static_cast(static_cast(nextVaddr) + displacement)); - } + if (followCodeAddresses && info.HasRipRelativeDisp && text[bufOff + info.OpcodeOffset] == 0x8d && (info.RexPrefix & 8)) { + std::int32_t displacement = 0; + std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); + enqueue(static_cast(static_cast(nextVaddr) + displacement)); + } - if (data != nullptr && info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.Opcode == 0x8D) { - std::int32_t displacement = 0; - std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); - const auto base = static_cast(static_cast(nextVaddr) + displacement); - takeAddress(base); - for (VirtualAddress target : ReadRelativeTableTargets(*data, base, textVaddr, text.size())) takeAddress(target); - } + if (data != nullptr && info.HasRipRelativeDisp && !info.IsTwoByteOpcode && info.Opcode == 0x8D) { + std::int32_t displacement = 0; + std::memcpy(&displacement, text.data() + bufOff + info.RipRelativeDispOffset, 4); + const auto base = static_cast(static_cast(nextVaddr) + displacement); + takeAddress(base); + for (VirtualAddress target : ReadRelativeTableTargets(*data, base, textVaddr, text.size())) takeAddress(target); + } - switch (info.FlowKind) { - using enum Codegen::ControlFlowKind; - case Sequential: - enqueue(nextVaddr); - break; - case ConditionalBranch: - enqueue(nextVaddr); - if (info.HasBranchTarget) - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - break; - case UnconditionalJump: - if (info.HasRipRelativeDisp) { - std::int32_t disp = 0; - std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); - auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); - if (resolved.has_value()) - enqueue(*resolved); - } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - } - break; - case Call: - if (info.HasRipRelativeDisp) { - std::int32_t disp = 0; - std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); - VirtualAddress slotVaddr = static_cast( - static_cast(nextVaddr) + disp - ); - auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); - if (resolved.has_value()) - enqueue(*resolved); - } else if (info.HasBranchTarget) { - enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); - } - enqueue(nextVaddr); - break; - case IndirectCall: - enqueue(nextVaddr); - break; - case Return: - case IndirectJump: - case Trap: - break; + bool fallThrough = true; + switch (info.FlowKind) { + using enum Codegen::ControlFlowKind; + case Sequential: + break; + case ConditionalBranch: + if (info.HasBranchTarget) + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + break; + case UnconditionalJump: + fallThrough = false; + if (info.HasRipRelativeDisp) { + std::int32_t disp = 0; + std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); + VirtualAddress slotVaddr = static_cast( + static_cast(nextVaddr) + disp + ); + auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); + if (resolved.has_value()) + enqueue(*resolved); + } else if (info.HasBranchTarget) { + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + } + break; + case Call: + if (info.HasRipRelativeDisp) { + std::int32_t disp = 0; + std::memcpy(&disp, text.data() + bufOff + info.RipRelativeDispOffset, 4); + VirtualAddress slotVaddr = static_cast( + static_cast(nextVaddr) + disp + ); + auto resolved = relativeRelocations.TargetOfSlot(slotVaddr); + if (resolved.has_value()) + enqueue(*resolved); + } else if (info.HasBranchTarget) { + enqueue(static_cast(static_cast(nextVaddr) + info.BranchDisp)); + } + break; + case IndirectCall: + break; + case Return: + case IndirectJump: + case Trap: + fallThrough = false; + break; + } + if (!fallThrough) break; + va = nextVaddr; } } } diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp index cf163ac5553..3eede671ebc 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictNidFilter.cpp @@ -79,7 +79,12 @@ class StrictImage { const auto slot = read(*rela + offset); const auto info = read(*rela + offset + 8); if (static_cast(info) != 8) continue; - if (info >> 32) throw RelinkerException("Strict filter: relative relocation has a symbol", slot); + if (const auto symbol = info >> 32; symbol != 0) { + const auto symbols = table(6, 0x61000039); + const auto size = tags.find(0x6100003F); + if (!symbols || size == tags.end() || symbol >= size->second / 24 || (read(*symbols + symbol * 24 + 4) & 15) != 3 || read(*symbols + symbol * 24 + 8) != 0) + throw RelinkerException("Strict filter: relative relocation has a symbol", slot); + } const auto target = read(*rela + offset + 16); if (input.ImportSlots.contains(slot) || !input.Pointers.emplace(slot, target).second) throw RelinkerException("Strict filter: overlapping pointer relocations", slot); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp index 71fd7342ef3..21229806dda 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/StrictReachability.cpp @@ -163,7 +163,8 @@ class Analyzer { if (info.HasRipRelativeDisp) { std::int32_t displacement; std::memcpy(&displacement, input.Text.data() + offset + info.RipRelativeDispOffset, sizeof(displacement)); - const auto target = next + static_cast(static_cast(displacement)); + auto target = next + static_cast(static_cast(displacement)); + if (info.AddressSize32) target = static_cast(target); if (input.ImportSlots.contains(target)) region.Imports.insert(target); if (!info.IsTwoByteOpcode && info.Opcode == 0x8D) { addAddressTaken(target); diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index e6c3545a7c3..52cf793e561 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -262,13 +262,15 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto size = get(sizeTag, sceSizeTag); if (size % 24 != 0) fail("Invalid relocation table size"); const auto offset = table(addressTag, sceAddressTag, size); - output.assign(bytes.begin() + offset, bytes.begin() + offset + size); + if (plt && size != 0 && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); + output.reserve(size); for (std::uint64_t index = 0; index < size; index += 24) { - const auto info = Io::ReadU64(output, index + 8); + const auto info = Io::ReadU64(bytes, offset + index + 8); const auto relocation = static_cast(info); + if (relocation == 0) continue; if ((info >> 32) >= image.Symbols.size() || (plt && relocation != 7) || (relocation != 1 && relocation != 6 && relocation != 7 && relocation != 8 && relocation != 16 && relocation != 17 && relocation != 18)) fail("Unsupported relocation or symbol index"); - const auto target = Io::ReadU64(output, index); - const auto addend = Io::ReadU64(output, index + 16); + const auto target = Io::ReadU64(bytes, offset + index); + const auto addend = Io::ReadU64(bytes, offset + index + 16); mapped(target, 8, 2); const auto next = relocationTargets.lower_bound(target); if ((next != relocationTargets.end() && next->first < target + 8) || (next != relocationTargets.begin() && std::prev(next)->second > target)) fail("Overlapping guest relocations"); @@ -283,6 +285,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto& symbol = image.Symbols[info >> 32]; if (((info >> 32) == 0 && tls == nullptr) || ((info >> 32) != 0 && (symbol.Info & 15) != 6) || (relocation == 16 && addend != 0)) fail("Invalid guest TLS relocation"); } + output.insert(output.end(), bytes.begin() + offset + index, bytes.begin() + offset + index + 24); } }; if (tags.contains(7) || tags.contains(0x6100002f)) { @@ -290,7 +293,6 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector } copyRelocations(7, 0x6100002f, 8, 0x61000031, image.Dynamic.RelaData, false); copyRelocations(23, 0x61000029, 2, 0x6100002d, image.Dynamic.RelaPltData, true); - if (!image.Dynamic.RelaPltData.empty() && get(20, 0x6100002b) != 7) fail("Unsupported PLT relocation format"); if (tags.contains(3) || tags.contains(0x61000027)) image.Got = get(3, 0x61000027); if (tags.contains(12)) image.Init = tags.at(12); if (tags.contains(13)) image.Fini = tags.at(13); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index f6c8f8a4958..3b33b9a01aa 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include @@ -22,6 +24,11 @@ namespace Relinker { namespace { +struct GuestExport { + std::size_t Provider; + std::uint8_t Type; +}; + std::vector ReadNeededNames(const Domain::SysVDynamicSection& dynamic) { if (dynamic.DynamicSegmentData.size() % 16 != 0) throw Domain::RelinkerException("Invalid executable dependency table"); std::vector names; @@ -44,7 +51,7 @@ std::string FoldFilename(std::string name) { std::string ModuleStem(std::string name, const bool windows) { if (windows) name = FoldFilename(std::move(name)); - for (const std::string_view suffix : {".debug_prx", ".sprx", ".prx"}) { + for (const std::string_view suffix : {".debug_prx", ".suprx", ".sprx", ".prx"}) { if (name.size() > suffix.size() && name.ends_with(suffix)) return name.substr(0, name.size() - suffix.size()); } return {}; @@ -120,9 +127,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::FileReader reader; std::map discovered; const auto matchesIdentity = [](const std::string& name, const std::vector& identities) { - return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { - return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx"; - }); + const auto stem = std::string_view(name).substr(0, name.size() - Domain::ModuleSuffix(name).size()); + return std::any_of(identities.begin(), identities.end(), [&](const auto& identity) { return name == identity || stem == identity; }); }; for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)), windows)); if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); @@ -136,7 +142,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (std::filesystem::equivalent(path, outputPath)) throw Domain::RelinkerException("Executable output would overwrite an input module: " + Io::Utf8Path(outputPath)); } - std::map> exports; + std::unordered_map> exports; std::map> sharedExports; std::set outputNames; for (const auto& path : paths) { @@ -153,10 +159,11 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path for (const auto& symbol : image.Symbols) { if (symbol.Section == 0 || symbol.Section == AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; auto& providers = exports[symbol.Name]; - const bool repeated = std::find(providers.begin(), providers.end(), images.size()) != providers.end(); - if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front()].SourcePath)); - if (!repeated) providers.push_back(images.size()); - if (!windows && providers.size() > 1) sharedExports[symbol.Name].insert(providers.begin(), providers.end()); + const bool repeated = !providers.empty() && providers.back().Provider == images.size(); + if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front().Provider].SourcePath)); + if (!repeated) providers.push_back({images.size(), static_cast(symbol.Info & 15)}); + if (!windows && providers.size() > 1) + for (const auto& provider : providers) sharedExports[symbol.Name].insert(provider.Provider); } images.push_back(std::move(image)); } @@ -264,20 +271,19 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (shared == sharedExports.end() || declared == guestNames.end() || !shared->second.contains(declared->second)) rejectSharedImport(symbol.Name, Io::Utf8Path(images[index].SourcePath)); const auto found = exports.find(symbol.Name); - std::vector providers; + const GuestExport* provider = nullptr; if (found != exports.end()) { - for (const auto provider : found->second) { - if (symbol.Library.empty() || (declared != guestNames.end() && declared->second == provider)) providers.push_back(provider); + for (const auto& candidate : found->second) { + if (!symbol.Library.empty() && (declared == guestNames.end() || declared->second != candidate.Provider)) continue; + if (provider != nullptr) throw Domain::RelinkerException("Ambiguous guest import after stripping #: " + symbol.Name); + provider = &candidate; } } - if (providers.size() > 1) throw Domain::RelinkerException("Ambiguous guest import after stripping #: " + symbol.Name); - if (!providers.empty()) { - const auto& provider = images[providers.front()]; - const auto exported = std::find_if(provider.Symbols.begin(), provider.Symbols.end(), [&](const auto& candidate) { return candidate.Section != 0 && candidate.Section != AbsoluteSection && candidate.Name == symbol.Name && (candidate.Info >> 4) != 0 && candidate.Visibility != 1 && candidate.Visibility != 2; }); - if (exported == provider.Symbols.end() || ((symbol.Info & 15) != 0 && (symbol.Info & 15) != (exported->Info & 15))) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); - if (providers.front() != index) dependencies[index].insert(providers.front()); + if (provider != nullptr) { + if ((symbol.Info & 15) != 0 && (symbol.Info & 15) != provider->Type) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); + if (provider->Provider != index) dependencies[index].insert(provider->Provider); } else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name); - if (providers.empty()) systemImports[index].insert(symbolIndex); + if (provider == nullptr) systemImports[index].insert(symbolIndex); } } std::vector required(images.size()); @@ -331,7 +337,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path }; for (const auto index : order) if (!windows && !deferred(index)) addNeeded("$ORIGIN/" + appDirectory(images[index]) + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); - if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); + if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !std::filesystem::path(runPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); const auto outputDirectory = std::filesystem::absolute(outputPath).parent_path().lexically_normal(); std::vector artifacts; for (const auto index : order) { @@ -342,7 +348,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::vector codeHeaders; for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header); if (toIntel) { - auto converted = Codegen::MakeAmd64OnlyConverter(toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(image.Bytes), codeHeaders); + auto converted = Codegen::MakeAmd64OnlyConverter(toRosetta ? Codegen::Amd64OnlyTarget::Rosetta : Codegen::Amd64OnlyTarget::Intel)->Convert(std::move(image.Bytes), image.Headers); image.Trampolines = std::move(converted.Trampolines); image.Bytes = std::move(converted.Bytes); } @@ -370,9 +376,12 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime); else { std::string guestRunPath = runPath; - if (runPath == "$ORIGIN" || runPath.starts_with("$ORIGIN/")) { - const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); - guestRunPath = "$ORIGIN/" + relativeRoot + runPath.substr(7); + const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); + for (std::size_t position = 0; (position = guestRunPath.find("$ORIGIN", position)) != std::string::npos;) { + position += 7; + if (position != guestRunPath.size() && guestRunPath[position] != '/' && guestRunPath[position] != ':') continue; + guestRunPath.insert(position, "/" + relativeRoot); + position += relativeRoot.size() + 1; } std::vector needed; for (const auto dependency : dependencies[index]) { diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index d3ad5fee490..c65cfeac1af 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -82,6 +82,11 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( return value.substr(0, hashPos); }; + auto symbolInfo = [](const NidReference& ref) -> std::uint8_t { + if (ref.Nid.empty()) return static_cast((STB_WEAK << 4) | STT_NOTYPE); + return static_cast(((ref.Weak ? STB_WEAK : STB_GLOBAL) << 4) | STT_FUNC); + }; + static constexpr std::size_t kRelaEntSize = 24; std::vector pltSlots(originalJmprelCount, nullptr); @@ -132,8 +137,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( const NidReference& ref = *slot; const std::uint32_t nameOff = _appendStr(result.DynStrData, stripHashSuffix(ref.Nid)); - const auto info = static_cast((STB_GLOBAL << 4) | STT_FUNC); - _appendElfSym(result.DynSymData, nameOff, info, STV_DEFAULT, 0, 0, 0); + _appendElfSym(result.DynSymData, nameOff, symbolInfo(ref), STV_DEFAULT, 0, 0, 0); const std::uint64_t relaInfo = (static_cast(symIdx) << 32) | R_X86_64_JUMP_SLOT; _appendRela(result.RelaPltData, ref.RelocationAddress, relaInfo, ref.Addend); @@ -143,8 +147,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( for (const NidReference* slotPtr : nonPltRefs) { const NidReference& ref = *slotPtr; const std::uint32_t nameOff = _appendStr(result.DynStrData, stripHashSuffix(ref.Nid)); - const auto info = static_cast((STB_GLOBAL << 4) | STT_FUNC); - _appendElfSym(result.DynSymData, nameOff, info, STV_DEFAULT, 0, 0, 0); + _appendElfSym(result.DynSymData, nameOff, symbolInfo(ref), STV_DEFAULT, 0, 0, 0); const std::uint64_t relaInfo = (static_cast(symIdx) << 32) | ref.RelocationTypeValue; _appendRela(result.RelaData, ref.RelocationAddress, relaInfo, ref.Addend); diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index c6af1ec10b5..a214a56f8d4 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -86,6 +86,12 @@ ElfHeader ElfReader::ReadHeader() const { } if (_fileBuffer[4] != 2 || _fileBuffer[5] != 1 || _fileBuffer[6] != 1) throw RelinkerException("Expected little-endian ELF64 version 1"); + if (_readU16At(0x12) != 62) + throw RelinkerException("Expected an x86-64 ELF"); + if (_readU32At(0x14) != 1) + throw RelinkerException("Unsupported ELF object version: expected 1", 0x14); + if (_readU16At(0x34) != 64) + throw RelinkerException("Invalid ELF header size: expected 64 bytes", 0x34); ElfHeader header{}; header.Machine = _readU16At(0x12); @@ -128,6 +134,23 @@ std::vector ElfReader::ReadProgramHeaders() const { offset += header.ProgramHeaderEntrySize; } + for (auto& relro : headers) { + if (relro.Type != PT_OS_RELRO || relro.MemorySize == 0) continue; + bool covered = false; + for (const auto& load : headers) { + if (load.Type != PT_LOAD || load.MemorySize == 0) continue; + const bool overlaps = relro.MappedAddress < load.MappedAddress ? load.MappedAddress - relro.MappedAddress < relro.MemorySize : relro.MappedAddress - load.MappedAddress < load.MemorySize; + if (!overlaps) continue; + if (relro.MappedAddress < load.MappedAddress || relro.MemorySize > load.MemorySize - (relro.MappedAddress - load.MappedAddress)) + throw RelinkerException("PT_SCE_RELRO partially overlaps a PT_LOAD segment", relro.MappedAddress); + covered = true; + } + if (!covered) { + relro.Type = PT_LOAD; + relro.Flags = PF_R | PF_W; + } + } + return headers; } @@ -261,18 +284,6 @@ std::vector ElfReader::ReadSegment(const ProgramHeader& header) co _fileBuffer.begin() + header.Offset + header.FileSize); } -std::vector ElfReader::ReadCodeSegments() const { - static constexpr std::uint32_t kPtLoad = 1; - static constexpr std::uint32_t kPfX = 0x1; - std::vector result; - for (const auto& ph : ReadProgramHeaders()) { - if (ph.Type == kPtLoad && (ph.Flags & kPfX)) { - result.push_back(ph); - } - } - return result; -} - std::uint64_t ElfReader::GetFileSize() const { return _fileBuffer.size(); } diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index a486d3d6465..f0ad0772aa2 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -219,6 +219,15 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf return relaOff + off; }; + auto isZeroSectionSymbol = [&](const std::uint32_t symIdx) { + const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; + if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < symEntSize) + throw RelinkerException("Symbol table entry out of bounds", symOff); + std::uint64_t value = 0; + std::memcpy(&value, raw.data() + symOff + 8, sizeof(value)); + return (raw[symOff + 4] & 0xf) == 3 && value == 0; + }; + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { for (ByteCount off = 0; relaSize >= relaEntSize && off <= relaSize - relaEntSize; off += relaEntSize) { const FileByteOffset pos = relaEntryPos(relaOff, off); @@ -236,20 +245,24 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf continue; if (relType == 8) { - if (symIdx != 0) + if (symIdx != 0 && !isZeroSectionSymbol(symIdx)) throw RelinkerException("RELATIVE relocation has a nonzero symbol index", pos); continue; } const FileByteOffset symOff = dynSymTabOffset + static_cast(symIdx) * symEntSize; - if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < 4) + if (symOff < dynSymTabOffset || symOff > raw.size() || raw.size() - symOff < 5) throw RelinkerException("Symbol table entry out of bounds", symOff); std::uint32_t nameOff = 0; std::memcpy(&nameOff, raw.data() + symOff, 4); const auto name = readCStr(nameOff); - nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend}); + if (name.empty() && (raw.size() - symOff < symEntSize + || std::any_of(raw.begin() + static_cast(symOff), raw.begin() + static_cast(symOff + symEntSize), + [](const std::uint8_t byte) { return byte != 0; }))) + throw RelinkerException("Unnamed import symbol is not a null symbol entry", symOff); + nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend, (raw[symOff + 4] >> 4) == STB_WEAK}); } }; @@ -405,9 +418,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::memcpy(&rOffset, raw.data() + pos, 8); std::memcpy(&rInfo, raw.data() + pos + 8, 8); std::memcpy(&rAddend, raw.data() + pos + 16, 8); - const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); - if (symIdx == 0 && relType == R_X86_64_RELATIVE) + if (relType == R_X86_64_RELATIVE) appendRela(dynSection.RelaData, rOffset, static_cast(R_X86_64_RELATIVE), rAddend); } }; @@ -418,6 +430,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::vector entries; entries.reserve(nidRefs.size()); for (const auto& ref : nidRefs) { + if (ref.Nid.empty()) continue; CallRegistryEntry entry; entry.Nid = ref.Nid; entry.Library = ref.Library; diff --git a/core/relinker/relinker/tests/CallSiteResolverTests.cpp b/core/relinker/relinker/tests/CallSiteResolverTests.cpp new file mode 100644 index 00000000000..f780307eb3f --- /dev/null +++ b/core/relinker/relinker/tests/CallSiteResolverTests.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void require(bool condition, const std::string& name) { + if (!condition) throw std::runtime_error(name); +} + +void check(const std::string& name, std::initializer_list instruction, bool expected) { + auto resolver = Relinker::MakeCallSiteResolver(); + const std::vector text(instruction); + const auto sites = resolver->ResolveCallSites(text, 0x1000, 0x1100, 8); + require(sites == (expected ? std::vector{0x1000} : std::vector{}), name); + require(resolver->ResolveCallSites(text, 0x1000, 0x1200, 8).empty(), name + " unexpected address"); +} + +} + +int main() { + try { + check("indirect call", {0xff, 0x15, 0xfa, 0, 0, 0}, true); + check("indirect tail jump", {0xff, 0x25, 0xfa, 0, 0, 0}, true); + check("far indirect call", {0xff, 0x1d, 0xfa, 0, 0, 0}, true); + check("far indirect jump", {0xff, 0x2d, 0xfa, 0, 0, 0}, true); + check("RIP-relative function pointer load", {0x48, 0x8b, 0x05, 0xf9, 0, 0, 0}, true); + check("increment memory", {0xff, 0x05, 0xfa, 0, 0, 0}, false); + check("decrement memory", {0xff, 0x0d, 0xfa, 0, 0, 0}, false); + check("push memory", {0xff, 0x35, 0xfa, 0, 0, 0}, true); + std::cout << "Call-site resolver tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "call_site_resolver_tests: " << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp new file mode 100644 index 00000000000..5870e3c5e7a --- /dev/null +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace Relinker::UnusedNidFilter; +using Relinker::VirtualAddress; +using Bytes = std::vector; +constexpr VirtualAddress TextAddress = 0xec00001000; + +class Pointers : public IRelativeRelocationIndex { +public: + std::map Slots; + + std::optional TargetOfSlot(VirtualAddress slot) const override { + const auto position = Slots.find(slot); + if (position == Slots.end()) return std::nullopt; + return position->second; + } + + std::vector Targets() const override { + std::vector targets; + for (const auto& [slot, target] : Slots) targets.push_back(target); + return targets; + } +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void RequireOffsets(const IControlFlowGraph& graph, std::initializer_list offsets) { + std::unordered_set expected; + for (const auto offset : offsets) expected.insert(TextAddress + offset); + Require(graph.ReachableVaddrs() == expected, "Unexpected reachable instruction set"); + for (const auto address : expected) Require(graph.IsReachable(address), "Missing reachable instruction"); + Require(!graph.IsReachable(TextAddress - 1), "Address before code is reachable"); +} + +void SetDisplacement(Bytes& text, std::size_t offset, std::size_t next, VirtualAddress target) { + const auto displacement = static_cast(target - (TextAddress + next)); + std::memcpy(text.data() + offset, &displacement, sizeof(displacement)); +} + +void CheckFallthrough() { + const Pointers pointers; + const Bytes text{0x90, 0x48, 0x89, 0xc0, 0xc3, 0x90}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 1, 4}); + const auto end = BuildControlFlowGraph(Bytes{0x90, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*end, {0, 1}); + const auto empty = BuildControlFlowGraph(Bytes{}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*empty, {}); + const auto outside = BuildControlFlowGraph(text, TextAddress, TextAddress - 1, {TextAddress + text.size()}, pointers); + RequireOffsets(*outside, {}); +} + +void CheckBranches() { + const Pointers pointers; + const Bytes text{0x75, 0x03, 0x90, 0xeb, 0x03, 0x90, 0xeb, 0xfa, 0xc3, 0x90}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {TextAddress, TextAddress + 2}, pointers); + RequireOffsets(*graph, {0, 2, 3, 5, 6, 8}); + const auto self = BuildControlFlowGraph(Bytes{0xeb, 0xfe, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*self, {0}); + const auto outside = BuildControlFlowGraph(Bytes{0xeb, 0x7f, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*outside, {0}); + const auto trap = BuildControlFlowGraph(Bytes{0x0f, 0x0b, 0x90}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*trap, {0}); +} + +void CheckCalls() { + const Pointers pointers; + const Bytes text{0xe8, 0x03, 0, 0, 0, 0x90, 0xc3, 0x90, 0xe8, 0xf3, 0xff, 0xff, 0xff, 0xc3}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 5, 6, 8, 13}); + const auto indirect = BuildControlFlowGraph(Bytes{0xff, 0xd0, 0x90, 0xff, 0xe0, 0xc3}, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*indirect, {0, 2, 3}); +} + +void CheckRelocatedTransfers() { + constexpr auto slot = TextAddress + 0x1000; + Bytes text{0xff, 0x15, 0, 0, 0, 0, 0x90, 0xc3, 0xff, 0x25, 0, 0, 0, 0, 0x90, 0x90, 0xc3}; + SetDisplacement(text, 2, 6, slot); + SetDisplacement(text, 10, 14, slot + 8); + Pointers pointers; + pointers.Slots = {{slot, TextAddress + 8}, {slot + 8, TextAddress + 16}}; + const auto graph = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*graph, {0, 6, 7, 8, 16}); + const Pointers missing; + const auto unresolved = BuildControlFlowGraph(text, TextAddress, TextAddress, {TextAddress + 8}, missing); + RequireOffsets(*unresolved, {0, 6, 7, 8}); +} + +void CheckCodeAddresses() { + Bytes text(17, 0x90); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, TextAddress + 16); + text[7] = text[16] = 0xc3; + const Pointers pointers; + const auto ordinary = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers); + RequireOffsets(*ordinary, {0, 7}); + const auto addresses = BuildControlFlowGraph(text, TextAddress, TextAddress, {}, pointers, true); + RequireOffsets(*addresses, {0, 7, 16}); +} + +void CheckSpeculativeRoots() { + constexpr auto table = TextAddress + 0x1000; + Bytes text(32, 0xc3); + text[0] = 0x48; + text[1] = 0x8d; + text[2] = 0x05; + SetDisplacement(text, 3, 7, table); + text[16] = 0x90; + text[31] = 0x0f; + Bytes entries(12); + const std::int32_t targets[]{static_cast(TextAddress + 16 - table), + static_cast(TextAddress + 31 - table), 0}; + std::memcpy(entries.data(), targets, sizeof(targets)); + Pointers pointers; + pointers.Slots = {{table + 16, TextAddress + 24}, {table + 24, TextAddress + 31}}; + const std::vector data{{table, entries}}; + const auto graph = BuildAddressTakenControlFlowGraph(text, TextAddress, TextAddress, {}, pointers, data); + RequireOffsets(*graph, {0, 7, 16, 17, 24}); + bool threw = false; + try { + BuildAddressTakenControlFlowGraph(text, TextAddress, TextAddress, {TextAddress + 31}, pointers, data); + } catch (const Codegen::CodegenException&) { + threw = true; + } + Require(threw, "Malformed mandatory root was treated as speculative"); +} + +void CheckBorrowedReachability() { + const Pointers pointers; + const Bytes text{0x90, 0x90, 0xc3, 0x90, 0xc3}; + std::unordered_set reachable{TextAddress + 1}; + const auto first = BuildControlFlowGraph(std::span(text), TextAddress, TextAddress, {}, pointers, false, reachable); + RequireOffsets(*first, {0, 1}); + Require(&first->ReachableVaddrs() == &reachable, "Borrowed graph copied the result set"); + const auto second = BuildControlFlowGraph(std::span(text), TextAddress, TextAddress + 3, + {TextAddress + 3, TextAddress + 4}, pointers, false, reachable); + RequireOffsets(*second, {0, 1, 3, 4}); + RequireOffsets(*first, {0, 1, 3, 4}); +} + +} + +int main() { + try { + CheckFallthrough(); + CheckBranches(); + CheckCalls(); + CheckRelocatedTransfers(); + CheckCodeAddresses(); + CheckSpeculativeRoots(); + CheckBorrowedReachability(); + std::cout << "Control-flow graph tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "control_flow_graph_tests: " << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp new file mode 100644 index 00000000000..264cb6057c7 --- /dev/null +++ b/core/relinker/relinker/tests/SdkMmanSyscallTests.cpp @@ -0,0 +1,87 @@ +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; +struct Fixture { Bytes code; std::size_t syscall; }; + +const std::vector Fixtures = { + {{0x55, 0x48, 0x89, 0xE5, 0x53, 0x50, 0x89, 0xD1, 0x49, 0x89, 0xF0, 0x48, 0x89, 0xFE, + 0xF6, 0xC1, 0x04, 0x75, 0x31, 0x48, 0x63, 0xD1, 0xB8, 0x4A, 0x00, 0x00, 0x00, + 0x48, 0x89, 0xF7, 0x4C, 0x89, 0xC6, 0x45, 0x31, 0xD2, 0x45, 0x31, 0xC0, 0x45, 0x31, 0xC9, + 0x0F, 0x05, 0x0F, 0x93, 0xC1, 0x48, 0x89, 0xC3, 0x48, 0xF7, 0xDB, 0x84, 0xC9, + 0x48, 0x0F, 0x45, 0xD8, 0x48, 0x85, 0xDB}, 42}, + {{0x49, 0x63, 0xD6, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, 0xB8, 0xDD, 0x01, 0x00, 0x00, + 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x48, 0x89, 0xC1, 0x0F, 0x93, 0xC2, 0x48, 0xF7, 0xD8, + 0x84, 0xD2, 0x48, 0x0F, 0x45, 0xC1, 0x48, 0x85, 0xC0}, 17}, + {{0x44, 0x89, 0xF0, 0x83, 0xE0, 0xFB, 0x48, 0x63, 0xD0, 0x4C, 0x63, 0xD1, 0x4D, 0x63, 0xC0, + 0xB8, 0xDD, 0x01, 0x00, 0x00, 0x48, 0x89, 0xDE, 0x0F, 0x05, 0x0F, 0x93, 0xC1, + 0x49, 0x89, 0xC7, 0x49, 0xF7, 0xDF, 0x84, 0xC9, 0x4C, 0x0F, 0x45, 0xF8, 0x4D, 0x85, 0xFF}, 23}, +}; + +void Require(bool value, const std::string& message) { + if (!value) throw std::runtime_error(message); +} + +bool Rejected(const Bytes& code, bool trap, std::size_t size) { + try { + Relinker::MakeSyscallScanner(trap)->ScanCodeSectionForSyscalls(code, 0, size); + return false; + } catch (const std::runtime_error&) { + return true; + } +} + +void CheckFixtures() { + std::size_t rejectedMutations = 0; + for (const auto& fixture : Fixtures) { + Require(Rejected(fixture.code, false, fixture.code.size()), "default mode accepted an SDK inline syscall"); + for (const auto padding : {std::size_t{0}, std::size_t{1}, std::size_t{7}, std::size_t{64}}) { + Bytes code(padding, 0x90); + code.insert(code.end(), fixture.code.begin(), fixture.code.end()); + const auto trapped = Relinker::MakeSyscallScanner(true)->ScanCodeSectionForSyscalls(code, 0x123400, code.size()); + Require(trapped == std::vector{padding + fixture.syscall}, "SDK inline syscall returned the wrong patch offset"); + } + for (std::size_t length = fixture.syscall + 2; length < fixture.code.size(); ++length) + Require(Rejected(fixture.code, true, length), "SDK matching read its suffix beyond the code section"); + for (std::size_t removed = 1; removed <= fixture.syscall; ++removed) { + const Bytes fragment(fixture.code.begin() + static_cast(removed), fixture.code.end()); + if (Rejected(fragment, false, fragment.size())) + Require(Rejected(fragment, true, fragment.size()), "SDK matching accepted a truncated prefix"); + } + for (std::size_t offset = 0; offset < fixture.code.size(); ++offset) { + if (offset == fixture.syscall || offset == fixture.syscall + 1) continue; + for (unsigned bit = 0; bit < 8; ++bit) { + auto code = fixture.code; + code[offset] ^= static_cast(1u << bit); + if (!Rejected(code, false, code.size())) continue; + Require(Rejected(code, true, code.size()), "SDK matching accepted a changed instruction sequence"); + ++rejectedMutations; + } + } + } + Require(rejectedMutations > 1000, "too few near-match rejection cases ran"); + Bytes embedded = {0x48, 0xB8}; + embedded.insert(embedded.end(), Fixtures.front().code.begin(), Fixtures.front().code.end()); + Require(Rejected(embedded, false, embedded.size()), "embedded fixture lost its syscall instruction"); + Require(Rejected(embedded, true, embedded.size()), "SDK matching accepted a prefix that starts inside an immediate"); + for (const auto& code : std::vector{{0xB8, 0xDD, 0x01, 0x00, 0x00, 0x0F, 0x05, 0xC3}, {0xCD, 0x80}, {0x0F, 0x34}, {0x0F, 0x07}}) + Require(Rejected(code, true, code.size()), "SDK trap mode accepted an unrelated privileged instruction"); + std::cout << "SDK inline syscall checks passed, " << rejectedMutations << " near-match rejections\n"; +} + +} + +int main() { + try { + CheckFixtures(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index f62ec3f0943..4878395603b 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -98,6 +98,22 @@ void fallthroughAfterZeroPadding() { require(AnalyzeStrictReachability(input).ImportSlots.contains(0x2000), "Zero padding after a region's last jump dropped its fall-through edge"); } +void addressSizeOverrideImport() { + auto input = fixture(); + input.Text.assign(32, 0); + input.TextVaddr = 0x100001000ull; + input.Entries = {input.TextVaddr}; + input.ImportSlots = {0x2000, 0x100002000ull}; + emit(input, 0, {0x67, 0xFF, 0x15, 0xF9, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x2000}, "EIP-relative import was not resolved with 32-bit address wrapping"); + input.Text.assign(32, 0); + emit(input, 0, {0x67, 0x48, 0x8B, 0x05, 0xF8, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x2000}, "REX.W EIP-relative load did not resolve its import"); + input.Text.assign(32, 0); + emit(input, 0, {0xFF, 0x15, 0xFA, 0x0F, 0x00, 0x00, 0xC3}); + require(AnalyzeStrictReachability(input).ImportSlots == std::set{0x100002000ull}, "RIP-relative import lost its full 64-bit address"); +} + void callbackAndRelocationRoots() { auto input = fixture(); ripOperand(input, 0, {0x48, 0x8D, 0x3D}, 0x1020); @@ -294,8 +310,10 @@ std::vector elfFixture(const StrictReachabilityInput& input) { bytes[6] = 1; write(bytes, 16, 3); write(bytes, 18, 62); + write(bytes, 20, 1); write(bytes, 24, input.Entries.at(0)); write(bytes, 32, 64); + write(bytes, 52, 64); write(bytes, 54, 56); write(bytes, 56, 2); write(bytes, 64, 1); @@ -576,6 +594,32 @@ void sceDynlibDataOffsets() { "Duplicate PT_SCE_DYNLIBDATA segments were accepted"); requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, duplicate, input.Text, input.TextVaddr); }, "Strict filter accepted duplicate PT_SCE_DYNLIBDATA segments"); + + auto sectionRelative = bytes; + write(sectionRelative, 208, 0x70); + write(sectionRelative, 216, 0x70); + write(sectionRelative, 0x330, 0x61000039); + write(sectionRelative, 0x338, 0x100); + write(sectionRelative, 0x340, 0x6100003B); + write(sectionRelative, 0x348, 24); + write(sectionRelative, 0x350, 0x6100003F); + write(sectionRelative, 0x358, 48); + write(sectionRelative, 0x61C, 3); + write(sectionRelative, 0x908, (std::uint64_t{1} << 32) | 8); + require(Relinker::MakeStrictUnusedNidFilter()->Filter(references, sectionRelative, input.Text, input.TextVaddr).size() == 1, + "Strict filter rejected a RELATIVE relocation against a zero-value section symbol"); + auto valuedSection = sectionRelative; + write(valuedSection, 0x620, 0x10); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, valuedSection, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation against a section symbol with a value"); + auto untyped = sectionRelative; + write(untyped, 0x61C, 0); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, untyped, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation against a non-section symbol"); + auto outside = sectionRelative; + write(outside, 0x908, (std::uint64_t{2} << 32) | 8); + requireFailure([&] { (void)Relinker::MakeStrictUnusedNidFilter()->Filter(references, outside, input.Text, input.TextVaddr); }, + "Strict filter accepted a RELATIVE relocation symbol outside the symbol table"); } } @@ -586,6 +630,7 @@ int main() { ud1DeadTail(); conditionalTailCall(); fallthroughAfterZeroPadding(); + addressSizeOverrideImport(); callbackAndRelocationRoots(); registerImportCall(); mergedJumpTable(); diff --git a/core/relinker/relinker/tests/test_elf_header.py b/core/relinker/relinker/tests/test_elf_header.py index 553665bbde4..171c44e1c03 100644 --- a/core/relinker/relinker/tests/test_elf_header.py +++ b/core/relinker/relinker/tests/test_elf_header.py @@ -1,4 +1,5 @@ from pathlib import Path +import struct import subprocess import sys import tempfile @@ -6,19 +7,27 @@ from test_guest_intel_trampolines import main_fixture +MODES = ([], ["--windows"], ["--to-intel"], ["--windows", "--to-intel"], + ["--macos"], ["--macos", "--to-rosetta"]) + + def identification_cases(relinker, directory): image = main_fixture() - cases = [("sysv", image, False)] + cases = [("sysv", image, None)] freebsd = bytearray(image) freebsd[7] = 9 - cases.append(("freebsd", freebsd, False)) + cases.append(("freebsd", freebsd, None)) for field, values in ((4, (0, 1, 255)), (5, (0, 2, 255)), (6, (0, 2, 255))): for value in values: data = bytearray(image) data[field] = value - cases.append((f"ident-{field}-{value}", data, True)) - for mode, flags in enumerate(([], ["--windows"], ["--to-intel"], ["--windows", "--to-intel"])): - for name, data, invalid in cases: + cases.append((f"ident-{field}-{value}", data, "Expected little-endian ELF64 version 1")) + for value in (0, 3, 183): + data = bytearray(image) + data[18:20] = value.to_bytes(2, "little") + cases.append((f"machine-{value}", data, "Expected an x86-64 ELF")) + for mode, flags in enumerate(MODES): + for name, data, error in cases: source = Path(directory) / f"{mode}-{name}.elf" output = source.with_suffix(".out") registry = output.with_name(output.stem + ".registry.json") @@ -26,20 +35,56 @@ def identification_cases(relinker, directory): result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", *flags, str(source), str(output)], capture_output=True, text=True, timeout=20) assert source.read_bytes() == data, (mode, name, "source changed") - if invalid: - assert result.returncode == 2 and "Expected little-endian ELF64 version 1" in result.stderr, (mode, name, result) + if error: + assert result.returncode == 2 and error in result.stderr, (mode, name, result) assert not output.exists() and not registry.exists(), (mode, name, "output written for unsupported ELF identification") else: assert result.returncode == 0 and registry.is_file(), (mode, name, result) - assert output.read_bytes().startswith(b"MZ" if "--windows" in flags else b"\x7fELF"), (mode, name) + magic = b"\x7fELF" + if "--windows" in flags: + magic = b"MZ" + elif "--macos" in flags: + magic = b"\xcf\xfa\xed\xfe" + assert output.read_bytes().startswith(magic), (mode, name) + + +def structural_cases(relinker, directory): + fields = ( + (20, "I", (0, 2, 0xFFFFFFFF), "Unsupported ELF object version"), + (52, "H", (0, 63, 65, 65535), "Invalid ELF header size"), + ) + for mode, flags in enumerate(MODES): + for filter_level in (0, 1, 2): + for offset, encoding, values, error in fields: + for value in values: + case = Path(directory) / f"header-{mode}-{filter_level}-{offset}-{value}" + case.mkdir() + source = case / "input.elf" + output = case / "output.out" + registry = case / "output.registry.json" + data = main_fixture() + struct.pack_into("<" + encoding, data, offset, value) + source.write_bytes(data) + result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", + f"unused-filter={filter_level}", *flags, str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and error in result.stderr, (case.name, result) + assert source.read_bytes() == data, (case.name, "source changed") + assert not output.exists() and not registry.exists(), (case.name, "output written") def main(): relinker = Path(sys.argv[1]).resolve() + headerOnly = bytearray(64) + headerOnly[0:7] = b"\x7fELF\x02\x01\x01" + headerOnly[18:20] = (62).to_bytes(2, "little") + headerOnly[20:24] = (1).to_bytes(4, "little") + headerOnly[52:54] = (64).to_bytes(2, "little") cases = [ ("tiny", bytes(10), "File too small for ELF header"), ("truncated", b"\x7fELF\x02\x01\x01" + bytes(25), "File too small for ELF header"), - ("header-only", b"\x7fELF\x02\x01\x01" + bytes(57), "No PT_DYNAMIC segment found"), + ("header-only", bytes(headerOnly), "No PT_DYNAMIC segment found"), + ("header-only-sections", main_fixture()[:64], "FileByteOffset out of bounds"), ("bad-magic", bytes(64), "Invalid ELF magic number"), ] with tempfile.TemporaryDirectory(prefix="anyps5-elf-header-") as directory: @@ -51,6 +96,7 @@ def main(): capture_output=True, text=True, timeout=20) assert result.returncode == 2 and error in result.stderr and not output.exists(), (name, result) identification_cases(relinker, directory) + structural_cases(relinker, directory) print("ELF header tests passed") diff --git a/core/relinker/relinker/tests/test_guest_export_lookup.py b/core/relinker/relinker/tests/test_guest_export_lookup.py new file mode 100644 index 00000000000..0204d163304 --- /dev/null +++ b/core/relinker/relinker/tests/test_guest_export_lookup.py @@ -0,0 +1,128 @@ +from pathlib import Path +import os +import struct +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture +from test_guest_module_directories import needed_libraries +from test_guest_symbol_names import imported_symbols, module_symbols +from test_windows_import_modules import executable + + +def guest_symbols(symbols, dependency=None): + image = guest_fixture(PLAIN_SITE) + image[0x400:0x406] = b'\xb8\x2a\x00\x00\x00\xc3' + strings = bytearray(b'\0') + table = bytearray(24) + for name, info, visibility, section in symbols: + offset = len(strings) + strings.extend(name.encode() + b'\0') + table.extend(struct.pack('= 0, (tags, strings) actual = strings[offset:end].decode() configured = run_path if run_path is not None else "$ORIGIN/libs" - expected = Path(configured.replace("$ORIGIN", str(output.parent))).resolve() - resolved = Path(actual.replace("$ORIGIN", str(artifact.parent))).resolve() + configured_paths = configured.split(":") if label.startswith("list") else [configured] + actual_paths = actual.split(":") if label.startswith("list") else [actual] + expected = [Path(path.replace("$ORIGIN", str(output.parent))).resolve() for path in configured_paths] + resolved = [Path(path.replace("$ORIGIN", str(artifact.parent))).resolve() for path in actual_paths] if resolved != expected or (label == "absolute" and actual != run_path): failures.append((str(relative), label, actual, str(resolved), str(expected))) checked += 1 diff --git a/core/relinker/relinker/tests/test_guest_syscall_trap.py b/core/relinker/relinker/tests/test_guest_syscall_trap.py index 8a94d30f398..6971a52b6be 100644 --- a/core/relinker/relinker/tests/test_guest_syscall_trap.py +++ b/core/relinker/relinker/tests/test_guest_syscall_trap.py @@ -4,38 +4,60 @@ import sys import tempfile -from test_guest_intel_trampolines import elf_bytes_at, elf_loads, guest_fixture, main_fixture +from test_guest_intel_trampolines import elf_bytes_at, elf_loads, guest_fixture, main_fixture, pe_bytes_at, pe_sections GENERIC_STUB = bytes.fromhex( "48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 " "4c 8b 4c 24 08 0f 05 c3") NUMBERED_STUB = bytes.fromhex("48 c7 c0 09 00 00 00 49 89 ca 0f 05 c3") +MPROTECT_INLINE = bytes.fromhex( + "55 48 89 e5 53 50 89 d1 49 89 f0 48 89 fe f6 c1 04 75 31 48 63 d1 " + "b8 4a 00 00 00 48 89 f7 4c 89 c6 45 31 d2 45 31 c0 45 31 c9 " + "0f 05 0f 93 c1 48 89 c3 48 f7 db 84 c9 48 0f 45 d8 48 85 db") +MMAP_INLINE = bytes.fromhex( + "49 63 d6 4c 63 d1 4d 63 c0 b8 dd 01 00 00 48 89 de " + "0f 05 48 89 c1 0f 93 c2 48 f7 d8 84 d2 48 0f 45 c1 48 85 c0") +MMAP_EXEC_INLINE = bytes.fromhex( + "44 89 f0 83 e0 fb 48 63 d0 4c 63 d1 4d 63 c0 b8 dd 01 00 00 48 89 de " + "0f 05 0f 93 c1 49 89 c7 49 f7 df 84 c9 4c 0f 45 f8 4d 85 ff") def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-guest-syscall-trap-") as directory: work = Path(directory) - modules = work / "sce_module" - modules.mkdir() - module = bytearray(guest_fixture(b"\x90" * 6)) - module[0x400:0x400 + len(GENERIC_STUB)] = GENERIC_STUB - module[0x440:0x440 + len(NUMBERED_STUB)] = NUMBERED_STUB - (modules / "sample.prx").write_bytes(module) - source = work / "input.elf" - source.write_bytes(main_fixture()) - output = work / "output.elf" - result = subprocess.run( - [str(relinker), "--trap-sdk-syscall-stubs", str(source), str(output)], - capture_output=True, text=True, timeout=30) - assert result.returncode == 0, (result.stdout, result.stderr) - - converted = output.parent / "app0" / "sce_module" / "sample.prx.guest.prx" - image = converted.read_bytes() - loads = elf_loads(image) - assert elf_bytes_at(image, loads, 0x1017, 3) == b"\x0f\x0b\xc3" - assert elf_bytes_at(image, loads, 0x104a, 3) == b"\x0f\x0b\xc3" + for windows in (False, True): + case = work / ("windows" if windows else "linux") + modules = case / "sce_module" + modules.mkdir(parents=True) + module = bytearray(guest_fixture(b"\x90" * 6)) + for offset, stub in ((0x400, GENERIC_STUB), (0x440, NUMBERED_STUB), + (0x450, MPROTECT_INLINE), (0x490, MMAP_INLINE), (0x4b8, MMAP_EXEC_INLINE)): + module[offset:offset + len(stub)] = stub + (modules / "sample.prx").write_bytes(module) + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / ("output.exe" if windows else "output.elf") + options = ["--windows"] if windows else [] + result = subprocess.run( + [str(relinker), *options, "--trap-sdk-syscall-stubs", str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (windows, result.stdout, result.stderr) + + converted = output.parent / "app0" / "sce_module" / "sample.prx.guest.prx" + image = converted.read_bytes() + if windows: + sections = pe_sections(image) + base = next(section[1] for section in sections if section[0] == b".elf0") + converted_code = pe_bytes_at(image, sections, base, 0x100) + else: + converted_code = elf_bytes_at(image, elf_loads(image), 0x1000, 0x100) + expected = bytearray(module[0x400:0x500]) + for offset in (0x17, 0x4a, 0x7a, 0xa1, 0xcf): + assert expected[offset:offset + 2] == b"\x0f\x05" + expected[offset:offset + 2] = b"\x0f\x0b" + assert converted_code == expected, (windows, converted_code.hex(), expected.hex()) if __name__ == "__main__": diff --git a/core/relinker/relinker/tests/test_input_preservation.py b/core/relinker/relinker/tests/test_input_preservation.py index 2114abbe35d..3ec4114b305 100644 --- a/core/relinker/relinker/tests/test_input_preservation.py +++ b/core/relinker/relinker/tests/test_input_preservation.py @@ -8,6 +8,72 @@ from test_linux_desktop_entry_output import png +def check_diagnostic_inputs(relinker, work): + filename = "windows-diagnostics-imports.txt" + for bundled in (False, True): + for alias in ("direct", "hardlink", "symlink"): + for registry in (False, True): + case = work / f"diagnostics-{bundled}-{alias}-{registry}" + modules = case / "sce_module" + modules.mkdir(parents=True) + diagnostics = (modules if bundled and alias == "direct" else case) / filename + source = diagnostics if not bundled and alias == "direct" else case / "input.elf" + original = bytes(main_fixture()) + source.write_bytes(original) + module = diagnostics if bundled and alias == "direct" else modules / "sample.prx" + module_original = bytes(guest_fixture(PLAIN_SITE)) + module.write_bytes(module_original) + protected = module if bundled else source + if alias == "hardlink": + os.link(protected, diagnostics) + elif alias == "symlink": + try: + diagnostics.symlink_to(protected) + except OSError as error: + if os.name != "nt" or error.winerror != 1314: + raise + print("Diagnostic symbolic-link case skipped: Windows symlink privilege unavailable") + continue + output = case / "output.exe" + previous_output = b"previous executable" + output.write_bytes(previous_output) + registry_path = case / "output.registry.json" + previous_registry = b"previous registry" + registry_path.write_bytes(previous_registry) + options = [] if bundled else ["--skip-sce-module"] + if registry: + options.append("--registry") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", *options, + str(source), str(output)], cwd=diagnostics.parent, + capture_output=True, text=True, timeout=20) + owner = "an input module" if bundled else "the input executable" + assert result.returncode == 2 and f"Windows import diagnostics output would overwrite {owner}" in result.stderr, (result.stdout, result.stderr) + assert str(diagnostics) in result.stderr, result.stderr + assert source.read_bytes() == original and module.read_bytes() == module_original + assert diagnostics.read_bytes() == (module_original if bundled else original) + assert output.read_bytes() == previous_output and registry_path.read_bytes() == previous_registry + assert not (case / "app0").exists() + + for enabled in (False, True): + for existing in (False, True): + case = work / f"diagnostics-valid-{enabled}-{existing}" + case.mkdir() + diagnostics = case / filename + source = case / "input.elf" if enabled else diagnostics + original = bytes(main_fixture()) + source.write_bytes(original) + if enabled and existing: + diagnostics.write_bytes(b"previous diagnostics") + output = case / "output.exe" + options = ["--windows-diagnostics"] if enabled else [] + result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", *options, + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0 and output.read_bytes().startswith(b"MZ"), (result.stdout, result.stderr) + assert source.read_bytes() == original + assert diagnostics.read_bytes() == (b"" if enabled else original) + + def check_registry_inputs(relinker, work, windows): options = ["--windows"] if windows else [] for module_registry in (False, True): @@ -53,6 +119,29 @@ def check_registry_inputs(relinker, work, windows): assert not (case / "app0").exists() +def check_registry_after_failed_patch(relinker, work): + case = work / "registry-failed-patch" + (case / "sce_module").mkdir(parents=True) + (case / "sce_module" / "sample.prx").write_bytes(guest_fixture(PLAIN_SITE)) + (case / "sce_sys").mkdir() + (case / "sce_sys" / "icon0.png").write_bytes(b"\x89PNG\r\n\x1a\n" + bytes(40)) + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / "output.exe" + previous_output = b"previous executable" + output.write_bytes(previous_output) + registries = [case / "output.registry.json", case / "output.sample.prx.guest.prx.registry.json"] + previous_registry = b"previous registry" + for registry in registries: + registry.write_bytes(previous_registry) + result = subprocess.run([str(relinker), "--windows", "--registry", str(source), str(output)], + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Invalid Windows icon PNG" in result.stderr, (result.stdout, result.stderr) + assert output.read_bytes() == previous_output + assert all(registry.read_bytes() == previous_registry for registry in registries) + assert not (case / "app0").exists() + + def check_desktop_inputs(relinker, work): for desktop in (False, True): for bundled in (False, True): @@ -112,7 +201,9 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-input-preservation-") as directory: work = Path(directory) + check_diagnostic_inputs(relinker, work) check_desktop_inputs(relinker, work) + check_registry_after_failed_patch(relinker, work) for windows in (False, True): options = ["--windows"] if windows else [] check_registry_inputs(relinker, work, windows) diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 5a0d06fc3a9..cbf3d4cb366 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -79,13 +79,37 @@ def main(): 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) - source = work / "invalid-relative.elf" - source.write_bytes(image_with_relocations([noop, (0x300, (1 << 32) | 8, 0x210)], False, False)) - output = work / "invalid-relative.exe" - result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], - capture_output=True, text=True, timeout=20) - assert result.returncode == 2 and "RELATIVE relocation has a nonzero symbol index" in result.stderr - assert not output.exists() + for sce in (False, True): + image = image_with_relocations([noop, (0x300, (1 << 32) | 8, 0x210)], sce, False) + struct.pack_into("> 32) * 24) + imports.append((target, info & 0xffffffff, addend, strings[name:strings.index(b"\0", name)], bind_type, section, value, size)) + return sorted(imports) + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-null-symbol-") as directory: + work = Path(directory) + source = work / "null-entry.elf" + source.write_bytes(image_with()) + for target, options, magic in TARGETS: + for level in (0, 1): + output = work / f"null-entry-{target}-{level}.out" + result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", f"unused-filter={level}", + *options, str(source), str(output)], capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (target, level, result.stdout, result.stderr) + data = output.read_bytes() + assert data.startswith(magic), target + registry = json.loads(output.with_suffix(".registry.json").read_text(encoding="utf-8")) + assert registry == [], (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)], level + if target == "windows" and os.name == "nt": + run = subprocess.run([str(output)], capture_output=True, timeout=20) + assert run.returncode == 42, (level, run.returncode, run.stderr) + source = work / "named-unnamed.elf" + source.write_bytes(image_with(struct.pack("= 0, (name, stub.hex()) + displacement = struct.unpack_from("> 4 + return bindings + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix='anyps5-weak-imports-') as directory: + work = Path(directory) + + for name, image, symbol, expected in (('linux-weak', weak_executable('a.prx', 'absent#A#B'), 'absent', STB_WEAK), + ('linux-global', executable('a.prx', 'absent#A#B'), 'absent', STB_GLOBAL)): + source = work / (name + '.elf') + source.write_bytes(image) + output = work / (name + '.out') + result = subprocess.run([str(relinker), '--skip-sce-module', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + bindings = import_bindings(output.read_bytes()) + assert bindings.get(symbol) == expected, (name, bindings) + + for name, symbol, expected in (('windows-weak-missing', 'absent#A#B', 33), + ('windows-weak-present', 'shared#A#B', 11)): + case = work / name + (case / 'prx').mkdir(parents=True) + (case / 'prx' / 'a.prx').write_bytes(provider(11)) + source = case / 'input.elf' + source.write_bytes(weak_executable('a.prx', symbol)) + output = case / 'output.exe' + result = subprocess.run([str(relinker), '--windows', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (name, result.stdout, result.stderr) + if os.name == 'nt': + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + + print('Weak import tests passed') + + +if __name__ == '__main__': + main() diff --git a/core/relinker/relinker/tests/test_windows_runpath.py b/core/relinker/relinker/tests/test_windows_runpath.py new file mode 100644 index 00000000000..44db3f9bd22 --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_runpath.py @@ -0,0 +1,42 @@ +from pathlib import Path +import os +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_windows_import_modules import executable, provider + + +def main(): + relinker = Path(sys.argv[1]).resolve() + with tempfile.TemporaryDirectory(prefix="anyps5-windows-runpath-") as directory: + work = Path(directory) + seed = work / "seed" + (seed / "prx").mkdir(parents=True) + (seed / "prx" / "a.prx").write_bytes(provider(77)) + source = seed / "input.elf" + source.write_bytes(main_fixture()) + result = subprocess.run([str(relinker), "--windows", str(source), str(seed / "output.exe")], + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + library = (seed / "app0" / "prx" / "a.prx.guest.prx").read_bytes() + + for index, runpath in enumerate(("$ORIGIN", "$ORIGIN/", "$ORIGIN\\", "$ORIGIN/.", ".")): + case = work / str(index) + case.mkdir() + (case / "a.prx").write_bytes(library) + source = case / "input.elf" + source.write_bytes(executable("a.prx")) + output = case / "output.exe" + result = subprocess.run([str(relinker), "--windows", "--skip-sce-module", "--rpath", runpath, + str(source), str(output)], capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (runpath, result.stdout, result.stderr) + if os.name == "nt": + executed = subprocess.run([str(output)], cwd=work, capture_output=True, text=True, timeout=20) + assert executed.returncode == 77, (runpath, executed.returncode, executed.stdout, executed.stderr) + print("Windows runpath tests passed") + + +if __name__ == "__main__": + main() diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 09417b63e0e..2ec33e7ec7f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -484,11 +484,14 @@ Throughout the project, every function at every stage either **does exactly what - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_log_f32 and v_log_f16](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Dual-source blending](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. +- [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. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - [Ordered not-equal compares](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual` with both operands checked for NaN on their bits instead of `OpFOrdNotEqual`; the two agree for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of the compare feeds a ballot on a 32-wide subgroup (#825) - [sce::Job::JobManager](../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Only CPU sequences exist: `initialize` and `calculateRequiredMemorySize` with compute sequences, and both compute dispatches, are not implemented, and `createCompute` and `destroyCompute` return the errors the original returns when no compute sequence was configured. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run - 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. +- A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. The `unused-filter=1` and `unused-filter=2` readers parse the program headers themselves and do not load it. - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. +- A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. - [DB_SHADER_CONTROL.PRIMITIVE_ORDERED_PIXEL_SHADER](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the host pixel interlock spans the whole shader, not only up to `s_sendmsg` ORDERED_PS_DONE. - [POPS collision SGPRs](../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2aa4daa4a03..cddf9fde72f 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -35,7 +35,7 @@ relinker --macos source/input.elf eboot Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. -The executable output must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. An existing output file can be replaced if it is separate from those inputs. +The executable, registry, Windows import diagnostics, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. ## Options @@ -60,6 +60,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--skip-sce-module` | Deprecated. Disable bundled module processing and module directory checks. Conflicts with explicit `--sce-module-path`. | | `--exclude-sce-module ` | 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. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. There is no `--help` flag; invoking `relinker` without arguments prints the usage syntax and exits with an error. diff --git a/tools/rosetta/tests/CMakeLists.txt b/tools/rosetta/tests/CMakeLists.txt index 2b78486292a..df0e9698254 100644 --- a/tools/rosetta/tests/CMakeLists.txt +++ b/tools/rosetta/tests/CMakeLists.txt @@ -27,7 +27,9 @@ add_test_executable(rosetta_cpuid_tests ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/ClzeroOperands.cpp ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/ClzeroLowering.cpp ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/ReciprocalOperands.cpp - ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/ReciprocalLowering.cpp) + ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/ReciprocalLowering.cpp + ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/WideStoreOperands.cpp + ${ANYPS5_RELINKER_ROOT}/codegen/src/x86/WideStoreLowering.cpp) target_include_directories(rosetta_cpuid_tests PRIVATE ${ANYPS5_RELINKER_ROOT}/domain/include ${ANYPS5_RELINKER_ROOT}/codegen/include) target_compile_features(rosetta_cpuid_tests PRIVATE cxx_std_20) add_test(NAME rosetta_cpuid COMMAND rosetta_cpuid_tests)