diff --git a/README.md b/README.md index 41af87e63..e7efe9009 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Tool for automatic executables porting to Linux and Windows. Includes a [relinker](core/relinker) that converts executable to the target system's native format and implementations of [system prx libraries](core/libs/prx) suitable for dynamic linking. No emulation or separate runtime process. +`relinker --to-intel` rewrites the Zen 2-only instructions a title may contain (EXTRQ, INSERTQ, MOVNTSS, MOVNTSD) at relink time, in place when an equal-length Intel encoding exists and otherwise through a jump to a generated stub section, so Intel hosts need no runtime emulation. + Releases will be published after the first full successful launch of at least one game. ## Status diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index c556e72ad..732539688 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -7,6 +7,7 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsPeFormat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsLoadImage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsTlsBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsRelocationBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsImportBuilder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -28,6 +29,8 @@ add_executable(relinker ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionRewriter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/DecodedInstruction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/Sse4aOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/Sse4aLowering.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 @@ -69,10 +72,59 @@ if(WIN32) set_target_properties(windows_dependency_diagnostics_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) add_test(NAME windows_dependency_diagnostics COMMAND windows_dependency_diagnostics_tests) endif() + + add_executable(amd64_only_windows_tests EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/Amd64OnlyWindowsTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsLoadImage.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsStubEmitter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsPeFormat.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/windows/WindowsPeWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.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/X64InstructionRewriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/DecodedInstruction.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/Sse4aOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/Sse4aLowering.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) + set_target_properties(amd64_only_windows_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME amd64_only_windows COMMAND amd64_only_windows_tests) + endif() endif() if(BUILD_TESTING) + add_executable(amd64_only_converter_tests + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/Amd64OnlyConverterTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.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/X64InstructionRewriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/DecodedInstruction.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/Sse4aOperands.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/Sse4aLowering.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 + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SectionHeaderTableBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ) + target_include_directories(amd64_only_converter_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME amd64_only_converter COMMAND amd64_only_converter_tests) + add_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/codegen/include/codegen/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index 3d8c4cd45..51d90c0bc 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -1,6 +1,7 @@ #ifndef CODEGEN_CODEGENTYPES_HPP #define CODEGEN_CODEGENTYPES_HPP +#include #include #include #include @@ -28,9 +29,20 @@ struct RewriteResult { std::vector Adjustments; }; +struct TrampolineSite { + Domain::FileByteOffset Offset; + Domain::VirtualAddress Address; + std::size_t Length; + std::vector OriginalBytes; + std::vector Body; + std::size_t ReturnBranchOffset; +}; + struct ConvertResult { std::vector Bytes; std::size_t ReplacedCount; + std::vector Trampolines; + std::vector Reports; }; enum class ControlFlowKind : std::uint8_t { diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index 2881b934d..1ec1dbfc2 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -17,14 +17,39 @@ struct Entry { inline constexpr std::uint8_t k##name##Bytes[] = {__VA_ARGS__}; \ inline constexpr Entry k##name = {#name, k##name##Bytes, sizeof(k##name##Bytes)}; -AMD64_STUB(Monitorx, 0x0F, 0x01, 0xFA) -AMD64_STUB(Mwaitx, 0x0F, 0x01, 0xFB) -AMD64_STUB(Clzero, 0x0F, 0x01, 0xFC) -AMD64_STUB(Rdpru, 0x0F, 0x01, 0xFD) -AMD64_STUB(Mcommit, 0xF3, 0x0F, 0x01, 0xFA) +AMD64_STUB(JmpRel32, 0xE9, 0x00, 0x00, 0x00, 0x00) +AMD64_STUB(LeaRspBelowRedZone, 0x48, 0x8D, 0xA4, 0x24, 0x70, 0xFF, 0xFF, 0xFF) +AMD64_STUB(LeaRspRestore, 0x48, 0x8D, 0xA4, 0x24, 0x90, 0x00, 0x00, 0x00) +AMD64_STUB(Nop1, 0x90) +AMD64_STUB(Nop2, 0x66, 0x90) +AMD64_STUB(Nop3, 0x0F, 0x1F, 0x00) +AMD64_STUB(Nop4, 0x0F, 0x1F, 0x40, 0x00) +AMD64_STUB(Nop5, 0x0F, 0x1F, 0x44, 0x00, 0x00) +AMD64_STUB(Nop6, 0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00) +AMD64_STUB(Nop7, 0x0F, 0x1F, 0x80, 0x00, 0x00, 0x00, 0x00) #undef AMD64_STUB +inline constexpr Entry kNops[] = {kNop1, kNop2, kNop3, kNop4, kNop5, kNop6, kNop7}; + +inline constexpr Entry kMonitorx = {"MONITORX", nullptr, 0}; +inline constexpr Entry kMwaitx = {"MWAITX", nullptr, 0}; +inline constexpr Entry kClzero = {"CLZERO", nullptr, 0}; +inline constexpr Entry kRdpru = {"RDPRU", nullptr, 0}; +inline constexpr Entry kMcommit = {"MCOMMIT", 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}; +inline constexpr Entry kInsertqRegisterForm = {"INSERTQ register form", nullptr, 0}; +inline constexpr Entry kMovntss = {"MOVNTSS", nullptr, 0}; +inline constexpr Entry kMovntsd = {"MOVNTSD", nullptr, 0}; + +inline constexpr std::uint8_t kMovsStoreOpcode = 0x11; +inline constexpr std::uint8_t kPshufbZero = 0x80; +inline constexpr std::uint8_t kTrapFill = 0xCC; +inline constexpr std::size_t kStubAlignment = 16; +inline constexpr std::size_t kRedZoneSpillFrame = 0x90; + } // namespace Amd64OnlySubstitutionTable } // namespace Codegen diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp index aa062cc8e..98c740afa 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp @@ -8,10 +8,19 @@ namespace Codegen { +enum class Amd64OnlyLowering : std::uint8_t { + InPlace, + Trampoline, + Unsupported +}; + struct Amd64OnlyMatch { std::string InstructionName; std::size_t Length; + Amd64OnlyLowering Lowering; std::vector ReplacementBytes; + std::vector StubBody; + std::size_t ReturnBranchOffset; }; struct Amd64OnlySubstitutionReport { @@ -19,6 +28,7 @@ struct Amd64OnlySubstitutionReport { Domain::FileByteOffset Offset; std::size_t OriginalLength; std::size_t ReplacementLength; + Amd64OnlyLowering Lowering; }; } diff --git a/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp b/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp index ac42a23d2..5de292fac 100644 --- a/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp +++ b/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp @@ -10,6 +10,7 @@ struct DecodedInstruction { const std::uint8_t* Data; std::size_t Length; + [[nodiscard]] std::size_t OpcodeOffset() const; [[nodiscard]] bool IsShaNi() const; [[nodiscard]] bool IsExtrq() const; [[nodiscard]] bool IsInsertq() const; diff --git a/core/relinker/codegen/include/codegen/x86/Sse4aLowering.hpp b/core/relinker/codegen/include/codegen/x86/Sse4aLowering.hpp new file mode 100644 index 000000000..6064b9cca --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/Sse4aLowering.hpp @@ -0,0 +1,25 @@ +#ifndef CODEGEN_X86_SSE4ALOWERING_HPP +#define CODEGEN_X86_SSE4ALOWERING_HPP + +#include +#include +#include +#include +#include + +namespace Codegen { + +struct LoweredBody { + std::vector Bytes; + std::size_t ReturnBranchOffset; +}; + +class Sse4aLowering { +public: + [[nodiscard]] std::optional> LowerInPlace(const Sse4aOperands& operands, std::size_t originalLength) const; + [[nodiscard]] LoweredBody LowerOutOfLine(const Sse4aOperands& operands) const; +}; + +} + +#endif diff --git a/core/relinker/codegen/include/codegen/x86/Sse4aOperands.hpp b/core/relinker/codegen/include/codegen/x86/Sse4aOperands.hpp new file mode 100644 index 000000000..6e4322f9f --- /dev/null +++ b/core/relinker/codegen/include/codegen/x86/Sse4aOperands.hpp @@ -0,0 +1,22 @@ +#ifndef CODEGEN_X86_SSE4AOPERANDS_HPP +#define CODEGEN_X86_SSE4AOPERANDS_HPP + +#include +#include + +namespace Codegen { + +struct Sse4aOperands { + bool Insertq; + bool RegisterForm; + std::uint8_t Destination; + std::uint8_t Source; + std::uint8_t Length; + std::uint8_t Index; +}; + +[[nodiscard]] Sse4aOperands DecodeSse4a(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 69bc3598f..2472fe258 100644 --- a/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp +++ b/core/relinker/codegen/include/codegen/x86/X64OpcodeConstants.hpp @@ -106,6 +106,10 @@ inline constexpr std::uint8_t OneByteLoop = 0xE0; inline constexpr std::uint8_t OneByteLoopMax = 0xE2; inline constexpr std::uint8_t OneByteInOutImm8Min = 0xE4; inline constexpr std::uint8_t OneByteInOutImm8Max = 0xE7; +inline constexpr std::uint8_t OneByteInt = 0xCD; +inline constexpr std::uint8_t OneByteRetImm16 = 0xC2; +inline constexpr std::uint8_t OneByteRetFarImm16 = 0xCA; +inline constexpr std::uint8_t OneByteEnter = 0xC8; inline constexpr std::uint8_t TwoByteJccRel32Min = 0x80; inline constexpr std::uint8_t TwoByteJccRel32Max = 0x8F; @@ -158,6 +162,19 @@ inline constexpr std::uint8_t TwoByteShufpdImm8 = 0xC6; inline constexpr std::uint8_t TwoByteGrp9 = 0xC7; inline constexpr std::uint8_t TwoByteExtrqInsertqImm8Imm8 = 0x78; inline constexpr std::uint8_t TwoByteExtrqInsertqModRm = 0x79; +inline constexpr std::uint8_t TwoByteMovnts = 0x2B; +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 TwoByteMovCrDrMin = 0x20; +inline constexpr std::uint8_t TwoByteMovCrDrMax = 0x23; +inline constexpr std::uint8_t TwoBytePopcnt = 0xB8; +inline constexpr std::uint8_t TwoByteUd1 = 0xB9; +inline constexpr std::uint8_t TwoByteBtc = 0xBB; +inline constexpr std::uint8_t TwoByteXaddRm8 = 0xC0; +inline constexpr std::uint8_t TwoByteMovnti = 0xC3; +inline constexpr std::uint8_t TwoBytePinsrw = 0xC4; inline constexpr std::uint8_t VexNoModRmMin = 0x77; inline constexpr std::uint8_t VexNoModRmMax = 0x77; @@ -171,6 +188,7 @@ inline constexpr std::uint8_t EvexMap0F38 = 0x02; inline constexpr std::size_t ImmSizeNone = 0; inline constexpr std::size_t ImmSize8 = 1; inline constexpr std::size_t ImmSize16 = 2; +inline constexpr std::size_t ImmSizeEnter = 3; inline constexpr std::size_t ImmSize32 = 4; inline constexpr std::size_t ImmSize64 = 8; diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index 74f624611..41cd3c211 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -1,13 +1,28 @@ #include #include +#include +#include +#include #include #include +#include #include +#include +#include namespace Codegen { namespace { +template +auto _atFileOffset(const Domain::FileByteOffset base, const TOperation& operation) { + try { + return operation(); + } catch (const CodegenException& e) { + throw CodegenException(e.what(), base + e.FailureOffset); + } +} + class Amd64OnlyConverter : public IAmd64OnlyConverter { public: [[nodiscard]] ConvertResult Convert( @@ -16,64 +31,132 @@ public: ) const override; private: + struct Pending { + InstructionMatch Instruction; + Amd64OnlyMatch Substitution; + }; + X64InstructionRewriter _rewriter; std::unique_ptr _matcher = MakeAmd64OnlyInstructionMatcher(); std::unique_ptr _scanner = MakeInstructionScanner(); - std::size_t _convertSegment( + void _convertSegment( std::vector& fileBytes, - const Domain::ProgramHeader& ph + const Domain::ProgramHeader& ph, + ConvertResult& result ) const; + + [[nodiscard]] static std::set _collectBranchTargets( + const std::vector& seg, + const std::vector& matches, + const Domain::ProgramHeader& ph + ); }; -std::size_t Amd64OnlyConverter::_convertSegment( - std::vector& fileBytes, +std::set Amd64OnlyConverter::_collectBranchTargets( + const std::vector& seg, + const std::vector& matches, const Domain::ProgramHeader& ph +) { + 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)); + } + return targets; +} + +void Amd64OnlyConverter::_convertSegment( + std::vector& fileBytes, + const Domain::ProgramHeader& ph, + ConvertResult& result ) const { const auto segOffset = static_cast(ph.Offset); const auto segSize = static_cast(ph.FileSize); + if (segOffset > fileBytes.size() || segSize > fileBytes.size() - segOffset) + throw CodegenException("Code segment exceeds the file", ph.Offset); std::vector seg( - fileBytes.begin() + segOffset, - fileBytes.begin() + segOffset + segSize + fileBytes.begin() + static_cast(segOffset), + fileBytes.begin() + static_cast(segOffset + segSize) ); - const auto matches = _scanner->ScanCodeSection(seg, 0, seg.size()); - std::int64_t shift = 0; - std::size_t count = 0; + const auto matches = _atFileOffset(ph.Offset, [&] { return _scanner->ScanCodeSection(seg, 0, seg.size()); }); + std::vector pending; for (const auto& match : matches) { - const auto shiftedOffset = static_cast(match.Offset) + shift; - const auto offset = static_cast(shiftedOffset); - const auto result = _matcher->Match(seg.data() + offset, match.Length); - if (!result.has_value()) { - continue; + auto substitution = _atFileOffset(ph.Offset + match.Offset, [&] { return _matcher->Match(seg.data() + match.Offset, match.Length); }); + if (substitution.has_value()) + pending.push_back({match, std::move(*substitution)}); + } + if (pending.empty()) + return; + + 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{}; + + for (const auto& item : pending) { + const auto& match = item.Instruction; + const auto& substitution = item.Substitution; + const auto fileOffset = static_cast(ph.Offset + match.Offset); + const auto address = static_cast(ph.MappedAddress + match.Offset); + std::size_t replacementLength = 0; + + switch (substitution.Lowering) { + case Amd64OnlyLowering::InPlace: { + if (substitution.ReplacementBytes.size() != match.Length) + throw CodegenException("Intel substitution changes the instruction length", fileOffset); + seg = _atFileOffset(ph.Offset, [&] { return _rewriter.Rewrite(seg, {match.Offset, substitution.ReplacementBytes}).Bytes; }); + replacementLength = substitution.ReplacementBytes.size(); + ++result.ReplacedCount; + break; } - const RewriteRequest request{ - static_cast(offset), - result->ReplacementBytes - }; - auto rewriteResult = _rewriter.Rewrite(seg, request); - shift += static_cast(result->ReplacementBytes.size()) - - static_cast(match.Length); - seg = std::move(rewriteResult.Bytes); - ++count; + case Amd64OnlyLowering::Trampoline: { + if (match.Length < Amd64OnlySubstitutionTable::kJmpRel32.Size) + throw CodegenException("AMD-only instruction too short for a jump", fileOffset); + const auto hit = branchTargets.upper_bound(address); + if (hit != branchTargets.end() && *hit < address + match.Length) + throw CodegenException("Branch enters an AMD-only instruction", ph.Offset + (*hit - ph.MappedAddress)); + const auto begin = seg.begin() + static_cast(match.Offset); + result.Trampolines.push_back({ + fileOffset, + address, + match.Length, + std::vector(begin, begin + static_cast(match.Length)), + substitution.StubBody, + substitution.ReturnBranchOffset + }); + replacementLength = substitution.StubBody.size(); + break; + } + case Amd64OnlyLowering::Unsupported: + throw CodegenException("AMD-only instruction without Intel lowering: " + substitution.InstructionName, fileOffset); + } + + result.Reports.push_back({substitution.InstructionName, fileOffset, match.Length, replacementLength, substitution.Lowering}); } - const auto copySize = std::min(seg.size(), segSize); - std::copy(seg.begin(), seg.begin() + copySize, fileBytes.begin() + segOffset); - return count; + if (seg.size() != segSize) + throw CodegenException("Code segment size changed during Intel conversion", ph.Offset); + std::copy(seg.begin(), seg.end(), fileBytes.begin() + static_cast(segOffset)); } ConvertResult Amd64OnlyConverter::Convert( std::vector fileBytes, const std::vector& codeSegments ) const { - std::size_t total = 0; - for (const auto& ph : codeSegments) { - total += _convertSegment(fileBytes, ph); - } - return {std::move(fileBytes), total}; + ConvertResult result{{}, 0, {}, {}}; + for (const auto& ph : codeSegments) + _convertSegment(fileBytes, ph, result); + result.Bytes = std::move(fileBytes); + return result; } } diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index aaa7ee994..f76cfeb6f 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -1,7 +1,9 @@ #include +#include #include #include #include +#include namespace Codegen { @@ -29,7 +31,12 @@ std::vector InstructionScanner::ScanCodeSection( const std::uint8_t* cursor = codeSection.data() + i; const std::size_t available = limit - i; - const std::size_t length = decoder.Decode(cursor, available); + std::size_t length = 0; + try { + length = decoder.Decode(cursor, available); + } catch (const CodegenException& e) { + throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); + } matches.push_back({codeSectionOffset + i, length}); diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 70ecb7e3d..f371299d3 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -1,14 +1,20 @@ #include #include #include +#include +#include +#include +#include #include namespace Codegen { namespace { -std::vector _bytesOf(const Amd64OnlySubstitutionTable::Entry& e) { - return std::vector(e.Bytes, e.Bytes + e.Size); +using namespace Amd64OnlySubstitutionTable; + +Amd64OnlyMatch _unsupported(const Entry& entry, const std::size_t length) { + return Amd64OnlyMatch{entry.Name, length, Amd64OnlyLowering::Unsupported, {}, {}, 0}; } class Amd64OnlyInstructionMatcher : public IAmd64OnlyInstructionMatcher { @@ -17,34 +23,69 @@ public: const std::uint8_t* data, std::size_t length ) const override; + +private: + Sse4aLowering _lowering; + + [[nodiscard]] Amd64OnlyMatch _matchMovnts(const DecodedInstruction& instr, const Entry& entry) const; + [[nodiscard]] Amd64OnlyMatch _matchSse4a(const DecodedInstruction& instr, const Entry& entry, const Entry& registerFormEntry) const; }; +Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const { + using namespace X64OpcodeConstants; + const auto opcodeOffset = instr.OpcodeOffset(); + if (opcodeOffset + 2 >= instr.Length) + throw CodegenException("MOVNTSS/MOVNTSD truncated before its ModRM byte"); + const auto modrm = instr.Data[opcodeOffset + 2]; + if (((modrm >> ModRmModShift) & ModRmModMask) == ModRmModRegister) + throw CodegenException("MOVNTSS/MOVNTSD with a register operand"); + std::vector replacement(instr.Data, instr.Data + instr.Length); + replacement[opcodeOffset + 1] = kMovsStoreOpcode; + return Amd64OnlyMatch{entry.Name, instr.Length, Amd64OnlyLowering::InPlace, std::move(replacement), {}, 0}; +} + +Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchSse4a(const DecodedInstruction& instr, const Entry& entry, const Entry& registerFormEntry) const { + const auto operands = DecodeSse4a(instr.Data, instr.Length); + if (operands.RegisterForm) + return _unsupported(registerFormEntry, instr.Length); + if (auto inPlace = _lowering.LowerInPlace(operands, instr.Length)) + return Amd64OnlyMatch{entry.Name, instr.Length, Amd64OnlyLowering::InPlace, std::move(*inPlace), {}, 0}; + auto body = _lowering.LowerOutOfLine(operands); + return Amd64OnlyMatch{entry.Name, instr.Length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset}; +} + std::optional Amd64OnlyInstructionMatcher::Match( const std::uint8_t* data, std::size_t length ) const { - using namespace Amd64OnlySubstitutionTable; const DecodedInstruction instr{data, length}; - if (instr.IsMonitorx()) { - return Amd64OnlyMatch{kMonitorx.Name, length, _bytesOf(kMonitorx)}; - } + if (instr.IsMovntss()) + return _matchMovnts(instr, kMovntss); - if (instr.IsMwaitx()) { - return Amd64OnlyMatch{kMwaitx.Name, length, _bytesOf(kMwaitx)}; - } + if (instr.IsMovntsd()) + return _matchMovnts(instr, kMovntsd); - if (instr.IsClzero()) { - return Amd64OnlyMatch{kClzero.Name, length, _bytesOf(kClzero)}; - } + if (instr.IsExtrq()) + return _matchSse4a(instr, kExtrq, kExtrqRegisterForm); - if (instr.IsRdpru()) { - return Amd64OnlyMatch{kRdpru.Name, length, _bytesOf(kRdpru)}; - } + if (instr.IsInsertq()) + return _matchSse4a(instr, kInsertq, kInsertqRegisterForm); - if (instr.IsMcommit()) { - return Amd64OnlyMatch{kMcommit.Name, length, _bytesOf(kMcommit)}; - } + if (instr.IsMonitorx()) + return _unsupported(kMonitorx, length); + + if (instr.IsMwaitx()) + return _unsupported(kMwaitx, length); + + if (instr.IsClzero()) + return _unsupported(kClzero, length); + + if (instr.IsRdpru()) + return _unsupported(kRdpru, length); + + if (instr.IsMcommit()) + return _unsupported(kMcommit, length); return std::nullopt; } diff --git a/core/relinker/codegen/src/x86/DecodedInstruction.cpp b/core/relinker/codegen/src/x86/DecodedInstruction.cpp index 3ee2a1285..215d6890b 100644 --- a/core/relinker/codegen/src/x86/DecodedInstruction.cpp +++ b/core/relinker/codegen/src/x86/DecodedInstruction.cpp @@ -58,6 +58,13 @@ std::size_t DecodedInstruction::_skipPrefixesAndRex( return pos; } +std::size_t DecodedInstruction::OpcodeOffset() const { + bool operandSizeOverride = false; + bool repnePrefix = false; + bool repPrefix = false; + return _skipPrefixesAndRex(&operandSizeOverride, &repnePrefix, &repPrefix); +} + bool DecodedInstruction::IsShaNi() const { bool operandSizeOverride = false; bool repnePrefix = false; diff --git a/core/relinker/codegen/src/x86/Sse4aLowering.cpp b/core/relinker/codegen/src/x86/Sse4aLowering.cpp new file mode 100644 index 000000000..758028a36 --- /dev/null +++ b/core/relinker/codegen/src/x86/Sse4aLowering.cpp @@ -0,0 +1,214 @@ +#include +#include +#include +#include +#include +#include + +namespace Codegen { + +namespace { + +using namespace Amd64OnlySubstitutionTable; + +using Constant = std::array; + +constexpr std::uint8_t kPrefixPacked = 0x66; +constexpr std::uint8_t kPrefixScalar = 0xF3; +constexpr std::uint8_t kRexBase = 0x40; +constexpr std::uint8_t kRexR = 0x04; +constexpr std::uint8_t kRexB = 0x01; +constexpr std::uint8_t kModRmRegister = 0xC0; +constexpr std::uint8_t kModRmRip = 0x05; +constexpr std::uint8_t kModRmRspBase = 0x04; +constexpr std::uint8_t kSibRsp = 0x24; +constexpr std::uint8_t kShiftRight = 2; +constexpr std::uint8_t kShiftLeft = 6; +constexpr std::uint8_t kFieldBits = 64; + +std::uint8_t _rex(const std::uint8_t reg, const std::uint8_t rm) { + return static_cast(kRexBase | ((reg & 8) != 0 ? kRexR : 0) | ((rm & 8) != 0 ? kRexB : 0)); +} + +void _emit(std::vector& out, const std::uint8_t prefix, const std::uint8_t reg, const std::uint8_t rm, const std::initializer_list opcode, const std::uint8_t modrm) { + out.push_back(prefix); + const auto rex = _rex(reg, rm); + if (rex != kRexBase) + out.push_back(rex); + out.insert(out.end(), opcode.begin(), opcode.end()); + out.push_back(modrm); +} + +void _sse(std::vector& out, const std::uint8_t prefix, const std::initializer_list opcode, const std::uint8_t dst, const std::uint8_t src) { + _emit(out, prefix, dst, src, opcode, static_cast(kModRmRegister | ((dst & 7) << 3) | (src & 7))); +} + +void _shiftImm(std::vector& out, const std::uint8_t extension, const std::uint8_t reg, const std::uint8_t imm) { + _emit(out, kPrefixPacked, 0, reg, {0x0F, 0x73}, static_cast(kModRmRegister | (extension << 3) | (reg & 7))); + out.push_back(imm); +} + +void _nopFill(std::vector& out, std::size_t count) { + while (count > 0) { + const auto& nop = kNops[std::min(count, std::size(kNops)) - 1]; + out.insert(out.end(), nop.Bytes, nop.Bytes + nop.Size); + count -= nop.Size; + } +} + +std::uint64_t _fieldMask(const std::uint8_t length) { + return length >= kFieldBits ? ~std::uint64_t{0} : ((std::uint64_t{1} << length) - 1); +} + +class BodyBuilder { +public: + void Sse(const std::uint8_t prefix, const std::initializer_list opcode, const std::uint8_t dst, const std::uint8_t src) { + _sse(_bytes, prefix, opcode, dst, src); + } + + void ShiftImm(const std::uint8_t extension, const std::uint8_t reg, const std::uint8_t imm) { + _shiftImm(_bytes, extension, reg, imm); + } + + void RipOperand(const std::initializer_list opcode, const std::uint8_t reg, const Constant& constant) { + _emit(_bytes, kPrefixPacked, reg, 0, opcode, static_cast(((reg & 7) << 3) | kModRmRip)); + _fixups.push_back({_bytes.size(), _bytes.size() + 4, _constants.size()}); + _bytes.insert(_bytes.end(), 4, 0); + _constants.push_back(constant); + } + + void Spill(const std::uint8_t reg) { + _bytes.insert(_bytes.end(), kLeaRspBelowRedZone.Bytes, kLeaRspBelowRedZone.Bytes + kLeaRspBelowRedZone.Size); + _emit(_bytes, kPrefixScalar, reg, 0, {0x0F, 0x7F}, static_cast(((reg & 7) << 3) | kModRmRspBase)); + _bytes.push_back(kSibRsp); + } + + void Restore(const std::uint8_t reg) { + _emit(_bytes, kPrefixScalar, reg, 0, {0x0F, 0x6F}, static_cast(((reg & 7) << 3) | kModRmRspBase)); + _bytes.push_back(kSibRsp); + _bytes.insert(_bytes.end(), kLeaRspRestore.Bytes, kLeaRspRestore.Bytes + kLeaRspRestore.Size); + } + + LoweredBody Finish() { + const auto returnBranchOffset = _bytes.size(); + _bytes.insert(_bytes.end(), kJmpRel32.Bytes, kJmpRel32.Bytes + kJmpRel32.Size); + std::vector constantOffsets; + for (const auto& constant : _constants) { + while (_bytes.size() % kStubAlignment != 0) + _bytes.push_back(kTrapFill); + constantOffsets.push_back(_bytes.size()); + _bytes.insert(_bytes.end(), constant.begin(), constant.end()); + } + for (const auto& fixup : _fixups) { + const auto displacement = static_cast(constantOffsets[fixup.ConstantIndex]) - static_cast(fixup.InstructionEnd); + const auto value = static_cast(static_cast(displacement)); + for (std::size_t index = 0; index < 4; ++index) + _bytes[fixup.DisplacementOffset + index] = static_cast(value >> (index * 8)); + } + return {std::move(_bytes), returnBranchOffset}; + } + +private: + struct Fixup { + std::size_t DisplacementOffset; + std::size_t InstructionEnd; + std::size_t ConstantIndex; + }; + + std::vector _bytes; + std::vector _fixups; + std::vector _constants; +}; + +} + +std::optional> Sse4aLowering::LowerInPlace(const Sse4aOperands& operands, const std::size_t originalLength) const { + if (operands.RegisterForm) + return std::nullopt; + const auto length = operands.Length; + const auto index = operands.Index; + const auto dst = operands.Destination; + const auto src = operands.Source; + std::vector sequence; + if (operands.Insertq) { + if (dst == src && index == 0) { + } else if (length == kFieldBits && index == 0) { + _sse(sequence, kPrefixScalar, {0x0F, 0x7E}, dst, src); + } else if (index == 0 && length % 16 == 0) { + _sse(sequence, kPrefixPacked, {0x0F, 0x3A, 0x0E}, dst, src); + sequence.push_back(static_cast((1u << (length / 16)) - 1)); + } else { + return std::nullopt; + } + } else { + if (index == 0 && length == kFieldBits) { + } else if (index + length == kFieldBits) { + _shiftImm(sequence, kShiftRight, dst, index); + } else if (index == 0 && (length == 8 || length == 16 || length == 32)) { + const std::uint8_t opcode = length == 8 ? 0x32 : (length == 16 ? 0x34 : 0x35); + _sse(sequence, kPrefixPacked, {0x0F, 0x38, opcode}, dst, dst); + } else { + return std::nullopt; + } + } + if (sequence.size() > originalLength) + return std::nullopt; + _nopFill(sequence, originalLength - sequence.size()); + return sequence; +} + +LoweredBody Sse4aLowering::LowerOutOfLine(const Sse4aOperands& operands) const { + if (operands.RegisterForm) + throw CodegenException("EXTRQ/INSERTQ register form has no Intel lowering"); + const auto length = operands.Length; + const auto index = operands.Index; + const auto dst = operands.Destination; + const auto src = operands.Source; + const bool byteAligned = length % 8 == 0 && index % 8 == 0; + BodyBuilder body; + if (!operands.Insertq) { + if (byteAligned) { + Constant mask; + mask.fill(kPshufbZero); + for (std::size_t byte = 0; byte < length / 8; ++byte) + mask[byte] = static_cast(index / 8 + byte); + body.RipOperand({0x0F, 0x38, 0x00}, dst, mask); + } else { + if (index != 0) + body.ShiftImm(kShiftRight, dst, index); + if (length != kFieldBits) { + body.ShiftImm(kShiftLeft, dst, static_cast(kFieldBits - length)); + body.ShiftImm(kShiftRight, dst, static_cast(kFieldBits - length)); + } + } + } else if (byteAligned) { + if (src != dst) + body.Sse(kPrefixPacked, {0x0F, 0x6C}, dst, src); + Constant mask; + mask.fill(kPshufbZero); + for (std::size_t byte = 0; byte < 8; ++byte) + mask[byte] = static_cast(byte); + for (std::size_t byte = index / 8; byte < index / 8 + length / 8; ++byte) + mask[byte] = static_cast((src != dst ? 8 : 0) + (byte - index / 8)); + body.RipOperand({0x0F, 0x38, 0x00}, dst, mask); + } else { + std::uint8_t scratch = 0; + while (scratch == dst || scratch == src) + ++scratch; + body.Spill(scratch); + body.Sse(kPrefixPacked, {0x0F, 0x6F}, scratch, src); + if (index != 0) + body.ShiftImm(kShiftLeft, scratch, index); + body.Sse(kPrefixPacked, {0x0F, 0xEF}, scratch, dst); + Constant hole{}; + const auto holeMask = _fieldMask(length) << index; + for (std::size_t byte = 0; byte < 8; ++byte) + hole[byte] = static_cast(holeMask >> (byte * 8)); + body.RipOperand({0x0F, 0xDB}, scratch, hole); + body.Sse(kPrefixPacked, {0x0F, 0xEF}, dst, scratch); + body.Restore(scratch); + } + return body.Finish(); +} + +} diff --git a/core/relinker/codegen/src/x86/Sse4aOperands.cpp b/core/relinker/codegen/src/x86/Sse4aOperands.cpp new file mode 100644 index 000000000..1e6feb422 --- /dev/null +++ b/core/relinker/codegen/src/x86/Sse4aOperands.cpp @@ -0,0 +1,84 @@ +#include +#include +#include + +namespace Codegen { + +using namespace X64OpcodeConstants; + +Sse4aOperands DecodeSse4a(const std::uint8_t* data, const std::size_t length) { + std::size_t pos = 0; + bool operandSizeOverride = false; + bool repnePrefix = false; + + while (pos < length) { + const std::uint8_t b = data[pos]; + if (b == PrefixOperandSize) { + operandSizeOverride = true; + } else if (b == PrefixRepne) { + repnePrefix = true; + } else if (b != PrefixLock && b != PrefixRep && b != PrefixAddressSize && + b != PrefixSegCs && b != PrefixSegSs && b != PrefixSegDs && + b != PrefixSegEs && b != PrefixSegFs && b != PrefixSegGs) { + break; + } + pos += 1; + } + + std::uint8_t rex = 0; + if (pos < length && data[pos] >= RexMin && data[pos] <= RexMax) { + rex = data[pos]; + pos += 1; + } + + if (pos + 3 > length || data[pos] != TwoByteOpcodeEscape || + (data[pos + 1] != TwoByteExtrqInsertqImm8Imm8 && data[pos + 1] != TwoByteExtrqInsertqModRm) || + (!operandSizeOverride && !repnePrefix)) { + throw CodegenException("Not an SSE4a instruction"); + } + + const std::uint8_t opcode = data[pos + 1]; + const std::uint8_t modrm = data[pos + 2]; + pos += 3; + + if (((modrm >> ModRmModShift) & ModRmModMask) != ModRmModRegister) { + throw CodegenException("SSE4a instruction with a memory operand"); + } + + const auto regField = static_cast((modrm >> ModRmRegShift) & ModRmRegMask); + const auto reg = static_cast(regField | (((rex & 0x4) != 0) ? 8 : 0)); + const auto rm = static_cast((modrm & ModRmRmMask) | (((rex & 0x1) != 0) ? 8 : 0)); + + Sse4aOperands operands{}; + operands.Insertq = repnePrefix; + operands.RegisterForm = opcode == TwoByteExtrqInsertqModRm; + operands.Destination = reg; + operands.Source = rm; + + if (operands.RegisterForm) { + return operands; + } + + if (pos + 2 > length) { + throw CodegenException("SSE4a instruction truncated before its immediates"); + } + + if (!operands.Insertq) { + if (regField != 0) { + throw CodegenException("EXTRQ immediate form with a non-zero reg field"); + } + operands.Destination = rm; + } + + const auto rawLength = static_cast(data[pos] & 0x3F); + operands.Length = rawLength == 0 ? 64 : rawLength; + operands.Index = static_cast(data[pos + 1] & 0x3F); + + if (static_cast(operands.Length) + operands.Index > 64) { + throw CodegenException("SSE4a field exceeds 64 bits"); + } + + return operands; +} + +} diff --git a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp index 85c42cb9c..fe0ee7ef9 100644 --- a/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionDecoder.cpp @@ -202,9 +202,18 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t if (opcode == OneByteJmpRel8 || opcode == OneByteJrcxz || (opcode >= OneByteLoop && opcode <= OneByteLoopMax) || - (opcode >= OneByteInOutImm8Min && opcode <= OneByteInOutImm8Max)) { + (opcode >= OneByteInOutImm8Min && opcode <= OneByteInOutImm8Max) || + opcode == OneByteInt) { immediateSize = ImmSize8; } + + if (opcode == OneByteRetImm16 || opcode == OneByteRetFarImm16) { + immediateSize = ImmSize16; + } + + if (opcode == OneByteEnter) { + immediateSize = ImmSizeEnter; + } } else { if (opcode == TwoByteExtrqInsertqImm8Imm8) { if (!operandSizeOverride && !repnePrefix) { @@ -227,10 +236,15 @@ std::size_t X64InstructionDecoder::Decode(const std::uint8_t* data, std::size_t immediateSize = ImmSize8; } else if (opcode == TwoByteShldImm8 || opcode == TwoByteShrdImm8 || opcode == TwoByteShufpsImm8 || opcode == TwoByteShufpdImm8 || - opcode == TwoBytePextrw) { + opcode == TwoBytePextrw || opcode == TwoBytePinsrw) { hasModRm = true; immediateSize = ImmSize8; - } else if (opcode == TwoByteShldCl || opcode == TwoByteShrdCl) { + } else if (opcode == TwoByteShldCl || opcode == TwoByteShrdCl || + opcode == TwoBytePopcnt || opcode == TwoByteUd1 || opcode == TwoByteBtc || + opcode == TwoBytePrefetchw || opcode == TwoByteXaddRm8 || + opcode == TwoByteMovnti || opcode == TwoByteGrp6 || + opcode == TwoByteLar || opcode == TwoByteLsl || + (opcode >= TwoByteMovCrDrMin && opcode <= TwoByteMovCrDrMax)) { hasModRm = true; } else if ((opcode >= TwoByteCmovRangeMin && opcode <= TwoByteCmovRangeMax) || (opcode >= TwoByteModRmRangeAMin && opcode <= TwoByteModRmRangeAMax) || @@ -503,7 +517,13 @@ DecodedInstructionInfo X64InstructionDecoder::DecodeInstruction( (op >= TwoByteShiftImm8Min && op <= TwoByteShiftImm8Max) || op == TwoByteShldImm8 || op == TwoByteShrdImm8 || op == TwoByteShufpsImm8 || op == TwoByteShufpdImm8 || - op == TwoBytePextrw || op == TwoByteShldCl || op == TwoByteShrdCl) + op == TwoBytePextrw || op == TwoBytePinsrw || + op == TwoByteShldCl || op == TwoByteShrdCl || + op == TwoBytePopcnt || op == TwoByteUd1 || op == TwoByteBtc || + op == TwoBytePrefetchw || op == TwoByteXaddRm8 || + op == TwoByteMovnti || op == TwoByteGrp6 || + op == TwoByteLar || op == TwoByteLsl || + (op >= TwoByteMovCrDrMin && op <= TwoByteMovCrDrMax)) hasModRm = true; } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp new file mode 100644 index 000000000..88a57717d --- /dev/null +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -0,0 +1,339 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; + +void require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void requireFailure(const std::function& operation, const char* message) { + try { + operation(); + } catch (const Codegen::CodegenException&) { + return; + } catch (const Domain::RelinkerException&) { + return; + } + throw std::runtime_error(message); +} + +Domain::FileByteOffset failureOffset(const std::function& operation, const char* message) { + try { + operation(); + } catch (const Codegen::CodegenException& error) { + return error.FailureOffset; + } + throw std::runtime_error(message); +} + +template +void write(Bytes& bytes, std::size_t offset, TValue value) { + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("Test fixture write is out of bounds"); + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +template +TValue read(const Bytes& bytes, std::size_t offset) { + TValue value; + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("Test fixture read is out of bounds"); + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +Bytes withReturn(Bytes body, const std::size_t returnBranchOffset) { + body.at(returnBranchOffset) = 0xE9; + for (std::size_t index = 1; index <= 4; ++index) body.at(returnBranchOffset + index) = 0; + return body; +} + +const Bytes kExtrqSite = {0x66, 0x0F, 0x78, 0xC3, 0x08, 0x28}; +const Bytes kInsertqSelfSite = {0xF2, 0x0F, 0x78, 0xDB, 0x08, 0x08}; +const Bytes kInsertqCrossSite = {0xF2, 0x0F, 0x78, 0xC8, 0x08, 0x00}; +const Bytes kInsertqHighSite = {0xF2, 0x44, 0x0F, 0x78, 0xCC, 0x10, 0x10}; +const Bytes kInsertqWordSite = {0xF2, 0x0F, 0x78, 0xDC, 0x10, 0x10}; + +const Bytes kExtrqBody = { + 0x66, 0x0F, 0x38, 0x00, 0x1D, 0x07, 0x00, 0x00, 0x00, 0xE9, 0x00, 0x00, 0x00, 0x00, 0xCC, 0xCC, + 0x05, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}; +const Bytes kInsertqSelfBody = { + 0x66, 0x0F, 0x38, 0x00, 0x1D, 0x07, 0x00, 0x00, 0x00, 0xE9, 0x00, 0x00, 0x00, 0x00, 0xCC, 0xCC, + 0x00, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}; +const Bytes kInsertqCrossBody = { + 0x66, 0x0F, 0x6C, 0xC8, 0x66, 0x0F, 0x38, 0x00, 0x0D, 0x13, 0x00, 0x00, 0x00, 0xE9, 0x00, 0x00, + 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, + 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}; +const Bytes kInsertqHighBody = { + 0x66, 0x44, 0x0F, 0x6C, 0xCC, 0x66, 0x44, 0x0F, 0x38, 0x00, 0x0D, 0x11, 0x00, 0x00, 0x00, 0xE9, + 0x00, 0x00, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, + 0x00, 0x01, 0x08, 0x09, 0x04, 0x05, 0x06, 0x07, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}; +const Bytes kInsertqWordBody = { + 0x66, 0x0F, 0x6C, 0xDC, 0x66, 0x0F, 0x38, 0x00, 0x1D, 0x13, 0x00, 0x00, 0x00, 0xE9, 0x00, 0x00, + 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, + 0x00, 0x01, 0x08, 0x09, 0x04, 0x05, 0x06, 0x07, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}; + +void decoderLengths() { + const Codegen::X64InstructionDecoder decoder; + const std::vector instructions = { + kExtrqSite, kInsertqSelfSite, kInsertqCrossSite, kInsertqHighSite, kInsertqWordSite, + {0x66, 0x0F, 0x79, 0xCA}, {0xF2, 0x0F, 0x79, 0xCA}, {0x66, 0x45, 0x0F, 0x79, 0xCA}, + {0xF3, 0x0F, 0xB8, 0xC0}, {0xCD, 0x41}, {0x0F, 0x0D, 0x08}, {0x0F, 0xC0, 0xC1}, {0x0F, 0xC3, 0x07}, + {0x66, 0x0F, 0xC4, 0xC0, 0x01}, {0xC2, 0x08, 0x00}, {0xC8, 0x10, 0x00, 0x00}, {0xF3, 0x0F, 0x2B, 0x07}, + {0xF2, 0x44, 0x0F, 0x2B, 0x4C, 0x24, 0x10}, {0x0F, 0x01, 0xFA}, {0x0F, 0xB9, 0x00}, + {0x41, 0x0F, 0xBB, 0xF7}, {0x0F, 0xBB, 0x47, 0x08}}; + Bytes padded; + for (const auto& instruction : instructions) { + padded = instruction; + padded.insert(padded.end(), 8, 0x90); + require(decoder.Decode(padded.data(), padded.size()) == instruction.size(), "AMD-only or repaired two-byte opcode was decoded with the wrong length"); + } + requireFailure([&] { const Bytes bare = {0x0F, 0x78, 0xC3, 0x08, 0x28}; (void)decoder.Decode(bare.data(), bare.size()); }, "0F 78 without an SSE4a prefix was accepted"); +} + +void sse4aOperands() { + const auto check = [](const Bytes& site, const bool insertq, const int dst, const int src, const int length, const int index) { + const auto operands = Codegen::DecodeSse4a(site.data(), site.size()); + require(operands.Insertq == insertq && !operands.RegisterForm && operands.Destination == dst && operands.Source == src && operands.Length == length && operands.Index == index, "SSE4a operands were decoded incorrectly"); + }; + check(kExtrqSite, false, 3, 3, 8, 40); + check(kInsertqSelfSite, true, 3, 3, 8, 8); + check(kInsertqCrossSite, true, 1, 0, 8, 0); + check(kInsertqHighSite, true, 9, 4, 16, 16); + check(kInsertqWordSite, true, 3, 4, 16, 16); + const Bytes fullField = {0xF2, 0x0F, 0x78, 0xC8, 0x00, 0x00}; + require(Codegen::DecodeSse4a(fullField.data(), fullField.size()).Length == 64, "Zero length does not mean 64"); + const Bytes registerForm = {0x66, 0x45, 0x0F, 0x79, 0xCA}; + const auto decoded = Codegen::DecodeSse4a(registerForm.data(), registerForm.size()); + require(decoded.RegisterForm && !decoded.Insertq && decoded.Destination == 9 && decoded.Source == 10, "Register form operands were decoded incorrectly"); + requireFailure([] { const Bytes bytes = {0x66, 0x0F, 0x78, 0xCB, 0x08, 0x28}; (void)Codegen::DecodeSse4a(bytes.data(), bytes.size()); }, "EXTRQ with a non-zero reg field was accepted"); + requireFailure([] { const Bytes bytes = {0xF2, 0x0F, 0x78, 0x1B, 0x08, 0x08}; (void)Codegen::DecodeSse4a(bytes.data(), bytes.size()); }, "SSE4a memory operand was accepted"); + requireFailure([] { const Bytes bytes = {0xF2, 0x0F, 0x78, 0xC8, 0x20, 0x30}; (void)Codegen::DecodeSse4a(bytes.data(), bytes.size()); }, "Field beyond bit 64 was accepted"); +} + +void matcherSubstitutions() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + const auto match = [&](const Bytes& bytes) { return matcher->Match(bytes.data(), bytes.size()); }; + const auto movntss = match({0xF3, 0x0F, 0x2B, 0x07}); + require(movntss && movntss->Lowering == Codegen::Amd64OnlyLowering::InPlace && movntss->ReplacementBytes == Bytes{0xF3, 0x0F, 0x11, 0x07} && movntss->InstructionName == "MOVNTSS", "MOVNTSS was not rewritten to MOVSS"); + const auto movntsd = match({0xF2, 0x44, 0x0F, 0x2B, 0x4C, 0x24, 0x10}); + require(movntsd && movntsd->Lowering == Codegen::Amd64OnlyLowering::InPlace && movntsd->ReplacementBytes == Bytes{0xF2, 0x44, 0x0F, 0x11, 0x4C, 0x24, 0x10} && movntsd->InstructionName == "MOVNTSD", "MOVNTSD was not rewritten to MOVSD"); + requireFailure([&] { (void)match({0xF3, 0x0F, 0x2B, 0xC1}); }, "MOVNTSS with a register operand was accepted"); + const auto monitorx = match({0x0F, 0x01, 0xFA}); + require(monitorx && monitorx->Lowering == Codegen::Amd64OnlyLowering::Unsupported && monitorx->InstructionName == "MONITORX", "MONITORX was not reported as unsupported"); + const auto registerForm = match({0x66, 0x0F, 0x79, 0xCA}); + require(registerForm && registerForm->Lowering == Codegen::Amd64OnlyLowering::Unsupported, "EXTRQ register form was not reported as unsupported"); + require(!match({0x66, 0x0F, 0x2B, 0x07}) && !match({0x0F, 0x2B, 0x07}) && !match({0x48, 0x8B, 0x05, 0, 0, 0, 0}), "Ordinary instruction was matched"); + const auto stub = match(kInsertqHighSite); + require(stub && stub->Lowering == Codegen::Amd64OnlyLowering::Trampoline && stub->StubBody == kInsertqHighBody && stub->ReturnBranchOffset == 15 && stub->InstructionName == "INSERTQ", "INSERTQ was not lowered through a stub"); + const auto shiftInPlace = match({0x66, 0x0F, 0x78, 0xC3, 0x18, 0x28}); + require(shiftInPlace && shiftInPlace->Lowering == Codegen::Amd64OnlyLowering::InPlace && shiftInPlace->ReplacementBytes == Bytes{0x66, 0x0F, 0x73, 0xD3, 0x28, 0x90}, "Top-aligned EXTRQ was not lowered in place"); +} + +void goldenBodies() { + const Codegen::Sse4aLowering lowering; + const auto outOfLine = [&](const Bytes& site, const Bytes& expected, const std::size_t returnBranchOffset) { + const auto operands = Codegen::DecodeSse4a(site.data(), site.size()); + require(!lowering.LowerInPlace(operands, site.size()).has_value(), "Demon's Souls site unexpectedly qualified for an in-place lowering"); + const auto body = lowering.LowerOutOfLine(operands); + require(body.ReturnBranchOffset == returnBranchOffset, "Stub return branch is at the wrong offset"); + require(body.Bytes == expected, "Stub body differs from the golden encoding"); + }; + outOfLine(kExtrqSite, kExtrqBody, 9); + outOfLine(kInsertqSelfSite, kInsertqSelfBody, 9); + outOfLine(kInsertqCrossSite, kInsertqCrossBody, 13); + outOfLine(kInsertqHighSite, kInsertqHighBody, 15); + outOfLine(kInsertqWordSite, kInsertqWordBody, 13); + const auto inPlace = [&](const Bytes& site, const Bytes& expected) { + const auto operands = Codegen::DecodeSse4a(site.data(), site.size()); + const auto sequence = lowering.LowerInPlace(operands, site.size()); + require(sequence.has_value() && *sequence == expected, "In-place lowering differs from the golden encoding"); + }; + inPlace({0xF2, 0x0F, 0x78, 0xC8, 0x00, 0x00}, {0xF3, 0x0F, 0x7E, 0xC8, 0x66, 0x90}); + inPlace({0x66, 0x0F, 0x78, 0xC3, 0x18, 0x28}, {0x66, 0x0F, 0x73, 0xD3, 0x28, 0x90}); + inPlace({0x66, 0x0F, 0x78, 0xC3, 0x08, 0x00}, {0x66, 0x0F, 0x38, 0x32, 0xDB, 0x90}); + inPlace({0xF2, 0x0F, 0x78, 0xDB, 0x08, 0x00}, {0x66, 0x0F, 0x1F, 0x44, 0x00, 0x00}); + inPlace({0xF2, 0x0F, 0x78, 0xC8, 0x20, 0x00}, {0x66, 0x0F, 0x3A, 0x0E, 0xC8, 0x03}); + inPlace({0xF2, 0x45, 0x0F, 0x78, 0xC8, 0x10, 0x00}, {0x66, 0x45, 0x0F, 0x3A, 0x0E, 0xC8, 0x01}); + const auto highRegisters = Codegen::DecodeSse4a(kInsertqHighSite.data(), kInsertqHighSite.size()); + const auto generic = lowering.LowerOutOfLine(Codegen::Sse4aOperands{true, false, 9, 4, 5, 3}); + require(generic.Bytes[0] == 0x48 && generic.Bytes.size() % 16 == 0 && generic.ReturnBranchOffset < generic.Bytes.size(), "Generic INSERTQ body does not start with the red-zone skip"); + (void)highRegisters; + requireFailure([&] { (void)lowering.LowerOutOfLine(Codegen::Sse4aOperands{true, true, 1, 2, 0, 0}); }, "Register form was lowered out of line"); +} + +Bytes segmentFixture() { + Bytes file(0x300, 0xCC); + const Bytes text = { + 0xF3, 0x0F, 0xB8, 0xC0, + 0xCD, 0x41, + 0xEB, 0x07, + 0xF2, 0x44, 0x0F, 0x78, 0xCC, 0x10, 0x10, + 0xF3, 0x0F, 0x2B, 0x07, + 0xC3}; + std::copy(text.begin(), text.end(), file.begin() + 0x200); + return file; +} + +Domain::ProgramHeader segmentHeader(const std::uint64_t size) { + return {1, 5, 0x200, 0x1000, 0, size, size, 16}; +} + +void converterSegment() { + const auto converter = Codegen::MakeAmd64OnlyConverter(); + const auto file = segmentFixture(); + const auto result = converter->Convert(file, {segmentHeader(20)}); + require(result.ReplacedCount == 1 && result.Reports.size() == 2 && result.Trampolines.size() == 1, "Converter did not classify the segment's AMD-only instructions"); + const auto& site = result.Trampolines[0]; + require(site.Offset == 0x208 && site.Address == 0x1008 && site.Length == 7 && site.OriginalBytes == kInsertqHighSite && site.Body == kInsertqHighBody && site.ReturnBranchOffset == 15, "Trampoline site was recorded incorrectly"); + require(result.Reports[0].InstructionName == "INSERTQ" && result.Reports[0].Offset == 0x208 && result.Reports[0].Lowering == Codegen::Amd64OnlyLowering::Trampoline && result.Reports[0].ReplacementLength == 48, "Trampoline report is wrong"); + require(result.Reports[1].InstructionName == "MOVNTSS" && result.Reports[1].Offset == 0x20F && result.Reports[1].Lowering == Codegen::Amd64OnlyLowering::InPlace && result.Reports[1].ReplacementLength == 4, "In-place report is wrong"); + auto expected = file; + expected[0x211] = 0x11; + require(result.Bytes == expected, "Converter changed bytes other than the MOVNTSS opcode"); + const auto untouched = converter->Convert(Bytes(0x300, 0x90), {segmentHeader(0x100)}); + require(untouched.ReplacedCount == 0 && untouched.Trampolines.empty() && untouched.Reports.empty() && untouched.Bytes == Bytes(0x300, 0x90), "Segment without AMD-only instructions was changed"); + auto branchInside = file; + branchInside[0x207] = 0x02; + requireFailure([&] { (void)converter->Convert(branchInside, {segmentHeader(20)}); }, "Branch into an AMD-only instruction was accepted"); + auto monitorx = file; + monitorx[0x20F] = 0x0F; + monitorx[0x210] = 0x01; + monitorx[0x211] = 0xFA; + monitorx[0x212] = 0x90; + requireFailure([&] { (void)converter->Convert(monitorx, {segmentHeader(20)}); }, "MONITORX was silently kept"); + auto registerForm = file; + const Bytes extrqRegister = {0x66, 0x0F, 0x79, 0xCA}; + std::copy(extrqRegister.begin(), extrqRegister.end(), registerForm.begin() + 0x20F); + requireFailure([&] { (void)converter->Convert(registerForm, {segmentHeader(20)}); }, "EXTRQ register form was silently kept"); + requireFailure([&] { (void)converter->Convert(file, {segmentHeader(0x200)}); }, "Segment exceeding the file was accepted"); +} + +void converterFailureOffsets() { + const auto converter = Codegen::MakeAmd64OnlyConverter(); + const auto file = segmentFixture(); + auto undecodable = file; + const Bytes bareSse4a = {0x0F, 0x78, 0xC0, 0x00}; + std::copy(bareSse4a.begin(), bareSse4a.end(), undecodable.begin() + 0x20F); + require(failureOffset([&] { (void)converter->Convert(undecodable, {segmentHeader(20)}); }, "Undecodable instruction was accepted") == 0x20F, "Decoder failure does not carry the file offset"); + auto memoryForm = file; + memoryForm[0x20C] = 0x08; + require(failureOffset([&] { (void)converter->Convert(memoryForm, {segmentHeader(20)}); }, "INSERTQ memory form was accepted") == 0x208, "SSE4a operand failure does not carry the file offset"); + auto movntsRegister = file; + movntsRegister[0x212] = 0xC1; + require(failureOffset([&] { (void)converter->Convert(movntsRegister, {segmentHeader(20)}); }, "MOVNTSS register form was accepted") == 0x20F, "MOVNTSS failure does not carry the file offset"); + auto monitorx = file; + const Bytes monitorxBytes = {0x0F, 0x01, 0xFA, 0x90}; + std::copy(monitorxBytes.begin(), monitorxBytes.end(), monitorx.begin() + 0x20F); + require(failureOffset([&] { (void)converter->Convert(monitorx, {segmentHeader(20)}); }, "MONITORX was accepted") == 0x20F, "Unsupported instruction failure does not carry the file offset"); +} + +Bytes elfFixture(const Bytes& text) { + Bytes bytes(0x400); + bytes[0] = 0x7F; + bytes[1] = 'E'; + bytes[2] = 'L'; + bytes[3] = 'F'; + bytes[4] = 2; + bytes[5] = 1; + bytes[6] = 1; + write(bytes, 16, 3); + write(bytes, 18, 62); + write(bytes, 24, 0x1000); + write(bytes, 32, 64); + write(bytes, 54, 56); + write(bytes, 56, 6); + write(bytes, 64, 1); + write(bytes, 68, 5); + write(bytes, 72, 0x200); + write(bytes, 80, 0x1000); + write(bytes, 96, 0x100); + write(bytes, 104, 0x100); + write(bytes, 112, 0x1000); + write(bytes, 120, 1); + write(bytes, 124, 6); + write(bytes, 128, 0x300); + write(bytes, 136, 0x2000); + write(bytes, 152, 0x100); + write(bytes, 160, 0x100); + write(bytes, 168, 0x1000); + std::fill(bytes.begin() + 0x200, bytes.begin() + 0x300, 0xCC); + std::copy(text.begin(), text.end(), bytes.begin() + 0x200); + return bytes; +} + +std::vector elfHeaders() { + return {{1, 5, 0x200, 0x1000, 0, 0x100, 0x100, 0x1000}, {1, 6, 0x300, 0x2000, 0, 0x100, 0x100, 0x1000}}; +} + +void linuxPlacement() { + const auto source = elfFixture({0xEB, 0x06, 0xF2, 0x0F, 0x78, 0xDB, 0x08, 0x08, 0xC3}); + const auto headers = elfHeaders(); + const auto converted = Codegen::MakeAmd64OnlyConverter()->Convert(source, {headers[0]}); + require(converted.Trampolines.size() == 1 && converted.Bytes == source, "Linux fixture conversion produced unexpected results"); + const auto byteWriter = std::make_shared(); + Elfpatcher::Linux::LinuxElfPatcher patcher( + std::make_shared(), + std::make_shared(std::make_shared(), byteWriter), + std::make_shared(byteWriter), + byteWriter); + const auto output = patcher.Patch(converted.Bytes, headers, {}, 0, "$ORIGIN/libs", true, false, converted.Trampolines); + require(output[0x202] == 0xE9 && output[0x207] == 0x90, "Linux site was not replaced by a jump"); + const auto target = 0x1002 + 5 + static_cast(read(output, 0x203)); + require(target % 16 == 0 && target > 0x2100, "Linux stub is misaligned or inside the original image"); + const auto phNum = read(output, 56); + std::uint64_t bodyOffset = 0; + bool found = false; + for (std::uint16_t index = 0; index < phNum; ++index) { + const auto header = 64 + index * 56; + if (read(output, header) != 1) continue; + const auto vaddr = read(output, header + 16); + const auto memSize = read(output, header + 40); + if (static_cast(target) < vaddr || static_cast(target) >= vaddr + memSize) continue; + require((read(output, header + 4) & 1) != 0, "Linux stub segment is not executable"); + bodyOffset = read(output, header + 8) + (static_cast(target) - vaddr); + found = true; + } + require(found, "Linux stub is not inside a PT_LOAD segment"); + auto expectedBody = kInsertqSelfBody; + write(expectedBody, 10, static_cast(0x1008 - (target + 9 + 5))); + const Bytes actualBody(output.begin() + static_cast(bodyOffset), output.begin() + static_cast(bodyOffset + expectedBody.size())); + require(actualBody == expectedBody, "Linux stub body or return branch is wrong"); + auto altered = converted.Bytes; + altered[0x205] = 0xDC; + requireFailure([&] { (void)patcher.Patch(altered, headers, {}, 0, "$ORIGIN/libs", true, false, converted.Trampolines); }, "Changed Linux site bytes were accepted"); +} + +} + +int main() { + try { + decoderLengths(); + sse4aOperands(); + matcherSubstitutions(); + goldenBodies(); + converterSegment(); + converterFailureOffsets(); + linuxPlacement(); + std::cout << "AMD64-only converter tests passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/IElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/IElfPatcher.hpp index 41b667ba6..f01272daa 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/IElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/IElfPatcher.hpp @@ -1,6 +1,7 @@ #ifndef ELFPATCHER_IELFPATCHER_HPP #define ELFPATCHER_IELFPATCHER_HPP +#include #include #include #include @@ -19,7 +20,8 @@ public: std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, - bool dependencyDiagnostics + bool dependencyDiagnostics, + const std::vector& trampolines ) = 0; }; diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp index 30a9cceef..ae5782f33 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -27,7 +28,8 @@ public: std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, - bool dependencyDiagnostics + bool dependencyDiagnostics, + const std::vector& trampolines ) override; private: @@ -37,6 +39,7 @@ private: std::shared_ptr _byteWriter; void _appendDynEntry(std::vector& buf, std::int64_t tag, std::uint64_t val) const; + void _appendTrampoline(std::vector& buf, const Codegen::TrampolineSite& site, std::uint64_t extraBlockOffset, const std::function& vaddrOfExtraBlockOffset) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp index 2ee1017a0..2552cba55 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp @@ -7,7 +7,7 @@ namespace Elfpatcher::Windows { class WindowsPePatcher : public IElfPatcher { public: - std::vector Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics) override; + std::vector Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& trampolines) override; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTrampolineBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTrampolineBuilder.hpp new file mode 100644 index 000000000..d37410891 --- /dev/null +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTrampolineBuilder.hpp @@ -0,0 +1,16 @@ +#ifndef ELFPATCHER_WINDOWS_WINDOWSTRAMPOLINEBUILDER_HPP +#define ELFPATCHER_WINDOWS_WINDOWSTRAMPOLINEBUILDER_HPP + +#include +#include + +namespace Elfpatcher::Windows { + +class WindowsTrampolineBuilder { +public: + void Build(const std::vector& sites, const WindowsLoadImage& image, std::vector& sections, std::uint32_t& nextRva) const; +}; + +} + +#endif diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 0172eef32..90ce2bc28 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -2,6 +2,9 @@ #include #include #include +#include +#include +#include #include namespace Elfpatcher::Linux { @@ -24,6 +27,41 @@ void LinuxElfPatcher::_appendDynEntry(std::vector& buf, std::int64 _byteWriter->AppendU64(buf, val); } +void LinuxElfPatcher::_appendTrampoline( + std::vector& buf, + const Codegen::TrampolineSite& site, + const std::uint64_t extraBlockOffset, + const std::function& vaddrOfExtraBlockOffset) const +{ + using namespace Codegen::Amd64OnlySubstitutionTable; + if (site.Length < kJmpRel32.Size || site.OriginalBytes.size() != site.Length || site.Body.size() < kJmpRel32.Size || site.ReturnBranchOffset > site.Body.size() - kJmpRel32.Size || site.Body[site.ReturnBranchOffset] != kJmpRel32.Bytes[0]) + throw Domain::RelinkerException("Invalid AMD-only trampoline site", site.Offset); + if (site.Offset > extraBlockOffset || site.Length > extraBlockOffset - site.Offset) + throw Domain::RelinkerException("AMD-only instruction is outside the original image", site.Offset); + if (!std::equal(site.OriginalBytes.begin(), site.OriginalBytes.end(), buf.begin() + static_cast(site.Offset))) + throw Domain::RelinkerException("AMD-only site bytes changed before patching", site.Offset); + + while (buf.size() % kStubAlignment != 0) + buf.push_back(kTrapFill); + const auto bodyOff = static_cast(buf.size()); + const auto bodyVaddr = vaddrOfExtraBlockOffset(bodyOff); + for (const std::uint8_t b : site.Body) + buf.push_back(b); + + const auto inRange = [](const std::int64_t displacement) { + return displacement >= std::numeric_limits::min() && displacement <= std::numeric_limits::max(); + }; + const auto returnDisplacement = static_cast(site.Address + site.Length) - static_cast(bodyVaddr + site.ReturnBranchOffset + kJmpRel32.Size); + const auto jumpDisplacement = static_cast(bodyVaddr) - static_cast(site.Address + kJmpRel32.Size); + if (!inRange(returnDisplacement) || !inRange(jumpDisplacement)) + throw Domain::RelinkerException("AMD-only stub exceeds rel32 range", site.Offset); + _byteWriter->WriteU32(buf, static_cast(bodyOff + site.ReturnBranchOffset + 1), static_cast(returnDisplacement)); + + std::fill_n(buf.begin() + static_cast(site.Offset), site.Length, kNop1.Bytes[0]); + buf[static_cast(site.Offset)] = kJmpRel32.Bytes[0]; + _byteWriter->WriteU32(buf, static_cast(site.Offset + 1), static_cast(jumpDisplacement)); +} + std::vector LinuxElfPatcher::Patch( const std::vector& sourceElf, const std::vector& originalHeaders, @@ -31,7 +69,8 @@ std::vector LinuxElfPatcher::Patch( const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, - const bool dependencyDiagnostics) + const bool dependencyDiagnostics, + const std::vector& trampolines) { if (dependencyDiagnostics) throw Domain::RelinkerException("Linux target does not support --windows-diagnostics"); @@ -123,6 +162,9 @@ std::vector LinuxElfPatcher::Patch( buf.push_back(b); _byteWriter->WriteU64(buf, kEhdrEntryOffset, stubVaddr); + for (const auto& site : trampolines) + _appendTrampoline(buf, site, extraBlockOff, vaddrOfExtraBlockOffset); + static constexpr char kInterp[] = "/lib64/ld-linux-x86-64.so.2"; const auto interpOff = static_cast(buf.size()); constexpr std::uint64_t interpSize = sizeof(kInterp); diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 0910e7aad..68676bc2c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -39,7 +40,7 @@ void writeGotStub(std::vector& sections, const std::uint32_t targetRv } -std::vector WindowsPePatcher::Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics) { +std::vector WindowsPePatcher::Patch(const std::vector& sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector& trampolines) { WindowsLoadImage image(sourceElf, originalHeaders); if (originalPltGotVaddr != 0) image.GetRva(originalPltGotVaddr, 8); @@ -67,6 +68,7 @@ std::vector WindowsPePatcher::Patch(const std::vector +#include +#include +#include +#include +#include + +namespace Elfpatcher::Windows { + +namespace { + +PeSection& findSection(std::vector& sections, const std::uint32_t rva, const std::size_t length) { + for (auto& section : sections) { + if (rva < section.Rva || rva - section.Rva >= section.Data.size()) + continue; + if (length > section.Data.size() - (rva - section.Rva)) + throw Domain::RelinkerException("AMD-only instruction crosses a PE section boundary", rva); + return section; + } + throw Domain::RelinkerException("AMD-only instruction is outside the PE image", rva); +} + +} + +void WindowsTrampolineBuilder::Build(const std::vector& sites, const WindowsLoadImage& image, std::vector& sections, std::uint32_t& nextRva) const { + using namespace Codegen::Amd64OnlySubstitutionTable; + if (sites.empty()) + return; + const auto sectionRva = nextRva; + std::vector bytes; + for (const auto& site : sites) { + const auto siteRva = image.GetRva(site.Address, site.Length); + if (site.Length < kJmpRel32.Size || site.OriginalBytes.size() != site.Length || site.Body.size() < kJmpRel32.Size || site.ReturnBranchOffset > site.Body.size() - kJmpRel32.Size || site.Body[site.ReturnBranchOffset] != kJmpRel32.Bytes[0]) + throw Domain::RelinkerException("Invalid AMD-only trampoline site", siteRva); + auto& section = findSection(sections, siteRva, site.Length); + const auto offset = static_cast(siteRva - section.Rva); + if (!std::equal(site.OriginalBytes.begin(), site.OriginalBytes.end(), section.Data.begin() + offset)) + throw Domain::RelinkerException("AMD-only site bytes changed before patching", siteRva); + bytes.resize(Io::AlignUp(bytes.size(), kStubAlignment), kTrapFill); + const auto stubRva = CheckedRva(sectionRva + bytes.size()); + const auto bodyOffset = bytes.size(); + bytes.insert(bytes.end(), site.Body.begin(), site.Body.end()); + const auto returnRva = CheckedRva(siteRva + site.Length); + const auto displacement = static_cast(returnRva) - (static_cast(stubRva) + static_cast(site.ReturnBranchOffset + kJmpRel32.Size)); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("AMD-only stub return exceeds rel32 range", returnRva); + Io::WriteU32(bytes, bodyOffset + site.ReturnBranchOffset + 1, static_cast(displacement)); + WindowsStubEmitter jump(siteRva); + jump.Rip({0xe9}, stubRva); + const auto jumpBytes = jump.TakeBytes(); + std::fill_n(section.Data.begin() + offset, site.Length, kNop1.Bytes[0]); + std::copy(jumpBytes.begin(), jumpBytes.end(), section.Data.begin() + offset); + } + sections.push_back({".amdstub", sectionRva, SectionRead | SectionExecute | 0x20u, std::move(bytes)}); + nextRva = AlignRva(sectionRva + sections.back().Data.size()); +} + +} diff --git a/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp b/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp new file mode 100644 index 000000000..96c4e1160 --- /dev/null +++ b/core/relinker/elfpatcher/tests/Amd64OnlyWindowsTests.cpp @@ -0,0 +1,342 @@ +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace Elfpatcher::Windows; +using Bytes = std::vector; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +void requireFailure(const std::function& operation, const char* message) { + try { + operation(); + } catch (const Domain::RelinkerException&) { + return; + } + throw std::runtime_error(message); +} + +template +void write(Bytes& bytes, std::size_t offset, TValue value) { + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("Test fixture write is out of bounds"); + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +template +TValue read(const Bytes& bytes, std::size_t offset) { + TValue value; + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("Test fixture read is out of bounds"); + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +struct alignas(16) State { + std::uint8_t Xmm[16][16]; + std::uint64_t FlagsIn; + std::uint64_t FlagsOut; + std::uint64_t RspBefore; + std::uint64_t RspAfter; + std::uint8_t RedZone[128]; +}; + +constexpr std::uint64_t kCanary = 0xA5C3E17B9D24F608ull; +constexpr std::uint64_t kStatusFlags = 0x8D5; +constexpr std::uint32_t kXmmOffset = 0; +constexpr std::uint32_t kFlagsInOffset = 256; +constexpr std::uint32_t kFlagsOutOffset = 264; +constexpr std::uint32_t kRspBeforeOffset = 272; +constexpr std::uint32_t kRspAfterOffset = 280; +constexpr std::uint32_t kRedZoneOffset = 288; + +static_assert(offsetof(State, FlagsIn) == kFlagsInOffset && offsetof(State, FlagsOut) == kFlagsOutOffset && offsetof(State, RspBefore) == kRspBeforeOffset && offsetof(State, RspAfter) == kRspAfterOffset && offsetof(State, RedZone) == kRedZoneOffset, "State layout drifted"); + +void emit(Bytes& code, std::initializer_list bytes) { + code.insert(code.end(), bytes.begin(), bytes.end()); +} + +void emitU32(Bytes& code, std::uint32_t value) { + for (std::size_t index = 0; index < 4; ++index) code.push_back(static_cast(value >> (index * 8))); +} + +void movdquXmmRbx(Bytes& code, const std::uint8_t reg, const bool store) { + code.push_back(0xF3); + if (reg >= 8) code.push_back(0x44); + emit(code, {0x0F, static_cast(store ? 0x7F : 0x6F), static_cast(0x83 | ((reg & 7) << 3))}); + emitU32(code, kXmmOffset + reg * 16); +} + +void movdquXmmRsp(Bytes& code, const std::uint8_t reg, const bool store, const std::uint32_t offset) { + code.push_back(0xF3); + if (reg >= 8) code.push_back(0x44); + emit(code, {0x0F, static_cast(store ? 0x7F : 0x6F), static_cast(0x84 | ((reg & 7) << 3)), 0x24}); + emitU32(code, offset); +} + +class Harness { +public: + Harness() : _memory(VirtualAlloc(nullptr, kSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) { + if (_memory == nullptr) throw std::runtime_error("Cannot allocate executable memory"); + } + + ~Harness() { + VirtualFree(_memory, 0, MEM_RELEASE); + } + + void Run(const Bytes& body, const std::size_t returnBranchOffset, State& state) const { + Bytes code; + emit(code, {0x53, 0x55, 0x56, 0x57, 0x41, 0x54, 0x41, 0x55, 0x41, 0x56, 0x41, 0x57, 0x48, 0x81, 0xEC, 0xA8, 0x00, 0x00, 0x00}); + for (std::uint8_t reg = 6; reg < 16; ++reg) movdquXmmRsp(code, reg, true, (reg - 6) * 16); + emit(code, {0x48, 0x89, 0xCB, 0xFF, 0xB3}); + emitU32(code, kFlagsInOffset); + emit(code, {0x9D, 0x48, 0xB8}); + for (std::size_t index = 0; index < 8; ++index) code.push_back(static_cast(kCanary >> (index * 8))); + for (int slot = 0; slot < 16; ++slot) emit(code, {0x48, 0x89, 0x44, 0x24, static_cast(-128 + slot * 8)}); + emit(code, {0x48, 0x89, 0xA3}); + emitU32(code, kRspBeforeOffset); + for (std::uint8_t reg = 0; reg < 16; ++reg) movdquXmmRbx(code, reg, false); + code.push_back(0xE9); + const auto jumpOffset = code.size(); + emitU32(code, 0); + const auto continuation = code.size(); + for (int slot = 0; slot < 16; ++slot) { + emit(code, {0x48, 0x8B, 0x44, 0x24, static_cast(-128 + slot * 8), 0x48, 0x89, 0x83}); + emitU32(code, kRedZoneOffset + slot * 8); + } + emit(code, {0x48, 0x89, 0xA3}); + emitU32(code, kRspAfterOffset); + emit(code, {0x9C, 0x8F, 0x83}); + emitU32(code, kFlagsOutOffset); + for (std::uint8_t reg = 0; reg < 16; ++reg) movdquXmmRbx(code, reg, true); + for (std::uint8_t reg = 6; reg < 16; ++reg) movdquXmmRsp(code, reg, false, (reg - 6) * 16); + emit(code, {0x48, 0x81, 0xC4, 0xA8, 0x00, 0x00, 0x00, 0x41, 0x5F, 0x41, 0x5E, 0x41, 0x5D, 0x41, 0x5C, 0x5F, 0x5E, 0x5D, 0x5B, 0xC3}); + code.resize(Io::AlignUp(code.size(), std::size_t{16}), 0xCC); + const auto bodyOffset = code.size(); + code.insert(code.end(), body.begin(), body.end()); + write(code, jumpOffset, static_cast(bodyOffset - (jumpOffset + 4))); + write(code, bodyOffset + returnBranchOffset + 1, static_cast(continuation - (bodyOffset + returnBranchOffset + 5))); + if (code.size() > kSize) throw std::runtime_error("Harness code exceeds its page"); + std::memcpy(_memory, code.data(), code.size()); + FlushInstructionCache(GetCurrentProcess(), _memory, code.size()); + reinterpret_cast(_memory)(&state); + } + +private: + static constexpr std::size_t kSize = 4096; + void* _memory; +}; + +std::uint64_t low(const std::uint8_t (&lane)[16]) { + std::uint64_t value; + std::memcpy(&value, lane, 8); + return value; +} + +std::uint64_t referenceLow(const Codegen::Sse4aOperands& operands, const State& input) { + const auto mask = operands.Length >= 64 ? ~std::uint64_t{0} : ((std::uint64_t{1} << operands.Length) - 1); + const auto destination = low(input.Xmm[operands.Destination]); + if (!operands.Insertq) return (destination >> operands.Index) & mask; + const auto hole = mask << operands.Index; + return (destination & ~hole) | ((low(input.Xmm[operands.Source]) & mask) << operands.Index); +} + +std::uint64_t libcReferenceLow(const Codegen::Sse4aOperands& operands, const State& input) { + CONTEXT context{}; + for (unsigned reg = 0; reg < 16; ++reg) { + std::memcpy(&context.FltSave.XmmRegisters[reg].Low, input.Xmm[reg], 8); + std::memcpy(&context.FltSave.XmmRegisters[reg].High, input.Xmm[reg] + 8, 8); + } + sse4a::Instruction instruction; + instruction.op = operands.Insertq ? sse4a::Op::Insertq : sse4a::Op::Extrq; + instruction.destination = operands.Destination; + instruction.source = operands.Source; + instruction.length = static_cast(operands.Length == 64 ? 0 : operands.Length); + instruction.index = operands.Index; + sse4a::Execute(instruction, context); + return static_cast(context.FltSave.XmmRegisters[operands.Destination].Low); +} + +Bytes encode(const bool insertq, const std::uint8_t dst, const std::uint8_t src, const std::uint8_t length, const std::uint8_t index) { + Bytes bytes = {static_cast(insertq ? 0xF2 : 0x66)}; + const std::uint8_t reg = insertq ? dst : 0; + const std::uint8_t rm = insertq ? src : dst; + const auto rex = static_cast(0x40 | (reg >= 8 ? 4 : 0) | (rm >= 8 ? 1 : 0)); + if (rex != 0x40) bytes.push_back(rex); + bytes.insert(bytes.end(), {0x0F, 0x78, static_cast(0xC0 | ((reg & 7) << 3) | (rm & 7)), static_cast(length == 64 ? 0 : length), index}); + return bytes; +} + +struct Case { + bool Insertq; + std::uint8_t Destination; + std::uint8_t Source; + std::uint8_t Length; + std::uint8_t Index; +}; + +std::string describe(const Case& item) { + return std::string(item.Insertq ? "insertq xmm" : "extrq xmm") + std::to_string(item.Destination) + (item.Insertq ? ", xmm" + std::to_string(item.Source) : "") + ", " + std::to_string(item.Length) + ", " + std::to_string(item.Index); +} + +std::size_t g_executions = 0; + +void executeCase(const Harness& harness, const Codegen::IAmd64OnlyInstructionMatcher& matcher, const Case& item, std::mt19937_64& random, const int images) { + const auto site = encode(item.Insertq, item.Destination, item.Source, item.Length, item.Index); + const auto match = matcher.Match(site.data(), site.size()); + require(match.has_value() && match->Lowering != Codegen::Amd64OnlyLowering::Unsupported, "Immediate-form SSE4a instruction was not lowered: " + describe(item)); + Bytes body; + std::size_t returnBranchOffset = 0; + if (match->Lowering == Codegen::Amd64OnlyLowering::InPlace) { + require(match->ReplacementBytes.size() == site.size(), "In-place lowering changed the length: " + describe(item)); + body = match->ReplacementBytes; + returnBranchOffset = body.size(); + body.insert(body.end(), {0xE9, 0, 0, 0, 0}); + } else { + body = match->StubBody; + returnBranchOffset = match->ReturnBranchOffset; + require(body.size() % 16 == 0 || body.size() == returnBranchOffset + 5, "Stub body with constants is not padded to 16 bytes: " + describe(item)); + } + const auto operands = Codegen::DecodeSse4a(site.data(), site.size()); + const std::array flags = {0x202, 0x203, 0x246, 0xAC7}; + for (int image = 0; image < images; ++image) { + State input{}; + for (auto& lane : input.Xmm) for (auto& byte : lane) byte = static_cast(random()); + input.FlagsIn = flags[static_cast(image) % flags.size()]; + State state = input; + harness.Run(body, returnBranchOffset, state); + ++g_executions; + const auto expected = referenceLow(operands, input); + require(expected == libcReferenceLow(operands, input), "Transcribed reference disagrees with the libc emulation: " + describe(item)); + require(low(state.Xmm[operands.Destination]) == expected, "Lowered sequence computed the wrong field: " + describe(item)); + for (unsigned reg = 0; reg < 16; ++reg) { + if (reg == operands.Destination) continue; + require(std::memcmp(state.Xmm[reg], input.Xmm[reg], 16) == 0, "Lowered sequence clobbered xmm" + std::to_string(reg) + ": " + describe(item)); + } + require(((state.FlagsOut ^ input.FlagsIn) & kStatusFlags) == 0, "Lowered sequence changed RFLAGS: " + describe(item)); + require(state.RspAfter == state.RspBefore, "Lowered sequence did not restore rsp: " + describe(item)); + for (int slot = 0; slot < 16; ++slot) { + std::uint64_t value; + std::memcpy(&value, state.RedZone + slot * 8, 8); + require(value == kCanary, "Lowered sequence wrote into the red zone: " + describe(item)); + } + } +} + +void cpuExecution() { + const Harness harness; + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + std::mt19937_64 random(0x5EED); + const std::vector sites = {{false, 3, 3, 8, 40}, {true, 3, 3, 8, 8}, {true, 1, 0, 8, 0}, {true, 9, 4, 16, 16}, {true, 3, 4, 16, 16}}; + for (const auto& site : sites) executeCase(harness, *matcher, site, random, 64); + const std::vector> pairs = {{3, 3}, {1, 0}, {9, 4}, {3, 4}, {15, 15}, {0, 15}, {8, 8}}; + for (std::uint8_t length = 1; length <= 64; ++length) { + for (std::uint8_t index = 0; index + length <= 64; ++index) { + for (const auto& [dst, src] : pairs) executeCase(harness, *matcher, {true, dst, src, length, index}, random, 2); + for (const std::uint8_t dst : std::array{0, 3, 9, 15}) executeCase(harness, *matcher, {false, dst, dst, length, index}, random, 2); + } + } +} + +Bytes elfFixture(const Bytes& text) { + Bytes bytes(0x400); + bytes[0] = 0x7F; + bytes[1] = 'E'; + bytes[2] = 'L'; + bytes[3] = 'F'; + bytes[4] = 2; + bytes[5] = 1; + bytes[6] = 1; + write(bytes, 16, 3); + write(bytes, 18, 62); + write(bytes, 24, 0x1000); + write(bytes, 32, 64); + write(bytes, 54, 56); + write(bytes, 56, 2); + std::fill(bytes.begin() + 0x200, bytes.begin() + 0x300, 0xCC); + std::copy(text.begin(), text.end(), bytes.begin() + 0x200); + return bytes; +} + +std::vector elfHeaders() { + return {{1, 5, 0x200, 0x1000, 0, 0x100, 0x100, 0x1000}, {1, 6, 0x300, 0x2000, 0, 0x100, 0x100, 0x1000}}; +} + +void peBuilder() { + const Bytes site = {0xF2, 0x0F, 0x78, 0xDB, 0x08, 0x08}; + Bytes text = {0xEB, 0x06}; + text.insert(text.end(), site.begin(), site.end()); + text.push_back(0xC3); + const auto source = elfFixture(text); + const auto headers = elfHeaders(); + const auto converted = Codegen::MakeAmd64OnlyConverter()->Convert(source, {headers[0]}); + require(converted.Trampolines.size() == 1, "PE fixture conversion did not produce one stub"); + const WindowsLoadImage image(converted.Bytes, headers); + auto sections = image.BuildSections(); + auto nextRva = image.GetEndRva(); + const auto stubSectionRva = nextRva; + WindowsTrampolineBuilder().Build(converted.Trampolines, image, sections, nextRva); + require(sections.size() == 3 && sections.back().Name == ".amdstub" && sections.back().Rva == stubSectionRva && (sections.back().Characteristics & SectionExecute) != 0 && nextRva == AlignRva(stubSectionRva + sections.back().Data.size()), "Stub section was not appended"); + const auto& text0 = sections[0]; + const auto siteRva = LoadRva + 2; + require(text0.Data[2] == 0xE9 && text0.Data[7] == 0x90 && text0.Data[8] == 0xC3, "PE site was not replaced by a jump"); + const auto stubRva = static_cast(static_cast(siteRva + 5) + read(text0.Data, 3)); + require(stubRva == stubSectionRva && stubRva % 16 == 0, "PE jump does not land on the 16-aligned stub"); + const auto& body = sections.back().Data; + require(body.size() == 32 && body[0] == 0x66 && body[9] == 0xE9 && body[16] == 0x00 && body[17] == 0x00 && body[18] == 0x02, "PE stub body is wrong"); + require(static_cast(stubRva + 9 + 5) + read(body, 10) == siteRva + 6, "PE stub does not return past the site"); + require(read(body, 5) == 7 && (stubRva + 16) % 16 == 0, "PE mask constant is not 16-aligned"); + std::array directories{}; + const auto file = WindowsPeWriter().Write(sections, LoadRva, directories); + require(file.size() > 0x400, "PE writer rejected the stub section"); + auto altered = converted.Bytes; + altered[0x205] = 0xDC; + requireFailure([&] { + const WindowsLoadImage alteredImage(altered, headers); + auto alteredSections = alteredImage.BuildSections(); + auto rva = alteredImage.GetEndRva(); + WindowsTrampolineBuilder().Build(converted.Trampolines, alteredImage, alteredSections, rva); + }, "Changed PE site bytes were accepted"); + requireFailure([&] { + auto unmapped = converted.Trampolines; + unmapped[0].Address = 0x5000; + auto freshSections = image.BuildSections(); + auto rva = image.GetEndRva(); + WindowsTrampolineBuilder().Build(unmapped, image, freshSections, rva); + }, "Unmapped PE site was accepted"); + auto untouched = image.BuildSections(); + auto untouchedRva = image.GetEndRva(); + WindowsTrampolineBuilder().Build({}, image, untouched, untouchedRva); + require(untouched.size() == 2 && untouchedRva == image.GetEndRva(), "Empty site list changed the image"); +} + +} + +int main() { + try { + cpuExecution(); + peBuilder(); + std::cout << "AMD64-only Windows tests passed (" << g_executions << " lowered sequences executed)\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 58c2658d3..a7654d9b1 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -18,10 +18,12 @@ #include #include #include +#include #include #include #include #include +#include int main(const int argc, char* argv[]) { Cli::Args args; @@ -39,17 +41,15 @@ int main(const int argc, char* argv[]) { auto sourceBytes = fileReader.Read(args.inputPath); const std::string absPath = std::filesystem::absolute(args.outputPath).string(); + std::vector trampolines; if (args.toIntel) { - std::cout << "Mode: Intel instruction conversion; system unchanged; unused-filter=" << args.unusedFilterLevel << " (not applied)\n"; - - const Relinker::ElfReader elfReader(sourceBytes); - const auto converter = Codegen::MakeAmd64OnlyConverter(); - auto result = converter->Convert(std::move(sourceBytes), elfReader.ReadCodeSegments()); - - fileWriter.Write(absPath, std::move(result.Bytes)); - std::cout << "OK: " << result.ReplacedCount << " instructions replaced\n"; - - if (args.autorun) return Cli::Autorun(absPath, args.toWindows); + const auto codeSegments = Relinker::ElfReader(sourceBytes).ReadCodeSegments(); + auto converted = Codegen::MakeAmd64OnlyConverter()->Convert(std::move(sourceBytes), codeSegments); + sourceBytes = std::move(converted.Bytes); + trampolines = std::move(converted.Trampolines); + for (const auto& report : converted.Reports) + 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"; + std::cout << "Intel conversion: " << converted.ReplacedCount << " in place, " << trampolines.size() << " stubs\n"; } auto elfReader = std::make_shared(sourceBytes); @@ -95,7 +95,7 @@ int main(const int argc, char* argv[]) { ); } - fileWriter.Write(absPath, patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, args.runPath, args.lazyBinding, args.windowsDiagnostics)); + fileWriter.Write(absPath, patcher->Patch(sourceBytes, result.OriginalHeaders, result.DynamicSection, result.OriginalPltGotVaddr, args.runPath, args.lazyBinding, args.windowsDiagnostics, trampolines)); std::cout << "External prx references: " << result.RegistryEntries.size() << "\nOutput file: " << absPath << '\n'; if (args.autorun) return Cli::Autorun(absPath, args.toWindows); @@ -105,6 +105,11 @@ int main(const int argc, char* argv[]) { if (e.FailureOffset != 0) std::cerr << " (offset 0x" << std::hex << e.FailureOffset << ")"; std::cerr << "\n"; return 2; + } catch (const Codegen::CodegenException& e) { + std::cerr << "FAIL: " << e.what(); + if (e.FailureOffset != 0) std::cerr << " (offset 0x" << std::hex << e.FailureOffset << ")"; + std::cerr << "\n"; + return 2; } catch (const std::exception& e) { std::cerr << "FAIL: " << e.what() << "\n"; return 2; diff --git a/core/relinker/relinker/tests/StrictNidFilterTests.cpp b/core/relinker/relinker/tests/StrictNidFilterTests.cpp index fe4bba7a9..63fbdac61 100644 --- a/core/relinker/relinker/tests/StrictNidFilterTests.cpp +++ b/core/relinker/relinker/tests/StrictNidFilterTests.cpp @@ -153,10 +153,12 @@ void vectorInstructionLengths() { {0xC5, 0xD9, 0x73, 0xD4, 0x20}, {0xC4, 0xE1, 0x79, 0x70, 0xC0, 0x1B}, {0xC4, 0xE1, 0x78, 0x77}, - {0x62, 0xF1, 0x7D, 0x48, 0x72, 0xD0, 0x04} + {0x62, 0xF1, 0x7D, 0x48, 0x72, 0xD0, 0x04}, + {0xF3, 0x0F, 0xB8, 0xC0}, + {0xCD, 0x41} }; for (const auto& instruction : instructions) - require(decoder.Decode(instruction.data(), instruction.size()) == instruction.size(), "Vector immediate or VZEROUPPER was decoded with the wrong length"); + require(decoder.Decode(instruction.data(), instruction.size()) == instruction.size(), "Vector immediate, VZEROUPPER, POPCNT or INT was decoded with the wrong length"); } void exceptionLandingPads() { diff --git a/docs/TechnicalDebt.md b/docs/TechnicalDebt.md index bf4a06b7c..f72aa68c5 100644 --- a/docs/TechnicalDebt.md +++ b/docs/TechnicalDebt.md @@ -30,3 +30,7 @@ Throughout the project, every function at every stage either **does exactly what - [Shader recompilation](../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../core/shader/recompiler) was written completely independently from [libs/prx](../core/libs/prx). - The executable file that [relinker](../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. - [Relinker](../core/relinker/elfpatcher/src) doesn't add an icon to the generated executable. This should be done without adding dependencies (only standard). +- `--to-intel` does not lower the register forms of EXTRQ/INSERTQ (`0F 79`, 4 or 5 bytes, too short for a jump) nor MONITORX/MWAITX/CLZERO/RDPRU/MCOMMIT; the [lowering](../core/relinker/codegen/src/x86/Sse4aLowering.cpp) fails the relink instead. SHA-NI is not substituted. +- 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 VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. +- The Linux placement of the `--to-intel` stubs in the [ELF patcher](../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. +- The libc [SSE4a trap emulation](../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it.