From a571a7f100eb5330727e277ae0f0caba1c303e56 Mon Sep 17 00:00:00 2001 From: Hashaam Zafar Date: Wed, 7 Oct 2026 11:01:58 -0700 Subject: [PATCH 1/2] feat(relinker): relocate RIP-relative operands into --to-intel stubs SHA-1 and SHA-256 instructions with a [rip+disp32] operand, and instructions with one that a short site absorbs, now move into the stub. The stub records each displacement as a relocation relative to the site, and the Linux, guest and Windows placements resolve it once the stub address is known. Indirect branches through [rip+disp32] still fail the relink. Co-Authored-By: Claude Opus 5.5 --- .../codegen/include/codegen/CodegenTypes.hpp | 1 + .../x86/Amd64OnlySubstitutionTypes.hpp | 7 + .../include/codegen/x86/ClzeroLowering.hpp | 3 +- .../codegen/x86/ReciprocalLowering.hpp | 3 +- .../include/codegen/x86/Sha1Lowering.hpp | 3 +- .../include/codegen/x86/Sha256Lowering.hpp | 3 +- .../include/codegen/x86/Sse4aLowering.hpp | 4 +- .../include/codegen/x86/StubBodyBuilder.hpp | 11 + .../codegen/src/Amd64OnlyConverter.cpp | 5 +- .../src/x86/Amd64OnlyInstructionMatcher.cpp | 49 ++-- .../codegen/src/x86/ClzeroLowering.cpp | 3 +- .../codegen/src/x86/ReciprocalLowering.cpp | 3 +- .../relinker/codegen/src/x86/Sha1Lowering.cpp | 3 +- .../relinker/codegen/src/x86/Sha1Operands.cpp | 2 + .../codegen/src/x86/Sha256Lowering.cpp | 3 +- .../codegen/src/x86/Sha256Operands.cpp | 2 + .../codegen/src/x86/Sse4aLowering.cpp | 7 +- .../codegen/src/x86/StubBodyBuilder.cpp | 50 +++- .../codegen/tests/Amd64OnlyConverterTests.cpp | 214 +++++++++++++++--- .../elfpatcher/src/linux/LinuxElfPatcher.cpp | 3 + .../src/linux/LinuxGuestModuleWriter.cpp | 3 + .../src/windows/WindowsTrampolineBuilder.cpp | 3 + .../tests/test_guest_intel_trampolines.py | 30 ++- docs/dev/TechnicalDebt.md | 2 +- 24 files changed, 338 insertions(+), 79 deletions(-) diff --git a/core/relinker/codegen/include/codegen/CodegenTypes.hpp b/core/relinker/codegen/include/codegen/CodegenTypes.hpp index ee1963333..e41f6cd53 100644 --- a/core/relinker/codegen/include/codegen/CodegenTypes.hpp +++ b/core/relinker/codegen/include/codegen/CodegenTypes.hpp @@ -36,6 +36,7 @@ struct TrampolineSite { std::vector OriginalBytes; std::vector Body; std::size_t ReturnBranchOffset; + std::vector Relocations = {}; }; struct ConvertResult { diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp index f54e2e1e0..2b835fd94 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTypes.hpp @@ -15,6 +15,12 @@ enum class Amd64OnlyLowering : std::uint8_t { Kept }; +struct StubRelocation { + std::size_t DisplacementOffset; + std::size_t InstructionEnd; + std::int64_t SiteTarget; +}; + struct Amd64OnlyMatch { std::string InstructionName; std::size_t Length; @@ -23,6 +29,7 @@ struct Amd64OnlyMatch { std::vector StubBody; std::size_t ReturnBranchOffset; bool Optional = false; + std::vector Relocations = {}; }; struct Amd64OnlySubstitutionReport { diff --git a/core/relinker/codegen/include/codegen/x86/ClzeroLowering.hpp b/core/relinker/codegen/include/codegen/x86/ClzeroLowering.hpp index 82b15ed41..029fd2bcc 100644 --- a/core/relinker/codegen/include/codegen/x86/ClzeroLowering.hpp +++ b/core/relinker/codegen/include/codegen/x86/ClzeroLowering.hpp @@ -4,14 +4,13 @@ #include #include #include -#include namespace Codegen { class ClzeroLowering { public: void EmitOutOfLine(StubBodyBuilder& body, const ClzeroOperands& operands) const; - [[nodiscard]] LoweredBody LowerOutOfLine(const ClzeroOperands& operands, std::span trailing = {}) const; + [[nodiscard]] LoweredBody LowerOutOfLine(const ClzeroOperands& operands) const; }; } diff --git a/core/relinker/codegen/include/codegen/x86/ReciprocalLowering.hpp b/core/relinker/codegen/include/codegen/x86/ReciprocalLowering.hpp index a4f20ea25..55eef20f2 100644 --- a/core/relinker/codegen/include/codegen/x86/ReciprocalLowering.hpp +++ b/core/relinker/codegen/include/codegen/x86/ReciprocalLowering.hpp @@ -3,14 +3,13 @@ #include #include -#include namespace Codegen { class ReciprocalLowering { public: void EmitOutOfLine(StubBodyBuilder& body, const ReciprocalOperands& operands) const; - [[nodiscard]] LoweredBody LowerOutOfLine(const ReciprocalOperands& operands, std::span trailing = {}) const; + [[nodiscard]] LoweredBody LowerOutOfLine(const ReciprocalOperands& operands) const; }; } diff --git a/core/relinker/codegen/include/codegen/x86/Sha1Lowering.hpp b/core/relinker/codegen/include/codegen/x86/Sha1Lowering.hpp index 1eabbd720..3507185d3 100644 --- a/core/relinker/codegen/include/codegen/x86/Sha1Lowering.hpp +++ b/core/relinker/codegen/include/codegen/x86/Sha1Lowering.hpp @@ -4,14 +4,13 @@ #include #include #include -#include namespace Codegen { class Sha1Lowering { public: void EmitOutOfLine(StubBodyBuilder& body, const Sha1Operands& operands) const; - [[nodiscard]] LoweredBody LowerOutOfLine(const Sha1Operands& operands, std::span trailing = {}) const; + [[nodiscard]] LoweredBody LowerOutOfLine(const Sha1Operands& operands) const; }; } diff --git a/core/relinker/codegen/include/codegen/x86/Sha256Lowering.hpp b/core/relinker/codegen/include/codegen/x86/Sha256Lowering.hpp index 2441149c6..12367e75b 100644 --- a/core/relinker/codegen/include/codegen/x86/Sha256Lowering.hpp +++ b/core/relinker/codegen/include/codegen/x86/Sha256Lowering.hpp @@ -4,14 +4,13 @@ #include #include #include -#include namespace Codegen { class Sha256Lowering { public: void EmitOutOfLine(StubBodyBuilder& body, const Sha256Operands& operands) const; - [[nodiscard]] LoweredBody LowerOutOfLine(const Sha256Operands& operands, std::span trailing = {}) const; + [[nodiscard]] LoweredBody LowerOutOfLine(const Sha256Operands& operands) const; }; } diff --git a/core/relinker/codegen/include/codegen/x86/Sse4aLowering.hpp b/core/relinker/codegen/include/codegen/x86/Sse4aLowering.hpp index 3df185400..f7e16a7da 100644 --- a/core/relinker/codegen/include/codegen/x86/Sse4aLowering.hpp +++ b/core/relinker/codegen/include/codegen/x86/Sse4aLowering.hpp @@ -15,8 +15,8 @@ class Sse4aLowering { public: [[nodiscard]] std::optional> LowerInPlace(const Sse4aOperands& operands, std::size_t originalLength) const; void EmitOutOfLine(StubBodyBuilder& body, const Sse4aOperands& operands) const; - [[nodiscard]] LoweredBody LowerOutOfLine(const Sse4aOperands& operands, std::span trailing = {}) const; - [[nodiscard]] LoweredBody LowerOutOfLine(std::span sequence, std::span trailing) const; + [[nodiscard]] LoweredBody LowerOutOfLine(const Sse4aOperands& operands) const; + [[nodiscard]] LoweredBody LowerOutOfLine(std::span sequence) const; }; } diff --git a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp index 692cbc6a4..212eeea54 100644 --- a/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp +++ b/core/relinker/codegen/include/codegen/x86/StubBodyBuilder.hpp @@ -1,10 +1,13 @@ #ifndef CODEGEN_X86_STUBBODYBUILDER_HPP #define CODEGEN_X86_STUBBODYBUILDER_HPP +#include +#include #include #include #include #include +#include #include #include @@ -13,6 +16,7 @@ namespace Codegen { struct LoweredBody { std::vector Bytes; std::size_t ReturnBranchOffset; + std::vector Relocations = {}; }; using StubConstant = std::array; @@ -26,12 +30,15 @@ struct MemoryOperand { std::int32_t Displacement; bool StackBase; std::size_t EncodedSize; + bool RipRelative; + std::size_t InstructionLength; }; [[nodiscard]] MemoryOperand DecodeMemoryOperand(const std::uint8_t* data, std::size_t length, std::size_t modRmOffset, std::uint8_t rex, std::vector prefixes); void EmitSse(std::vector& out, std::uint8_t prefix, std::initializer_list opcode, std::uint8_t dst, std::uint8_t src); void EmitShiftImm(std::vector& out, std::uint8_t extension, std::uint8_t reg, std::uint8_t imm); +void ApplyStubRelocations(std::span body, std::span relocations, std::uint64_t siteAddress, std::uint64_t stubAddress, Domain::FileByteOffset failureOffset); class StubBodyBuilder { public: @@ -45,6 +52,8 @@ public: void Spill(std::uint8_t reg); void Restore(std::uint8_t reg); void Raw(std::span bytes); + void Move(std::span instruction, std::optional ripDisplacementOffset); + void Advance(std::size_t length); [[nodiscard]] LoweredBody Finish(); private: @@ -57,7 +66,9 @@ private: std::vector _bytes; std::vector _fixups; std::vector _constants; + std::vector _relocations; std::size_t _stackDepth = 0; + std::size_t _siteOffset = 0; }; } diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index aa0bc0d09..0b3239d47 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -144,7 +144,7 @@ void Amd64OnlyConverter::_convertSegment( if (amdOnly && trailingBytes == 0) { sequence.push_back(bytes); taken.push_back(next); - } else if (amdOnly || info.FlowKind != ControlFlowKind::Sequential || info.HasRipRelativeDisp || info.HasBranchTarget) { + } else if (amdOnly || info.FlowKind != ControlFlowKind::Sequential || info.HasBranchTarget) { throw CodegenException("AMD-only instruction too short for a jump is followed by an instruction that cannot move", ph.Offset + following.Offset); } else { trailingBytes += following.Length; @@ -167,7 +167,8 @@ void Amd64OnlyConverter::_convertSegment( siteLength, std::vector(begin, begin + static_cast(siteLength)), stub.StubBody, - stub.ReturnBranchOffset + stub.ReturnBranchOffset, + stub.Relocations }); replacementLength = stub.StubBody.size(); consumed.insert(taken.begin(), taken.end()); diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 48bde0aaf..caff05f46 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -12,11 +12,14 @@ #include #include #include +#include #include #include #include #include +#include #include +#include #include namespace Codegen { @@ -67,6 +70,30 @@ const Entry& _sha1Entry(const Sha1Operands& operands) { return kSha1rnds4; } +Amd64OnlyMatch _trampoline(std::string name, const std::size_t length, LoweredBody body, const bool optional = false) { + return Amd64OnlyMatch{std::move(name), length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, optional, std::move(body.Relocations)}; +} + +void _moveTrailing(StubBodyBuilder& body, std::span trailing) { + const X64InstructionDecoder decoder; + while (!trailing.empty()) { + const auto info = decoder.DecodeInstruction(trailing.data(), trailing.size()); + if (info.Length == 0 || info.Length > trailing.size()) + throw CodegenException("Trailing bytes do not split into whole instructions"); + body.Move(trailing.first(info.Length), info.HasRipRelativeDisp ? std::optional(info.RipRelativeDispOffset) : std::nullopt); + trailing = trailing.subspan(info.Length); + } +} + +template +LoweredBody _outOfLine(const std::size_t length, const std::span trailing, const TEmit& emit) { + StubBodyBuilder body; + emit(body); + body.Advance(length); + _moveTrailing(body, trailing); + return body.Finish(); +} + Amd64OnlyMatch _inPlace(const Entry& entry, const std::size_t length, std::vector replacement) { while (replacement.size() < length) { const auto& nop = kNops[std::min(length - replacement.size(), std::size(kNops)) - 1]; @@ -131,26 +158,23 @@ Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchSse4a(const DecodedInstruction 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, trailing); const auto& name = operands.RegisterForm ? registerFormEntry.Name : entry.Name; - return Amd64OnlyMatch{name, instr.Length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset}; + return _trampoline(name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _lowering.EmitOutOfLine(body, operands); })); } Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchSha256(const DecodedInstruction& instr, std::span trailing) const { const auto operands = DecodeSha256(instr.Data, instr.Length); - auto body = _sha256Lowering.LowerOutOfLine(operands, trailing); - return Amd64OnlyMatch{_sha256Entry(operands).Name, instr.Length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset}; + return _trampoline(_sha256Entry(operands).Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _sha256Lowering.EmitOutOfLine(body, operands); })); } Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchSha1(const DecodedInstruction& instr, std::span trailing) const { const auto operands = DecodeSha1(instr.Data, instr.Length); - auto body = _sha1Lowering.LowerOutOfLine(operands, trailing); - return Amd64OnlyMatch{_sha1Entry(operands).Name, instr.Length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset}; + return _trampoline(_sha1Entry(operands).Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _sha1Lowering.EmitOutOfLine(body, operands); })); } Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchClzero(const DecodedInstruction& instr, std::span trailing) const { - auto body = _clzeroLowering.LowerOutOfLine(DecodeClzero(instr.Data, instr.Length), trailing); - return Amd64OnlyMatch{kClzero.Name, instr.Length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset}; + const auto operands = DecodeClzero(instr.Data, instr.Length); + return _trampoline(kClzero.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _clzeroLowering.EmitOutOfLine(body, operands); })); } std::optional Amd64OnlyInstructionMatcher::MatchSequence( @@ -190,11 +214,11 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( } else { return std::nullopt; } + body.Advance(instr.Length); } - body.Raw(trailing); - auto lowered = body.Finish(); + _moveTrailing(body, trailing); const bool optional = DecodeVexReciprocal(instructions.front().data(), instructions.front().size()).has_value(); - return Amd64OnlyMatch{name, instructions.front().size(), Amd64OnlyLowering::Trampoline, {}, std::move(lowered.Bytes), lowered.ReturnBranchOffset, optional}; + return _trampoline(name, instructions.front().size(), body.Finish(), optional); } std::optional Amd64OnlyInstructionMatcher::Match( @@ -229,8 +253,7 @@ std::optional Amd64OnlyInstructionMatcher::Match( return _validWait(instr) ? _inPlace(kMwaitx, length, std::vector(kPause.Bytes, kPause.Bytes + kPause.Size)) : _unsupported(kMwaitx, length); if (const auto reciprocal = DecodeVexReciprocal(data, length)) { - auto body = _reciprocalLowering.LowerOutOfLine(*reciprocal, trailing); - return Amd64OnlyMatch{_reciprocalEntry(*reciprocal).Name, length, Amd64OnlyLowering::Trampoline, {}, std::move(body.Bytes), body.ReturnBranchOffset, true}; + return _trampoline(_reciprocalEntry(*reciprocal).Name, length, _outOfLine(length, trailing, [&](StubBodyBuilder& body) { _reciprocalLowering.EmitOutOfLine(body, *reciprocal); }), true); } if (instr.IsClzero()) diff --git a/core/relinker/codegen/src/x86/ClzeroLowering.cpp b/core/relinker/codegen/src/x86/ClzeroLowering.cpp index c4a1ced26..507796eac 100644 --- a/core/relinker/codegen/src/x86/ClzeroLowering.cpp +++ b/core/relinker/codegen/src/x86/ClzeroLowering.cpp @@ -41,10 +41,9 @@ void ClzeroLowering::EmitOutOfLine(StubBodyBuilder& body, const ClzeroOperands& body.Restore(kScratch); } -LoweredBody ClzeroLowering::LowerOutOfLine(const ClzeroOperands& operands, std::span trailing) const { +LoweredBody ClzeroLowering::LowerOutOfLine(const ClzeroOperands& operands) const { StubBodyBuilder body; EmitOutOfLine(body, operands); - body.Raw(trailing); return body.Finish(); } diff --git a/core/relinker/codegen/src/x86/ReciprocalLowering.cpp b/core/relinker/codegen/src/x86/ReciprocalLowering.cpp index b6f8fcadc..8f6d794b4 100644 --- a/core/relinker/codegen/src/x86/ReciprocalLowering.cpp +++ b/core/relinker/codegen/src/x86/ReciprocalLowering.cpp @@ -55,10 +55,9 @@ void ReciprocalLowering::EmitOutOfLine(StubBodyBuilder& body, const ReciprocalOp body.Restore(scratch); } -LoweredBody ReciprocalLowering::LowerOutOfLine(const ReciprocalOperands& operands, std::span trailing) const { +LoweredBody ReciprocalLowering::LowerOutOfLine(const ReciprocalOperands& operands) const { StubBodyBuilder body; EmitOutOfLine(body, operands); - body.Raw(trailing); return body.Finish(); } diff --git a/core/relinker/codegen/src/x86/Sha1Lowering.cpp b/core/relinker/codegen/src/x86/Sha1Lowering.cpp index c8a4d2b5c..e8172687b 100644 --- a/core/relinker/codegen/src/x86/Sha1Lowering.cpp +++ b/core/relinker/codegen/src/x86/Sha1Lowering.cpp @@ -183,10 +183,9 @@ void Sha1Lowering::EmitOutOfLine(StubBodyBuilder& body, const Sha1Operands& oper } } -LoweredBody Sha1Lowering::LowerOutOfLine(const Sha1Operands& operands, std::span trailing) const { +LoweredBody Sha1Lowering::LowerOutOfLine(const Sha1Operands& operands) const { StubBodyBuilder body; EmitOutOfLine(body, operands); - body.Raw(trailing); return body.Finish(); } diff --git a/core/relinker/codegen/src/x86/Sha1Operands.cpp b/core/relinker/codegen/src/x86/Sha1Operands.cpp index 950267e12..69ea10a68 100644 --- a/core/relinker/codegen/src/x86/Sha1Operands.cpp +++ b/core/relinker/codegen/src/x86/Sha1Operands.cpp @@ -67,6 +67,8 @@ Sha1Operands DecodeSha1(const std::uint8_t* data, const std::size_t length) { } operands.Function = static_cast(data[immediateOffset] & 3); } + if (operands.Memory && operands.Memory->RipRelative && immediateOffset + (operands.Operation == Sha1Operation::Rnds4 ? 1 : 0) != length) + throw CodegenException("RIP-relative SHA-1 operand does not end the instruction"); return operands; } diff --git a/core/relinker/codegen/src/x86/Sha256Lowering.cpp b/core/relinker/codegen/src/x86/Sha256Lowering.cpp index 462436d0b..b11f652e7 100644 --- a/core/relinker/codegen/src/x86/Sha256Lowering.cpp +++ b/core/relinker/codegen/src/x86/Sha256Lowering.cpp @@ -205,10 +205,9 @@ void Sha256Lowering::EmitOutOfLine(StubBodyBuilder& body, const Sha256Operands& } } -LoweredBody Sha256Lowering::LowerOutOfLine(const Sha256Operands& operands, std::span trailing) const { +LoweredBody Sha256Lowering::LowerOutOfLine(const Sha256Operands& operands) const { StubBodyBuilder body; EmitOutOfLine(body, operands); - body.Raw(trailing); return body.Finish(); } diff --git a/core/relinker/codegen/src/x86/Sha256Operands.cpp b/core/relinker/codegen/src/x86/Sha256Operands.cpp index 625d1d295..a090514a1 100644 --- a/core/relinker/codegen/src/x86/Sha256Operands.cpp +++ b/core/relinker/codegen/src/x86/Sha256Operands.cpp @@ -57,6 +57,8 @@ Sha256Operands DecodeSha256(const std::uint8_t* data, const std::size_t length) operands.Destination = static_cast(((modrm >> ModRmRegShift) & ModRmRegMask) | (((rex & 0x4) != 0) ? 8 : 0)); if (((modrm >> ModRmModShift) & ModRmModMask) != ModRmModRegister) { operands.Memory = DecodeMemoryOperand(data, length, pos + 3, rex, std::move(prefixes)); + if (operands.Memory->RipRelative && pos + 3 + operands.Memory->EncodedSize != length) + throw CodegenException("RIP-relative SHA-256 operand does not end the instruction"); return operands; } operands.Source = static_cast((modrm & ModRmRmMask) | (((rex & 0x1) != 0) ? 8 : 0)); diff --git a/core/relinker/codegen/src/x86/Sse4aLowering.cpp b/core/relinker/codegen/src/x86/Sse4aLowering.cpp index 347231750..156cabc50 100644 --- a/core/relinker/codegen/src/x86/Sse4aLowering.cpp +++ b/core/relinker/codegen/src/x86/Sse4aLowering.cpp @@ -196,15 +196,14 @@ void Sse4aLowering::EmitOutOfLine(StubBodyBuilder& body, const Sse4aOperands& op _emitOutOfLine(body, operands); } -LoweredBody Sse4aLowering::LowerOutOfLine(const Sse4aOperands& operands, std::span trailing) const { - return LowerOutOfLine(std::span(&operands, 1), trailing); +LoweredBody Sse4aLowering::LowerOutOfLine(const Sse4aOperands& operands) const { + return LowerOutOfLine(std::span(&operands, 1)); } -LoweredBody Sse4aLowering::LowerOutOfLine(std::span sequence, std::span trailing) const { +LoweredBody Sse4aLowering::LowerOutOfLine(std::span sequence) const { StubBodyBuilder body; for (const auto& operands : sequence) _emitOutOfLine(body, operands); - body.Raw(trailing); return body.Finish(); } diff --git a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp index 7828b9691..a1b6e44b6 100644 --- a/core/relinker/codegen/src/x86/StubBodyBuilder.cpp +++ b/core/relinker/codegen/src/x86/StubBodyBuilder.cpp @@ -41,7 +41,7 @@ std::size_t _displacementSize(const std::uint8_t mod, const std::uint8_t rm, con using namespace X64OpcodeConstants; if (mod == ModRmModDisp8) return Disp8Size; - if (mod == ModRmModDisp32 || (mod == ModRmModIndirect && rm == ModRmRmSibPresent && (sib & SibBaseMask) == SibBaseDisp32)) + if (mod == ModRmModDisp32 || (mod == ModRmModIndirect && rm == ModRmRmRipRelative) || (mod == ModRmModIndirect && rm == ModRmRmSibPresent && (sib & SibBaseMask) == SibBaseDisp32)) return Disp32Size; return 0; } @@ -58,11 +58,10 @@ MemoryOperand DecodeMemoryOperand(const std::uint8_t* data, const std::size_t le if (modRmOffset >= length) throw CodegenException("Memory operand truncated before its ModRM byte"); const auto modrm = data[modRmOffset]; - MemoryOperand operand{std::move(prefixes), static_cast(rex & (kRexX | kRexB)), static_cast((modrm >> ModRmModShift) & ModRmModMask), static_cast(modrm & ModRmRmMask), 0, 0, false, 0}; + MemoryOperand operand{std::move(prefixes), static_cast(rex & (kRexX | kRexB)), static_cast((modrm >> ModRmModShift) & ModRmModMask), static_cast(modrm & ModRmRmMask), 0, 0, false, 0, false, length}; if (operand.Mod == ModRmModRegister) throw CodegenException("Register operand where a memory operand was expected"); - if (operand.Mod == ModRmModIndirect && operand.Rm == ModRmRmRipRelative) - throw CodegenException("RIP-relative memory operand cannot move into a stub"); + operand.RipRelative = operand.Mod == ModRmModIndirect && operand.Rm == ModRmRmRipRelative; auto pos = modRmOffset + 1; if (operand.Rm == ModRmRmSibPresent) { if (pos >= length) @@ -81,6 +80,21 @@ MemoryOperand DecodeMemoryOperand(const std::uint8_t* data, const std::size_t le return operand; } +void ApplyStubRelocations(const std::span body, const std::span relocations, const std::uint64_t siteAddress, const std::uint64_t stubAddress, const Domain::FileByteOffset failureOffset) { + using namespace X64OpcodeConstants; + for (const auto& relocation : relocations) { + if (relocation.InstructionEnd > body.size() || relocation.DisplacementOffset > relocation.InstructionEnd || relocation.InstructionEnd - relocation.DisplacementOffset < Disp32Size) + throw CodegenException("Stub relocation lies outside the stub body", failureOffset); + const auto target = siteAddress + static_cast(relocation.SiteTarget); + const auto displacement = static_cast(target - (stubAddress + relocation.InstructionEnd)); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw CodegenException("RIP-relative operand is out of rel32 range from its stub", failureOffset); + const auto value = static_cast(static_cast(displacement)); + for (std::size_t index = 0; index < Disp32Size; ++index) + body[relocation.DisplacementOffset + index] = static_cast(value >> (index * 8)); + } +} + void EmitSse(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))); } @@ -135,6 +149,11 @@ void StubBodyBuilder::Load(const std::uint8_t reg, const MemoryOperand& operand) _bytes.insert(_bytes.end(), {TwoByteOpcodeEscape, kMovdqu, static_cast((mod << ModRmModShift) | ((reg & 7) << ModRmRegShift) | operand.Rm)}); if (operand.Rm == ModRmRmSibPresent) _bytes.push_back(operand.Sib); + if (operand.RipRelative) { + _relocations.push_back({_bytes.size(), _bytes.size() + Disp32Size, static_cast(_siteOffset + operand.InstructionLength) + operand.Displacement}); + _bytes.insert(_bytes.end(), Disp32Size, 0); + return; + } const auto value = static_cast(static_cast(displacement)); for (std::size_t index = 0; index < _displacementSize(mod, operand.Rm, operand.Sib); ++index) _bytes.push_back(static_cast(value >> (index * 8))); @@ -158,6 +177,27 @@ void StubBodyBuilder::Raw(std::span bytes) { _bytes.insert(_bytes.end(), bytes.begin(), bytes.end()); } +void StubBodyBuilder::Move(const std::span instruction, const std::optional ripDisplacementOffset) { + using namespace X64OpcodeConstants; + const auto start = _bytes.size(); + Raw(instruction); + if (ripDisplacementOffset) { + if (*ripDisplacementOffset > instruction.size() || instruction.size() - *ripDisplacementOffset < Disp32Size) + throw CodegenException("RIP-relative displacement lies outside its instruction"); + std::uint32_t value = 0; + for (std::size_t index = 0; index < Disp32Size; ++index) { + value |= static_cast(instruction[*ripDisplacementOffset + index]) << (index * 8); + _bytes[start + *ripDisplacementOffset + index] = 0; + } + _relocations.push_back({start + *ripDisplacementOffset, start + instruction.size(), static_cast(_siteOffset + instruction.size()) + static_cast(value)}); + } + Advance(instruction.size()); +} + +void StubBodyBuilder::Advance(const std::size_t length) { + _siteOffset += length; +} + LoweredBody StubBodyBuilder::Finish() { const auto returnBranchOffset = _bytes.size(); _bytes.insert(_bytes.end(), kJmpRel32.Bytes, kJmpRel32.Bytes + kJmpRel32.Size); @@ -174,7 +214,7 @@ LoweredBody StubBodyBuilder::Finish() { for (std::size_t index = 0; index < 4; ++index) _bytes[fixup.DisplacementOffset + index] = static_cast(value >> (index * 8)); } - return {std::move(_bytes), returnBranchOffset}; + return {std::move(_bytes), returnBranchOffset, std::move(_relocations)}; } } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index b6091b327..882edd732 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -207,7 +208,11 @@ void sha256Operands() { const Bytes absolute = {0x0F, 0x38, 0xCC, 0x14, 0x25, 0x78, 0x56, 0x34, 0x12}; const auto noBase = Codegen::DecodeSha256(absolute.data(), absolute.size()); require(!noBase.Memory->StackBase && noBase.Memory->Displacement == 0x12345678, "SIB without a base was decoded incorrectly"); - requireFailure([] { const Bytes bytes = {0x0F, 0x38, 0xCC, 0x15, 0, 0, 0, 0}; (void)Codegen::DecodeSha256(bytes.data(), bytes.size()); }, "SHA-256 RIP-relative operand was accepted"); + const Bytes ripRelative = {0x0F, 0x38, 0xCC, 0x15, 0x78, 0x56, 0x34, 0x12}; + const auto rip = Codegen::DecodeSha256(ripRelative.data(), ripRelative.size()); + require(rip.Destination == 2 && rip.Memory && rip.Memory->RipRelative && rip.Memory->Displacement == 0x12345678 && rip.Memory->EncodedSize == 5 && rip.Memory->InstructionLength == 8 && !noBase.Memory->RipRelative, "SHA-256 RIP-relative operand was decoded incorrectly"); + requireFailure([] { const Bytes bytes = {0x0F, 0x38, 0xCC, 0x15, 0, 0, 0, 0, 0x90}; (void)Codegen::DecodeSha256(bytes.data(), bytes.size()); }, "SHA-256 RIP-relative operand that does not end the instruction was accepted"); + requireFailure([] { const Bytes bytes = {0x0F, 0x38, 0xCC, 0x15, 0, 0, 0}; (void)Codegen::DecodeSha256(bytes.data(), bytes.size()); }, "Truncated SHA-256 RIP-relative operand was accepted"); requireFailure([] { const Bytes bytes = {0x0F, 0x38, 0xCC, 0x54, 0x24}; (void)Codegen::DecodeSha256(bytes.data(), bytes.size()); }, "Truncated SHA-256 memory operand was accepted"); requireFailure([] { const Bytes bytes = {0x66, 0x0F, 0x38, 0xCB, 0xCA}; (void)Codegen::DecodeSha256(bytes.data(), bytes.size()); }, "Prefixed 0F 38 CB was decoded as SHA-256"); requireFailure([] { const Bytes bytes = {0x0F, 0x38, 0xC9, 0xCA}; (void)Codegen::DecodeSha256(bytes.data(), bytes.size()); }, "SHA-1 was decoded as SHA-256"); @@ -242,8 +247,14 @@ void sha1Operands() { const Bytes message = {0x0F, 0x38, 0xC9, 0x08}; const auto decodedMessage = Codegen::DecodeSha1(message.data(), message.size()); require(decodedMessage.Operation == Codegen::Sha1Operation::Msg1 && decodedMessage.Destination == 1 && decodedMessage.Memory && decodedMessage.Memory->Mod == 0 && decodedMessage.Memory->Rm == 0, "SHA1MSG1 memory operand was decoded incorrectly"); - requireFailure([] { const Bytes bytes = {0x0F, 0x38, 0xC9, 0x15, 0, 0, 0, 0}; (void)Codegen::DecodeSha1(bytes.data(), bytes.size()); }, "SHA-1 RIP-relative operand was accepted"); - requireFailure([] { const Bytes bytes = {0x0F, 0x3A, 0xCC, 0x15, 0, 0, 0, 0, 0x00}; (void)Codegen::DecodeSha1(bytes.data(), bytes.size()); }, "SHA1RNDS4 RIP-relative operand was accepted"); + const Bytes ripMessage = {0x0F, 0x38, 0xC9, 0x15, 0x10, 0x00, 0x00, 0x00}; + const auto ripDecoded = Codegen::DecodeSha1(ripMessage.data(), ripMessage.size()); + require(ripDecoded.Operation == Codegen::Sha1Operation::Msg1 && ripDecoded.Destination == 2 && ripDecoded.Memory && ripDecoded.Memory->RipRelative && ripDecoded.Memory->Displacement == 0x10 && ripDecoded.Memory->InstructionLength == 8, "SHA-1 RIP-relative operand was decoded incorrectly"); + const Bytes ripRounds = {0x0F, 0x3A, 0xCC, 0x15, 0xF0, 0xFF, 0xFF, 0xFF, 0x03}; + const auto ripRoundsDecoded = Codegen::DecodeSha1(ripRounds.data(), ripRounds.size()); + require(ripRoundsDecoded.Operation == Codegen::Sha1Operation::Rnds4 && ripRoundsDecoded.Function == 3 && ripRoundsDecoded.Memory && ripRoundsDecoded.Memory->RipRelative && ripRoundsDecoded.Memory->Displacement == -16 && ripRoundsDecoded.Memory->InstructionLength == 9, "SHA1RNDS4 RIP-relative operand was decoded incorrectly"); + requireFailure([] { const Bytes bytes = {0x0F, 0x38, 0xC9, 0x15, 0, 0, 0, 0, 0x90}; (void)Codegen::DecodeSha1(bytes.data(), bytes.size()); }, "SHA-1 RIP-relative operand that does not end the instruction was accepted"); + requireFailure([] { const Bytes bytes = {0x0F, 0x3A, 0xCC, 0x15, 0, 0, 0, 0, 0x00, 0x90}; (void)Codegen::DecodeSha1(bytes.data(), bytes.size()); }, "SHA1RNDS4 RIP-relative operand that does not end the instruction was accepted"); requireFailure([] { const Bytes bytes = {0x0F, 0x38, 0xC9, 0x54, 0x24}; (void)Codegen::DecodeSha1(bytes.data(), bytes.size()); }, "Truncated SHA-1 memory operand was accepted"); requireFailure([] { const Bytes bytes = {0x0F, 0x3A, 0xCC, 0xCA}; (void)Codegen::DecodeSha1(bytes.data(), bytes.size()); }, "SHA1RNDS4 without its immediate was accepted"); requireFailure([] { const Bytes bytes = {0x0F, 0x3A, 0xCC, 0x50, 0x02}; (void)Codegen::DecodeSha1(bytes.data(), bytes.size()); }, "SHA1RNDS4 memory form without its immediate was accepted"); @@ -365,6 +376,49 @@ void goldenBodies() { require(insertqRegisterForm.Bytes[0] == 0x48 && insertqRegisterForm.Bytes.size() % 16 == 0 && insertqRegisterForm.ReturnBranchOffset < insertqRegisterForm.Bytes.size(), "INSERTQ register form body does not start with the red-zone skip"); } +void ripRelativeBodies() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + const auto relocated = [](const Codegen::Amd64OnlyMatch& match, const std::size_t displacementOffset, const std::size_t instructionEnd, const std::int64_t siteTarget) { + return match.Relocations.size() == 1 && match.Relocations[0].DisplacementOffset == displacementOffset && match.Relocations[0].InstructionEnd == instructionEnd && match.Relocations[0].SiteTarget == siteTarget; + }; + const Bytes message = {0x0F, 0x38, 0xCC, 0x15, 0x78, 0x56, 0x34, 0x12}; + const auto sha256 = matcher->Match(message.data(), message.size()); + require(sha256 && sha256->StubBody.size() > 21 && Bytes(sha256->StubBody.begin() + 13, sha256->StubBody.begin() + 21) == Bytes{0xF3, 0x0F, 0x6F, 0x0D, 0x00, 0x00, 0x00, 0x00}, "SHA-256 RIP-relative operand was not reloaded through a RIP-relative MOVDQU"); + require(relocated(*sha256, 17, 21, 8 + 0x12345678), "SHA-256 RIP-relative operand has the wrong relocation"); + const Bytes prefixed = {0x65, 0x67, 0x0F, 0x38, 0xCC, 0x15, 0x78, 0x56, 0x34, 0x12}; + const auto segmentOverride = matcher->Match(prefixed.data(), prefixed.size()); + require(segmentOverride && segmentOverride->StubBody.size() > 23 && Bytes(segmentOverride->StubBody.begin() + 13, segmentOverride->StubBody.begin() + 23) == Bytes{0x65, 0x67, 0xF3, 0x0F, 0x6F, 0x0D, 0x00, 0x00, 0x00, 0x00}, "RIP-relative reload lost the segment or address-size prefix"); + require(relocated(*segmentOverride, 19, 23, 10 + 0x12345678), "Prefixed RIP-relative operand has the wrong relocation"); + const Bytes rounds = {0x0F, 0x3A, 0xCC, 0x15, 0xF0, 0xFF, 0xFF, 0xFF, 0x03}; + const auto sha1 = matcher->Match(rounds.data(), rounds.size()); + require(sha1 && sha1->StubBody.size() > 21 && Bytes(sha1->StubBody.begin() + 13, sha1->StubBody.begin() + 21) == Bytes{0xF3, 0x0F, 0x6F, 0x05, 0x00, 0x00, 0x00, 0x00}, "SHA1RNDS4 RIP-relative operand was not reloaded through a RIP-relative MOVDQU"); + require(relocated(*sha1, 17, 21, 9 - 16), "SHA1RNDS4 RIP-relative operand does not count its immediate"); + const Bytes extrq = {0x66, 0x0F, 0x79, 0xCA}; + const Bytes followers = {0x90, 0x48, 0x8D, 0x05, 0x10, 0x00, 0x00, 0x00}; + const auto moved = matcher->Match(extrq.data(), extrq.size(), followers); + require(moved && moved->ReturnBranchOffset >= followers.size(), "EXTRQ with followers was not lowered through a stub"); + const auto start = moved->ReturnBranchOffset - followers.size(); + require(Bytes(moved->StubBody.begin() + static_cast(start), moved->StubBody.begin() + static_cast(moved->ReturnBranchOffset)) == Bytes{0x90, 0x48, 0x8D, 0x05, 0x00, 0x00, 0x00, 0x00}, "Moved RIP-relative LEA was not copied with a cleared displacement"); + require(relocated(*moved, start + 4, start + 8, 4 + 8 + 0x10), "Moved RIP-relative LEA has the wrong relocation"); + const std::vector> sequence = {extrq, message}; + const auto pair = matcher->MatchSequence(sequence, {}); + require(pair && pair->Relocations.size() == 1 && pair->Relocations[0].SiteTarget == 4 + 8 + 0x12345678, "RIP-relative operand in a sequence is not relative to the site start"); +} + +void stubRelocations() { + Bytes body(8, 0xCC); + const std::vector relocations = {{2, 6, 0x1040}}; + Codegen::ApplyStubRelocations(body, relocations, 0x401000, 0x400F00, 0); + require(read(body, 2) == 0x401000 + 0x1040 - (0x400F00 + 6) && body[0] == 0xCC && body[6] == 0xCC, "Stub relocation was not resolved against the site address"); + Codegen::ApplyStubRelocations(body, relocations, 0x1000, 0x7FFF0000, 0); + require(read(body, 2) == 0x1000 + 0x1040 - (0x7FFF0000 + 6), "Stub placed above its site was not relocated backwards"); + requireFailure([&] { Codegen::ApplyStubRelocations(body, relocations, 0x1000, 0x1000 + 0x90000000ull, 0); }, "RIP-relative operand beyond rel32 range was accepted"); + const std::vector outside = {{6, 10, 0}}; + requireFailure([&] { Codegen::ApplyStubRelocations(body, outside, 0x1000, 0x2000, 0); }, "Stub relocation outside the body was accepted"); + const std::vector narrow = {{4, 6, 0}}; + requireFailure([&] { Codegen::ApplyStubRelocations(body, narrow, 0x1000, 0x2000, 0); }, "Stub relocation narrower than a displacement was accepted"); +} + Bytes segmentFixture() { Bytes file(0x300, 0xCC); const Bytes text = { @@ -431,7 +485,16 @@ void converterRipRelativeFollower() { }; for (const auto& following : kRipRelativeVectorLoads) { const auto [file, header] = withFollower(following); - require(failureOffset([&] { (void)converter->Convert(file, {header}); }, "RIP-relative vector load was moved into an EXTRQ stub") == 0x204, "RIP-relative follower failure does not carry its file offset"); + const auto result = converter->Convert(file, {header}); + require(result.Trampolines.size() == 1 && result.Trampolines[0].Length == 4 + following.size(), "RIP-relative vector load was not moved into the EXTRQ stub"); + const auto& site = result.Trampolines[0]; + const auto info = Codegen::X64InstructionDecoder{}.DecodeInstruction(following.data(), following.size()); + const auto start = site.ReturnBranchOffset - following.size(); + require(site.Relocations.size() == 1 && site.Relocations[0].DisplacementOffset == start + info.RipRelativeDispOffset && site.Relocations[0].InstructionEnd == site.ReturnBranchOffset && site.Relocations[0].SiteTarget == static_cast(4 + following.size()) + 0x10, "Moved RIP-relative vector load has the wrong relocation"); + } + for (const Bytes& following : {Bytes{0xFF, 0x15, 0x10, 0x00, 0x00, 0x00}, Bytes{0xFF, 0x25, 0x10, 0x00, 0x00, 0x00}}) { + const auto [file, header] = withFollower(following); + require(failureOffset([&] { (void)converter->Convert(file, {header}); }, "RIP-relative indirect branch was moved into an EXTRQ stub") == 0x204, "RIP-relative branch failure does not carry its file offset"); } for (const auto& following : kRegisterVectorOperations) { const auto [file, header] = withFollower(following); @@ -466,9 +529,12 @@ void converterSha256() { const auto memoryResult = converter->Convert(memoryForm, {segmentHeader(text.size())}); require(memoryResult.Trampolines.size() == 2 && memoryResult.Reports[1].InstructionName == "SHA256MSG2", "SHA-256 memory form was not lowered through a stub"); auto ripRelative = file; - const Bytes ripMessage = {0x0F, 0x38, 0xCD, 0x2D, 0x00, 0x00, 0x00, 0x00, 0xC3}; + const Bytes ripMessage = {0x0F, 0x38, 0xCD, 0x2D, 0x40, 0x00, 0x00, 0x00, 0xC3}; std::copy(ripMessage.begin(), ripMessage.end(), ripRelative.begin() + 0x20C); - require(failureOffset([&] { (void)converter->Convert(ripRelative, {segmentHeader(text.size() + 3)}); }, "SHA-256 RIP-relative form was accepted") == 0x20C, "SHA-256 operand failure does not carry the file offset"); + const auto ripResult = converter->Convert(ripRelative, {segmentHeader(text.size() + 3)}); + require(ripResult.Trampolines.size() == 2 && ripResult.Bytes == ripRelative && ripResult.Reports[1].InstructionName == "SHA256MSG2", "SHA-256 RIP-relative form was not lowered through a stub"); + const auto& ripSite = ripResult.Trampolines[1]; + require(ripSite.Offset == 0x20C && ripSite.Length == 8 && ripSite.Relocations.size() == 1 && ripSite.Relocations[0].SiteTarget == 8 + 0x40 && ripSite.Relocations[0].InstructionEnd <= ripSite.ReturnBranchOffset, "SHA-256 RIP-relative site was recorded without its relocation"); } void converterSha1() { @@ -498,9 +564,12 @@ void converterSha1() { const auto memoryResult = converter->Convert(memoryForm, {segmentHeader(text.size())}); require(memoryResult.Trampolines.size() == 3 && memoryResult.Reports[2].InstructionName == "SHA1MSG2", "SHA-1 memory form was not lowered through a stub"); auto ripRelative = file; - const Bytes ripMessage = {0x0F, 0x38, 0xCA, 0x2D, 0x00, 0x00, 0x00, 0x00, 0xC3}; + const Bytes ripMessage = {0x0F, 0x38, 0xCA, 0x2D, 0xF0, 0xFF, 0xFF, 0xFF, 0xC3}; std::copy(ripMessage.begin(), ripMessage.end(), ripRelative.begin() + 0x211); - require(failureOffset([&] { (void)converter->Convert(ripRelative, {segmentHeader(text.size() + 3)}); }, "SHA-1 RIP-relative form was accepted") == 0x211, "SHA-1 operand failure does not carry the file offset"); + const auto ripResult = converter->Convert(ripRelative, {segmentHeader(text.size() + 3)}); + require(ripResult.Trampolines.size() == 3 && ripResult.Bytes == ripRelative && ripResult.Reports[2].InstructionName == "SHA1MSG2", "SHA-1 RIP-relative form was not lowered through a stub"); + const auto& ripSite = ripResult.Trampolines[2]; + require(ripSite.Offset == 0x211 && ripSite.Length == 8 && ripSite.Relocations.size() == 1 && ripSite.Relocations[0].SiteTarget == 8 - 16 && ripSite.Relocations[0].InstructionEnd <= ripSite.ReturnBranchOffset, "SHA-1 RIP-relative site was recorded without its relocation"); } void converterMonitorWait() { @@ -586,8 +655,10 @@ void converterReciprocal() { auto ripRelative = file; const Bytes load = {0xC5, 0xF8, 0x52, 0xD5, 0x8B, 0x05, 0x10, 0x00, 0x00, 0x00, 0xC3}; std::copy(load.begin(), load.end(), ripRelative.begin() + 0x200); - const auto kept = converter->Convert(ripRelative, {segmentHeader(load.size())}); - require(kept.Trampolines.empty() && kept.KeptCount == 1 && kept.Bytes == ripRelative, "A following RIP-relative instruction was moved into a stub"); + const auto moved = converter->Convert(ripRelative, {segmentHeader(load.size())}); + require(moved.Trampolines.size() == 1 && moved.KeptCount == 0 && moved.Bytes == ripRelative && moved.Trampolines[0].Length == 10, "VRSQRTPS did not absorb the following RIP-relative load"); + const auto& movedSite = moved.Trampolines[0]; + require(movedSite.Relocations.size() == 1 && movedSite.Relocations[0].DisplacementOffset == movedSite.ReturnBranchOffset - 4 && movedSite.Relocations[0].InstructionEnd == movedSite.ReturnBranchOffset && movedSite.Relocations[0].SiteTarget == 10 + 0x10, "Absorbed RIP-relative load has the wrong relocation"); } void converterStrayRex() { @@ -681,35 +752,40 @@ 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"); +Elfpatcher::Linux::LinuxElfPatcher linuxPatcher() { const auto byteWriter = std::make_shared(); - Elfpatcher::Linux::LinuxElfPatcher patcher( + return Elfpatcher::Linux::LinuxElfPatcher( 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"); +} + +std::uint64_t executableFileOffset(const Bytes& output, const std::uint64_t address) { 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; + if (address < vaddr || address >= 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; + return read(output, header + 8) + (address - vaddr); } - require(found, "Linux stub is not inside a PT_LOAD segment"); + throw std::runtime_error("Linux stub is not inside a PT_LOAD segment"); +} + +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"); + auto patcher = linuxPatcher(); + 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 bodyOffset = executableFileOffset(output, static_cast(target)); 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())); @@ -719,6 +795,23 @@ void linuxPlacement() { requireFailure([&] { (void)patcher.Patch(altered, headers, {}, 0, "$ORIGIN/libs", true, false, converted.Trampolines); }, "Changed Linux site bytes were accepted"); } +void linuxRipRelativePlacement() { + Bytes text = {0x0F, 0x38, 0xCC, 0x15, 0x00, 0x00, 0x00, 0x00, 0xC3}; + write(text, 4, 0x2040 - 0x1008); + const auto source = elfFixture(text); + const auto headers = elfHeaders(); + const auto converted = Codegen::MakeAmd64OnlyConverter()->Convert(source, {headers[0]}); + require(converted.Trampolines.size() == 1 && converted.Trampolines[0].Relocations.size() == 1, "RIP-relative Linux fixture was not lowered with a relocation"); + auto patcher = linuxPatcher(); + const auto output = patcher.Patch(converted.Bytes, headers, {}, 0, "$ORIGIN/libs", true, false, converted.Trampolines); + require(output[0x200] == 0xE9, "RIP-relative Linux site was not replaced by a jump"); + const auto stub = 0x1000 + 5 + static_cast(read(output, 0x201)); + const auto& relocation = converted.Trampolines[0].Relocations[0]; + const auto bodyOffset = executableFileOffset(output, static_cast(stub)); + const auto operand = stub + static_cast(relocation.InstructionEnd) + read(output, bodyOffset + relocation.DisplacementOffset); + require(operand == 0x2040, "Relocated RIP-relative operand does not reach the original address"); +} + } #if defined(__linux__) && defined(__x86_64__) @@ -918,19 +1011,24 @@ struct StubRun { std::uint64_t Rcx; std::uint64_t Flags; std::uint64_t Xmm[16][2]; + std::uint64_t SiteAddress; }; -StubRun runStubBody(const Codegen::Amd64OnlyMatch& match, const std::uint64_t rax, const std::uint64_t (&xmmIn)[16][2]) { +StubRun runStubBody(const Codegen::Amd64OnlyMatch& match, const std::uint64_t rax, const std::uint64_t (&xmmIn)[16][2], const Bytes& data = {}) { require(match.Lowering == Codegen::Amd64OnlyLowering::Trampoline, "Stub was not produced"); auto body = match.StubBody; const auto ret = body.size(); body.push_back(0xC3); const auto displacement = static_cast(ret - (match.ReturnBranchOffset + 5)); std::memcpy(body.data() + match.ReturnBranchOffset + 1, &displacement, sizeof(displacement)); - void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(body.size() <= 4096 && data.size() <= 4096, "Stub or its data does not fit in a page"); + auto* code = static_cast(mmap(nullptr, 3 * 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); require(code != MAP_FAILED, "cannot map executable memory for the stub"); - std::memcpy(code, body.data(), body.size()); StubRun run{}; + run.SiteAddress = reinterpret_cast(code) + 4096; + Codegen::ApplyStubRelocations(std::span(body).first(match.ReturnBranchOffset), match.Relocations, run.SiteAddress, reinterpret_cast(code), 0); + std::memcpy(code, body.data(), body.size()); + std::copy(data.begin(), data.end(), code + 2 * 4096); asm volatile( "movdqu 0x00(%[xmmIn]), %%xmm0\n\t" "movdqu 0x10(%[xmmIn]), %%xmm1\n\t" @@ -978,7 +1076,7 @@ StubRun runStubBody(const Codegen::Amd64OnlyMatch& match, const std::uint64_t ra : [flags] "=&r"(run.Flags), [raxOut] "=&r"(run.Rax), [rcxOut] "=&r"(run.Rcx) : [xmmIn] "r"(xmmIn), [xmmOut] "r"(run.Xmm), [raxIn] "r"(rax), [code] "r"(code) : "rax", "rcx", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15", "memory", "cc"); - munmap(code, 4096); + munmap(code, 3 * 4096); return run; } @@ -1098,6 +1196,57 @@ void sha1Execution() { } } } + +void ripRelativeExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(); + std::uint64_t xmmIn[16][2]; + for (unsigned reg = 0; reg < 16; ++reg) { + xmmIn[reg][0] = 0x9e3779b97f4a7c15ull * (reg + 1); + xmmIn[reg][1] = 0xc2b2ae3d27d4eb4full * (reg + 3); + } + const std::uint64_t words[2] = {0x510e527f9b05688cull, 0x1f83d9ab5be0cd19ull}; + Bytes data(0x40, 0xA5); + std::memcpy(data.data() + 0x20, words, sizeof(words)); + const std::uint64_t rax = 0x5A5A5A5A5A5A5A5Aull; + const auto unchanged = [&](const StubRun& run, const std::uint32_t changed, const char* message) { + for (unsigned reg = 0; reg < 16; ++reg) + if ((changed & (1u << reg)) == 0) + require(run.Xmm[reg][0] == xmmIn[reg][0] && run.Xmm[reg][1] == xmmIn[reg][1], message); + require(run.Rcx == 0x1122334455667788ull && (run.Flags & 0x8D5) == (0x8D7 & 0x8D5), message); + }; + for (const std::uint8_t opcode : {std::uint8_t{0xCB}, std::uint8_t{0xCC}, std::uint8_t{0xCD}}) { + Bytes site = {0x0F, 0x38, opcode, 0x15, 0x00, 0x00, 0x00, 0x00}; + write(site, 4, 4096 + 0x20 - 8); + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Relocations.size() == 1, "SHA-256 RIP-relative stub was not produced"); + const auto run = runStubBody(*match, rax, xmmIn, data); + require(std::array{run.Xmm[2][0], run.Xmm[2][1]} == sha256Reference(opcode, xmmIn[2], words, xmmIn[0]), "SHA-256 RIP-relative stub did not read the original operand"); + require(run.Rax == rax, "SHA-256 RIP-relative stub changed rax"); + unchanged(run, 1u << 2, "SHA-256 RIP-relative stub clobbered a register or RFLAGS"); + } + Bytes rounds = {0x0F, 0x3A, 0xCC, 0x15, 0x00, 0x00, 0x00, 0x00, 0x02}; + write(rounds, 4, 4096 + 0x20 - 9); + const auto roundsMatch = matcher->Match(rounds.data(), rounds.size()); + require(roundsMatch && roundsMatch->Relocations.size() == 1, "SHA1RNDS4 RIP-relative stub was not produced"); + std::uint32_t x[4]; + std::uint32_t y[4]; + std::memcpy(x, xmmIn[2], sizeof(x)); + std::memcpy(y, words, sizeof(y)); + const auto expected = sha1Reference(Codegen::DecodeSha1(rounds.data(), rounds.size()), x, y); + const auto roundsRun = runStubBody(*roundsMatch, rax, xmmIn, data); + require(std::memcmp(roundsRun.Xmm[2], expected.data(), 16) == 0, "SHA1RNDS4 RIP-relative stub did not read the original operand"); + unchanged(roundsRun, 1u << 2, "SHA1RNDS4 RIP-relative stub clobbered a register or RFLAGS"); + const Bytes extrq = {0x66, 0x0F, 0x79, 0xCA}; + Bytes followers = {0xF3, 0x0F, 0x6F, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8D, 0x05, 0x00, 0x00, 0x00, 0x00}; + write(followers, 4, 4096 + 0x20 - 12); + write(followers, 11, 4096 + 0x30 - 19); + const auto moved = matcher->Match(extrq.data(), extrq.size(), followers); + require(moved && moved->Relocations.size() == 2, "EXTRQ with RIP-relative followers was not lowered with two relocations"); + const auto movedRun = runStubBody(*moved, rax, xmmIn, data); + require(movedRun.Xmm[3][0] == words[0] && movedRun.Xmm[3][1] == words[1], "Moved RIP-relative MOVDQU did not load from the original address"); + require(movedRun.Rax == movedRun.SiteAddress + 4096 + 0x30, "Moved RIP-relative LEA did not compute the original address"); + unchanged(movedRun, (1u << 1) | (1u << 3), "Stub with moved RIP-relative instructions clobbered a register or RFLAGS"); +} #else void reciprocalExecution() {} void registerFormExecution() {} @@ -1105,6 +1254,7 @@ void sha1Execution() {} void sha256Execution() {} void clzeroExecution() {} void immediateFormExecution() {} +void ripRelativeExecution() {} #endif void scannerZeroTail() { @@ -1126,12 +1276,15 @@ int main() { reciprocalOperands(); matcherSubstitutions(); goldenBodies(); + ripRelativeBodies(); + stubRelocations(); registerFormExecution(); immediateFormExecution(); sha256Execution(); sha1Execution(); clzeroExecution(); reciprocalExecution(); + ripRelativeExecution(); converterSegment(); converterRipRelativeFollower(); converterSha256(); @@ -1143,6 +1296,7 @@ int main() { rewriterStrayRex(); converterFailureOffsets(); linuxPlacement(); + linuxRipRelativePlacement(); scannerZeroTail(); std::cout << "AMD64-only converter tests passed\n"; } catch (const std::exception& error) { diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 3e52300dc..24746a4a1 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include namespace Elfpatcher::Linux { @@ -47,6 +49,7 @@ void LinuxElfPatcher::_appendTrampoline( const auto bodyVaddr = vaddrOfExtraBlockOffset(bodyOff); for (const std::uint8_t b : site.Body) buf.push_back(b); + Codegen::ApplyStubRelocations(std::span(buf.data() + bodyOff, site.ReturnBranchOffset), site.Relocations, site.Address, bodyVaddr, site.Offset); const auto inRange = [](const std::int64_t displacement) { return displacement >= std::numeric_limits::min() && displacement <= std::numeric_limits::max(); diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index 4e5b57824..81fc8549e 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -1,8 +1,10 @@ #include #include +#include #include #include #include +#include namespace Elfpatcher { @@ -154,6 +156,7 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma const auto returnDisplacement = displacement(returnAddress, stubReturn, site.Offset); const auto siteDisplacement = displacement(stubAddress, siteNext, site.Offset); bytes.insert(bytes.end(), site.Body.begin(), site.Body.end()); + Codegen::ApplyStubRelocations(std::span(bytes.data() + stubOffset, site.ReturnBranchOffset), site.Relocations, site.Address, stubAddress, site.Offset); Io::WriteU32(bytes, stubOffset + site.ReturnBranchOffset + 1, static_cast(returnDisplacement)); std::fill_n(bytes.begin() + static_cast(site.Offset), site.Length, kNop1.Bytes[0]); diff --git a/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp index 0dc522242..f8d06aa53 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp @@ -1,9 +1,11 @@ #include #include #include +#include #include #include #include +#include namespace Elfpatcher::Windows { @@ -40,6 +42,7 @@ void WindowsTrampolineBuilder::Build(const std::vector& const auto stubRva = CheckedRva(sectionRva + bytes.size()); const auto bodyOffset = bytes.size(); bytes.insert(bytes.end(), site.Body.begin(), site.Body.end()); + Codegen::ApplyStubRelocations(std::span(bytes.data() + bodyOffset, site.ReturnBranchOffset), site.Relocations, siteRva, stubRva, site.Offset); 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()) diff --git a/core/relinker/relinker/tests/test_guest_intel_trampolines.py b/core/relinker/relinker/tests/test_guest_intel_trampolines.py index c6b2222c9..a0c063d85 100644 --- a/core/relinker/relinker/tests/test_guest_intel_trampolines.py +++ b/core/relinker/relinker/tests/test_guest_intel_trampolines.py @@ -10,6 +10,9 @@ from test_linux_load_alignment import fixture as executable_fixture SITE = bytes.fromhex("f2 0f 78 db 08 08") SITE_ADDRESSES = (0x1002, 0x1012) PLAIN_SITE = b"\x90" * len(SITE) +RIP_DISPLACEMENT = 0x10F6 +RIP_SITE = bytes.fromhex("0f 38 cc 15") + struct.pack(" Date: Thu, 8 Oct 2026 11:10:42 +0200 Subject: [PATCH 2/2] docs: mark the VRSQRTPS/VRCPPS native-site count as predating RIP-relative followers Co-Authored-By: Claude --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f3a1ca591..25d560411 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -284,7 +284,7 @@ Throughout the project, every function at every stage either **does exactly what - [8_SRGB color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [COPY_DATA from the GPU clock](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. -- `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564). +- `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). - [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates the 101-key (US) arrangement and the alphanumeric layer of the 106-key (Japanese) one, which follows the JIS layout of the HID usages: the Yen (0x89) and Ro (0x87) keys return `\` (0x5C) unshifted, as JIS X 0201 places the yen sign there; whether the console returns U+00A5 instead is unverified. With the Kana LED (bit 4, the HID position) set the 106-key arrangement throws, as the kana the console returns are unknown. Ctrl and Alt do not change the character. - [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit and sceAudioOut2MasteringSetParam](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. - [sceImeKeyboardGetInfo](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered.