feat(relinker): --to-intel lowers SSE4a sites to same-length jumps into an appended stub section

EXTRQ, INSERTQ, MOVNTSS and MOVNTSD are rewritten at relink time: same-length in-place forms
where they exist, otherwise a jmp rel32 into an appended executable section holding a
scratch-free SSE body and a jump back, so no code shifts and no RIP-relative or jump-table
operand needs adjusting. Decoder fixes for POPCNT and 0F BB (BTC), codegen and elfpatcher
tests, README and technical-debt notes. The runtime illegal-instruction trap stays as a
fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177gthrWEBBfBu5xJDXgBfo
This commit is contained in:
devilismyfriend 2026-09-26 19:00:39 -07:00
parent af3c2024ed
commit 01c4e3edf7
28 changed files with 1515 additions and 77 deletions

View File

@ -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

View File

@ -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

View File

@ -1,6 +1,7 @@
#ifndef CODEGEN_CODEGENTYPES_HPP
#define CODEGEN_CODEGENTYPES_HPP
#include <codegen/x86/Amd64OnlySubstitutionTypes.hpp>
#include <domain/Types.hpp>
#include <cstdint>
#include <vector>
@ -28,9 +29,20 @@ struct RewriteResult {
std::vector<AddressAdjustment> Adjustments;
};
struct TrampolineSite {
Domain::FileByteOffset Offset;
Domain::VirtualAddress Address;
std::size_t Length;
std::vector<std::uint8_t> OriginalBytes;
std::vector<std::uint8_t> Body;
std::size_t ReturnBranchOffset;
};
struct ConvertResult {
std::vector<std::uint8_t> Bytes;
std::size_t ReplacedCount;
std::vector<TrampolineSite> Trampolines;
std::vector<Amd64OnlySubstitutionReport> Reports;
};
enum class ControlFlowKind : std::uint8_t {

View File

@ -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

View File

@ -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<std::uint8_t> ReplacementBytes;
std::vector<std::uint8_t> 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;
};
}

View File

@ -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;

View File

@ -0,0 +1,25 @@
#ifndef CODEGEN_X86_SSE4ALOWERING_HPP
#define CODEGEN_X86_SSE4ALOWERING_HPP
#include <codegen/x86/Sse4aOperands.hpp>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <vector>
namespace Codegen {
struct LoweredBody {
std::vector<std::uint8_t> Bytes;
std::size_t ReturnBranchOffset;
};
class Sse4aLowering {
public:
[[nodiscard]] std::optional<std::vector<std::uint8_t>> LowerInPlace(const Sse4aOperands& operands, std::size_t originalLength) const;
[[nodiscard]] LoweredBody LowerOutOfLine(const Sse4aOperands& operands) const;
};
}
#endif

View File

@ -0,0 +1,22 @@
#ifndef CODEGEN_X86_SSE4AOPERANDS_HPP
#define CODEGEN_X86_SSE4AOPERANDS_HPP
#include <cstddef>
#include <cstdint>
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

View File

@ -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;

View File

@ -1,13 +1,28 @@
#include <codegen/IAmd64OnlyConverter.hpp>
#include <codegen/IInstructionScanner.hpp>
#include <codegen/CodegenException.hpp>
#include <codegen/x86/Amd64OnlySubstitutionTable.hpp>
#include <codegen/x86/X64InstructionDecoder.hpp>
#include <codegen/x86/X64InstructionRewriter.hpp>
#include <codegen/x86/IAmd64OnlyInstructionMatcher.hpp>
#include <algorithm>
#include <memory>
#include <set>
#include <string>
namespace Codegen {
namespace {
template<typename TOperation>
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<IAmd64OnlyInstructionMatcher> _matcher = MakeAmd64OnlyInstructionMatcher();
std::unique_ptr<IInstructionScanner> _scanner = MakeInstructionScanner();
std::size_t _convertSegment(
void _convertSegment(
std::vector<std::uint8_t>& fileBytes,
const Domain::ProgramHeader& ph
const Domain::ProgramHeader& ph,
ConvertResult& result
) const;
[[nodiscard]] static std::set<std::uint64_t> _collectBranchTargets(
const std::vector<std::uint8_t>& seg,
const std::vector<InstructionMatch>& matches,
const Domain::ProgramHeader& ph
);
};
std::size_t Amd64OnlyConverter::_convertSegment(
std::vector<std::uint8_t>& fileBytes,
std::set<std::uint64_t> Amd64OnlyConverter::_collectBranchTargets(
const std::vector<std::uint8_t>& seg,
const std::vector<InstructionMatch>& matches,
const Domain::ProgramHeader& ph
) {
const X64InstructionDecoder decoder;
std::set<std::uint64_t> 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<std::int64_t>(ph.MappedAddress + match.Offset + info.Length) + info.BranchDisp;
if (target >= 0)
targets.insert(static_cast<std::uint64_t>(target));
}
return targets;
}
void Amd64OnlyConverter::_convertSegment(
std::vector<std::uint8_t>& fileBytes,
const Domain::ProgramHeader& ph,
ConvertResult& result
) const {
const auto segOffset = static_cast<std::size_t>(ph.Offset);
const auto segSize = static_cast<std::size_t>(ph.FileSize);
if (segOffset > fileBytes.size() || segSize > fileBytes.size() - segOffset)
throw CodegenException("Code segment exceeds the file", ph.Offset);
std::vector<std::uint8_t> seg(
fileBytes.begin() + segOffset,
fileBytes.begin() + segOffset + segSize
fileBytes.begin() + static_cast<std::ptrdiff_t>(segOffset),
fileBytes.begin() + static_cast<std::ptrdiff_t>(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> pending;
for (const auto& match : matches) {
const auto shiftedOffset = static_cast<std::int64_t>(match.Offset) + shift;
const auto offset = static_cast<std::size_t>(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<std::uint64_t>{};
for (const auto& item : pending) {
const auto& match = item.Instruction;
const auto& substitution = item.Substitution;
const auto fileOffset = static_cast<Domain::FileByteOffset>(ph.Offset + match.Offset);
const auto address = static_cast<Domain::VirtualAddress>(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<Domain::FileByteOffset>(offset),
result->ReplacementBytes
};
auto rewriteResult = _rewriter.Rewrite(seg, request);
shift += static_cast<std::int64_t>(result->ReplacementBytes.size()) -
static_cast<std::int64_t>(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<std::ptrdiff_t>(match.Offset);
result.Trampolines.push_back({
fileOffset,
address,
match.Length,
std::vector<std::uint8_t>(begin, begin + static_cast<std::ptrdiff_t>(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<std::ptrdiff_t>(segOffset));
}
ConvertResult Amd64OnlyConverter::Convert(
std::vector<std::uint8_t> fileBytes,
const std::vector<Domain::ProgramHeader>& 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;
}
}

View File

@ -1,7 +1,9 @@
#include <codegen/IInstructionScanner.hpp>
#include <codegen/CodegenException.hpp>
#include <codegen/x86/X64InstructionDecoder.hpp>
#include <algorithm>
#include <memory>
#include <string>
namespace Codegen {
@ -29,7 +31,12 @@ std::vector<InstructionMatch> 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});

View File

@ -1,14 +1,20 @@
#include <codegen/x86/IAmd64OnlyInstructionMatcher.hpp>
#include <codegen/x86/DecodedInstruction.hpp>
#include <codegen/x86/Amd64OnlySubstitutionTable.hpp>
#include <codegen/x86/Sse4aLowering.hpp>
#include <codegen/x86/Sse4aOperands.hpp>
#include <codegen/x86/X64OpcodeConstants.hpp>
#include <codegen/CodegenException.hpp>
#include <memory>
namespace Codegen {
namespace {
std::vector<std::uint8_t> _bytesOf(const Amd64OnlySubstitutionTable::Entry& e) {
return std::vector<std::uint8_t>(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<std::uint8_t> 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<Amd64OnlyMatch> 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;
}

View File

@ -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;

View File

@ -0,0 +1,214 @@
#include <codegen/x86/Sse4aLowering.hpp>
#include <codegen/x86/Amd64OnlySubstitutionTable.hpp>
#include <codegen/CodegenException.hpp>
#include <algorithm>
#include <array>
#include <initializer_list>
namespace Codegen {
namespace {
using namespace Amd64OnlySubstitutionTable;
using Constant = std::array<std::uint8_t, 16>;
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<std::uint8_t>(kRexBase | ((reg & 8) != 0 ? kRexR : 0) | ((rm & 8) != 0 ? kRexB : 0));
}
void _emit(std::vector<std::uint8_t>& out, const std::uint8_t prefix, const std::uint8_t reg, const std::uint8_t rm, const std::initializer_list<std::uint8_t> 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<std::uint8_t>& out, const std::uint8_t prefix, const std::initializer_list<std::uint8_t> opcode, const std::uint8_t dst, const std::uint8_t src) {
_emit(out, prefix, dst, src, opcode, static_cast<std::uint8_t>(kModRmRegister | ((dst & 7) << 3) | (src & 7)));
}
void _shiftImm(std::vector<std::uint8_t>& 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<std::uint8_t>(kModRmRegister | (extension << 3) | (reg & 7)));
out.push_back(imm);
}
void _nopFill(std::vector<std::uint8_t>& out, std::size_t count) {
while (count > 0) {
const auto& nop = kNops[std::min<std::size_t>(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<std::uint8_t> 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<std::uint8_t> opcode, const std::uint8_t reg, const Constant& constant) {
_emit(_bytes, kPrefixPacked, reg, 0, opcode, static_cast<std::uint8_t>(((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<std::uint8_t>(((reg & 7) << 3) | kModRmRspBase));
_bytes.push_back(kSibRsp);
}
void Restore(const std::uint8_t reg) {
_emit(_bytes, kPrefixScalar, reg, 0, {0x0F, 0x6F}, static_cast<std::uint8_t>(((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<std::size_t> 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<std::int64_t>(constantOffsets[fixup.ConstantIndex]) - static_cast<std::int64_t>(fixup.InstructionEnd);
const auto value = static_cast<std::uint32_t>(static_cast<std::int32_t>(displacement));
for (std::size_t index = 0; index < 4; ++index)
_bytes[fixup.DisplacementOffset + index] = static_cast<std::uint8_t>(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<std::uint8_t> _bytes;
std::vector<Fixup> _fixups;
std::vector<Constant> _constants;
};
}
std::optional<std::vector<std::uint8_t>> 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<std::uint8_t> 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<std::uint8_t>((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<std::uint8_t>(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<std::uint8_t>(kFieldBits - length));
body.ShiftImm(kShiftRight, dst, static_cast<std::uint8_t>(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<std::uint8_t>(byte);
for (std::size_t byte = index / 8; byte < index / 8 + length / 8; ++byte)
mask[byte] = static_cast<std::uint8_t>((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<std::uint8_t>(holeMask >> (byte * 8));
body.RipOperand({0x0F, 0xDB}, scratch, hole);
body.Sse(kPrefixPacked, {0x0F, 0xEF}, dst, scratch);
body.Restore(scratch);
}
return body.Finish();
}
}

View File

@ -0,0 +1,84 @@
#include <codegen/x86/Sse4aOperands.hpp>
#include <codegen/x86/X64OpcodeConstants.hpp>
#include <codegen/CodegenException.hpp>
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<std::uint8_t>((modrm >> ModRmRegShift) & ModRmRegMask);
const auto reg = static_cast<std::uint8_t>(regField | (((rex & 0x4) != 0) ? 8 : 0));
const auto rm = static_cast<std::uint8_t>((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<std::uint8_t>(data[pos] & 0x3F);
operands.Length = rawLength == 0 ? 64 : rawLength;
operands.Index = static_cast<std::uint8_t>(data[pos + 1] & 0x3F);
if (static_cast<unsigned>(operands.Length) + operands.Index > 64) {
throw CodegenException("SSE4a field exceeds 64 bits");
}
return operands;
}
}

View File

@ -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;
}

View File

@ -0,0 +1,339 @@
#include <codegen/IAmd64OnlyConverter.hpp>
#include <codegen/CodegenException.hpp>
#include <codegen/x86/IAmd64OnlyInstructionMatcher.hpp>
#include <codegen/x86/Sse4aLowering.hpp>
#include <codegen/x86/Sse4aOperands.hpp>
#include <codegen/x86/X64InstructionDecoder.hpp>
#include <elfpatcher/general/EntryStubBuilder.hpp>
#include <elfpatcher/general/ProgramHeaderLayoutBuilder.hpp>
#include <elfpatcher/general/SectionHeaderTableBuilder.hpp>
#include <elfpatcher/general/SegmentFilter.hpp>
#include <elfpatcher/linux/LinuxElfPatcher.hpp>
#include <io/ByteWriter.hpp>
#include <cstring>
#include <functional>
#include <iostream>
#include <stdexcept>
#include <string>
namespace {
using Bytes = std::vector<std::uint8_t>;
void require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void requireFailure(const std::function<void()>& 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<void()>& operation, const char* message) {
try {
operation();
} catch (const Codegen::CodegenException& error) {
return error.FailureOffset;
}
throw std::runtime_error(message);
}
template<typename TValue>
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<typename TValue>
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<Bytes> 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<std::uint16_t>(bytes, 16, 3);
write<std::uint16_t>(bytes, 18, 62);
write<std::uint64_t>(bytes, 24, 0x1000);
write<std::uint64_t>(bytes, 32, 64);
write<std::uint16_t>(bytes, 54, 56);
write<std::uint16_t>(bytes, 56, 6);
write<std::uint32_t>(bytes, 64, 1);
write<std::uint32_t>(bytes, 68, 5);
write<std::uint64_t>(bytes, 72, 0x200);
write<std::uint64_t>(bytes, 80, 0x1000);
write<std::uint64_t>(bytes, 96, 0x100);
write<std::uint64_t>(bytes, 104, 0x100);
write<std::uint64_t>(bytes, 112, 0x1000);
write<std::uint32_t>(bytes, 120, 1);
write<std::uint32_t>(bytes, 124, 6);
write<std::uint64_t>(bytes, 128, 0x300);
write<std::uint64_t>(bytes, 136, 0x2000);
write<std::uint64_t>(bytes, 152, 0x100);
write<std::uint64_t>(bytes, 160, 0x100);
write<std::uint64_t>(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<Domain::ProgramHeader> 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<Io::ByteWriter>();
Elfpatcher::Linux::LinuxElfPatcher patcher(
std::make_shared<Elfpatcher::EntryStubBuilder>(),
std::make_shared<Elfpatcher::ProgramHeaderLayoutBuilder>(std::make_shared<Elfpatcher::SegmentFilter>(), byteWriter),
std::make_shared<Elfpatcher::SectionHeaderTableBuilder>(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<std::int64_t>(read<std::int32_t>(output, 0x203));
require(target % 16 == 0 && target > 0x2100, "Linux stub is misaligned or inside the original image");
const auto phNum = read<std::uint16_t>(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<std::uint32_t>(output, header) != 1) continue;
const auto vaddr = read<std::uint64_t>(output, header + 16);
const auto memSize = read<std::uint64_t>(output, header + 40);
if (static_cast<std::uint64_t>(target) < vaddr || static_cast<std::uint64_t>(target) >= vaddr + memSize) continue;
require((read<std::uint32_t>(output, header + 4) & 1) != 0, "Linux stub segment is not executable");
bodyOffset = read<std::uint64_t>(output, header + 8) + (static_cast<std::uint64_t>(target) - vaddr);
found = true;
}
require(found, "Linux stub is not inside a PT_LOAD segment");
auto expectedBody = kInsertqSelfBody;
write<std::int32_t>(expectedBody, 10, static_cast<std::int32_t>(0x1008 - (target + 9 + 5)));
const Bytes actualBody(output.begin() + static_cast<std::ptrdiff_t>(bodyOffset), output.begin() + static_cast<std::ptrdiff_t>(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;
}
}

View File

@ -1,6 +1,7 @@
#ifndef ELFPATCHER_IELFPATCHER_HPP
#define ELFPATCHER_IELFPATCHER_HPP
#include <codegen/CodegenTypes.hpp>
#include <domain/Types.hpp>
#include <cstdint>
#include <string>
@ -19,7 +20,8 @@ public:
std::uint64_t originalPltGotVaddr,
const std::string& runPath,
bool lazyBinding,
bool dependencyDiagnostics
bool dependencyDiagnostics,
const std::vector<Codegen::TrampolineSite>& trampolines
) = 0;
};

View File

@ -6,6 +6,7 @@
#include <elfpatcher/general/IProgramHeaderLayoutBuilder.hpp>
#include <elfpatcher/general/ISectionHeaderTableBuilder.hpp>
#include <io/IByteWriter.hpp>
#include <functional>
#include <memory>
#include <string>
@ -27,7 +28,8 @@ public:
std::uint64_t originalPltGotVaddr,
const std::string& runPath,
bool lazyBinding,
bool dependencyDiagnostics
bool dependencyDiagnostics,
const std::vector<Codegen::TrampolineSite>& trampolines
) override;
private:
@ -37,6 +39,7 @@ private:
std::shared_ptr<Io::IByteWriter> _byteWriter;
void _appendDynEntry(std::vector<std::uint8_t>& buf, std::int64_t tag, std::uint64_t val) const;
void _appendTrampoline(std::vector<std::uint8_t>& buf, const Codegen::TrampolineSite& site, std::uint64_t extraBlockOffset, const std::function<std::uint64_t(std::uint64_t)>& vaddrOfExtraBlockOffset) const;
};
}

View File

@ -7,7 +7,7 @@ namespace Elfpatcher::Windows {
class WindowsPePatcher : public IElfPatcher {
public:
std::vector<std::uint8_t> Patch(const std::vector<std::uint8_t>& sourceElf, const std::vector<Domain::ProgramHeader>& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics) override;
std::vector<std::uint8_t> Patch(const std::vector<std::uint8_t>& sourceElf, const std::vector<Domain::ProgramHeader>& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector<Codegen::TrampolineSite>& trampolines) override;
};
}

View File

@ -0,0 +1,16 @@
#ifndef ELFPATCHER_WINDOWS_WINDOWSTRAMPOLINEBUILDER_HPP
#define ELFPATCHER_WINDOWS_WINDOWSTRAMPOLINEBUILDER_HPP
#include <codegen/CodegenTypes.hpp>
#include <elfpatcher/windows/WindowsLoadImage.hpp>
namespace Elfpatcher::Windows {
class WindowsTrampolineBuilder {
public:
void Build(const std::vector<Codegen::TrampolineSite>& sites, const WindowsLoadImage& image, std::vector<PeSection>& sections, std::uint32_t& nextRva) const;
};
}
#endif

View File

@ -2,6 +2,9 @@
#include <elfpatcher/general/ElfConstants.hpp>
#include <elfpatcher/general/ProgramHeaderLayoutRequest.hpp>
#include <elfpatcher/general/SectionHeaderTableRequest.hpp>
#include <codegen/x86/Amd64OnlySubstitutionTable.hpp>
#include <algorithm>
#include <limits>
#include <string>
namespace Elfpatcher::Linux {
@ -24,6 +27,41 @@ void LinuxElfPatcher::_appendDynEntry(std::vector<std::uint8_t>& buf, std::int64
_byteWriter->AppendU64(buf, val);
}
void LinuxElfPatcher::_appendTrampoline(
std::vector<std::uint8_t>& buf,
const Codegen::TrampolineSite& site,
const std::uint64_t extraBlockOffset,
const std::function<std::uint64_t(std::uint64_t)>& 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<std::ptrdiff_t>(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<std::uint64_t>(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<std::int32_t>::min() && displacement <= std::numeric_limits<std::int32_t>::max();
};
const auto returnDisplacement = static_cast<std::int64_t>(site.Address + site.Length) - static_cast<std::int64_t>(bodyVaddr + site.ReturnBranchOffset + kJmpRel32.Size);
const auto jumpDisplacement = static_cast<std::int64_t>(bodyVaddr) - static_cast<std::int64_t>(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<std::size_t>(bodyOff + site.ReturnBranchOffset + 1), static_cast<std::uint32_t>(returnDisplacement));
std::fill_n(buf.begin() + static_cast<std::ptrdiff_t>(site.Offset), site.Length, kNop1.Bytes[0]);
buf[static_cast<std::size_t>(site.Offset)] = kJmpRel32.Bytes[0];
_byteWriter->WriteU32(buf, static_cast<std::size_t>(site.Offset + 1), static_cast<std::uint32_t>(jumpDisplacement));
}
std::vector<std::uint8_t> LinuxElfPatcher::Patch(
const std::vector<std::uint8_t>& sourceElf,
const std::vector<Domain::ProgramHeader>& originalHeaders,
@ -31,7 +69,8 @@ std::vector<std::uint8_t> LinuxElfPatcher::Patch(
const std::uint64_t originalPltGotVaddr,
const std::string& runPath,
const bool lazyBinding,
const bool dependencyDiagnostics)
const bool dependencyDiagnostics,
const std::vector<Codegen::TrampolineSite>& trampolines)
{
if (dependencyDiagnostics)
throw Domain::RelinkerException("Linux target does not support --windows-diagnostics");
@ -123,6 +162,9 @@ std::vector<std::uint8_t> 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<std::uint64_t>(buf.size());
constexpr std::uint64_t interpSize = sizeof(kInterp);

View File

@ -4,6 +4,7 @@
#include <elfpatcher/windows/WindowsPeWriter.hpp>
#include <elfpatcher/windows/WindowsRelocationBuilder.hpp>
#include <elfpatcher/windows/WindowsTlsBuilder.hpp>
#include <elfpatcher/windows/WindowsTrampolineBuilder.hpp>
#include <io/BufferUtils.hpp>
#include <filesystem>
#include <fstream>
@ -39,7 +40,7 @@ void writeGotStub(std::vector<PeSection>& sections, const std::uint32_t targetRv
}
std::vector<std::uint8_t> WindowsPePatcher::Patch(const std::vector<std::uint8_t>& sourceElf, const std::vector<Domain::ProgramHeader>& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics) {
std::vector<std::uint8_t> WindowsPePatcher::Patch(const std::vector<std::uint8_t>& sourceElf, const std::vector<Domain::ProgramHeader>& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, const std::uint64_t originalPltGotVaddr, const std::string& runPath, const bool lazyBinding, const bool dependencyDiagnostics, const std::vector<Codegen::TrampolineSite>& trampolines) {
WindowsLoadImage image(sourceElf, originalHeaders);
if (originalPltGotVaddr != 0)
image.GetRva(originalPltGotVaddr, 8);
@ -67,6 +68,7 @@ std::vector<std::uint8_t> WindowsPePatcher::Patch(const std::vector<std::uint8_t
nextRva = AlignRva(nextRva + sections.back().Data.size());
}
directories[9] = WindowsTlsBuilder().Build(sourceElf, originalHeaders, image, sections, relocations.BaseRelocations, nextRva);
WindowsTrampolineBuilder().Build(trampolines, image, sections, nextRva);
auto relocationData = relocationBuilder.BuildBaseRelocations(relocations.BaseRelocations);
if (!relocationData.empty()) {
directories[5] = {nextRva, CheckedRva(relocationData.size())};

View File

@ -0,0 +1,58 @@
#include <elfpatcher/windows/WindowsTrampolineBuilder.hpp>
#include <elfpatcher/windows/WindowsStubEmitter.hpp>
#include <codegen/x86/Amd64OnlySubstitutionTable.hpp>
#include <io/BufferUtils.hpp>
#include <algorithm>
#include <limits>
namespace Elfpatcher::Windows {
namespace {
PeSection& findSection(std::vector<PeSection>& 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<Codegen::TrampolineSite>& sites, const WindowsLoadImage& image, std::vector<PeSection>& sections, std::uint32_t& nextRva) const {
using namespace Codegen::Amd64OnlySubstitutionTable;
if (sites.empty())
return;
const auto sectionRva = nextRva;
std::vector<std::uint8_t> 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<std::ptrdiff_t>(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<std::int64_t>(returnRva) - (static_cast<std::int64_t>(stubRva) + static_cast<std::int64_t>(site.ReturnBranchOffset + kJmpRel32.Size));
if (displacement < std::numeric_limits<std::int32_t>::min() || displacement > std::numeric_limits<std::int32_t>::max())
throw Domain::RelinkerException("AMD-only stub return exceeds rel32 range", returnRva);
Io::WriteU32(bytes, bodyOffset + site.ReturnBranchOffset + 1, static_cast<std::uint32_t>(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());
}
}

View File

@ -0,0 +1,342 @@
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <Sse4aEmulation.hpp>
#include <codegen/IAmd64OnlyConverter.hpp>
#include <codegen/x86/IAmd64OnlyInstructionMatcher.hpp>
#include <codegen/x86/Sse4aOperands.hpp>
#include <elfpatcher/windows/WindowsLoadImage.hpp>
#include <elfpatcher/windows/WindowsPeWriter.hpp>
#include <elfpatcher/windows/WindowsTrampolineBuilder.hpp>
#include <io/BufferUtils.hpp>
#include <array>
#include <cstring>
#include <functional>
#include <iostream>
#include <random>
#include <stdexcept>
#include <string>
namespace {
using namespace Elfpatcher::Windows;
using Bytes = std::vector<std::uint8_t>;
void require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
void requireFailure(const std::function<void()>& operation, const char* message) {
try {
operation();
} catch (const Domain::RelinkerException&) {
return;
}
throw std::runtime_error(message);
}
template<typename TValue>
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<typename TValue>
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<std::uint8_t> 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<std::uint8_t>(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<std::uint8_t>(store ? 0x7F : 0x6F), static_cast<std::uint8_t>(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<std::uint8_t>(store ? 0x7F : 0x6F), static_cast<std::uint8_t>(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<std::uint8_t>(kCanary >> (index * 8)));
for (int slot = 0; slot < 16; ++slot) emit(code, {0x48, 0x89, 0x44, 0x24, static_cast<std::uint8_t>(-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<std::uint8_t>(-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<std::int32_t>(code, jumpOffset, static_cast<std::int32_t>(bodyOffset - (jumpOffset + 4)));
write<std::int32_t>(code, bodyOffset + returnBranchOffset + 1, static_cast<std::int32_t>(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<void (*)(State*)>(_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<std::uint8_t>(operands.Length == 64 ? 0 : operands.Length);
instruction.index = operands.Index;
sse4a::Execute(instruction, context);
return static_cast<std::uint64_t>(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<std::uint8_t>(insertq ? 0xF2 : 0x66)};
const std::uint8_t reg = insertq ? dst : 0;
const std::uint8_t rm = insertq ? src : dst;
const auto rex = static_cast<std::uint8_t>(0x40 | (reg >= 8 ? 4 : 0) | (rm >= 8 ? 1 : 0));
if (rex != 0x40) bytes.push_back(rex);
bytes.insert(bytes.end(), {0x0F, 0x78, static_cast<std::uint8_t>(0xC0 | ((reg & 7) << 3) | (rm & 7)), static_cast<std::uint8_t>(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<std::uint64_t, 4> 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<std::uint8_t>(random());
input.FlagsIn = flags[static_cast<std::size_t>(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<Case> 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<std::pair<std::uint8_t, std::uint8_t>> 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<std::uint8_t, 4>{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<std::uint16_t>(bytes, 16, 3);
write<std::uint16_t>(bytes, 18, 62);
write<std::uint64_t>(bytes, 24, 0x1000);
write<std::uint64_t>(bytes, 32, 64);
write<std::uint16_t>(bytes, 54, 56);
write<std::uint16_t>(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<Domain::ProgramHeader> 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<std::uint32_t>(static_cast<std::int64_t>(siteRva + 5) + read<std::int32_t>(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<std::int64_t>(stubRva + 9 + 5) + read<std::int32_t>(body, 10) == siteRva + 6, "PE stub does not return past the site");
require(read<std::int32_t>(body, 5) == 7 && (stubRva + 16) % 16 == 0, "PE mask constant is not 16-aligned");
std::array<PeDirectory, 16> 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;
}

View File

@ -18,10 +18,12 @@
#include <relinker/output/CallRegistryWriter.hpp>
#include <relinker/pipeline/RelinkerPipeline.hpp>
#include <codegen/IAmd64OnlyConverter.hpp>
#include <codegen/CodegenException.hpp>
#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
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<Codegen::TrampolineSite> 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<Relinker::ElfReader>(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;

View File

@ -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() {

View File

@ -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.