feat(relinker): lower SHA-256 instructions with a memory operand

SHA256RNDS2/MSG1/MSG2 with a memory source made `--to-intel` fail the
whole relink. The stub now spills a scratch xmm, loads the operand into
it with MOVDQU (AMD does not fault on a misaligned SHA operand, so the
unaligned load keeps that behaviour) and runs the register-form
lowering. RSP-based operands get the spill depth added to their
displacement. RIP-relative operands still fail: the stub runs at a
different address.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Adrià Franch 2026-10-03 08:31:51 +02:00
parent 870bdd814f
commit 5fc16a9548
6 changed files with 127 additions and 8 deletions

View File

@ -1,8 +1,10 @@
#ifndef CODEGEN_X86_SHA256OPERANDS_HPP
#define CODEGEN_X86_SHA256OPERANDS_HPP
#include <codegen/x86/StubBodyBuilder.hpp>
#include <cstddef>
#include <cstdint>
#include <optional>
namespace Codegen {
@ -16,6 +18,7 @@ struct Sha256Operands {
Sha256Operation Operation;
std::uint8_t Destination;
std::uint8_t Source;
std::optional<MemoryOperand> Memory;
};
[[nodiscard]] Sha256Operands DecodeSha256(const std::uint8_t* data, std::size_t length);

View File

@ -17,6 +17,18 @@ struct LoweredBody {
using StubConstant = std::array<std::uint8_t, 16>;
struct MemoryOperand {
std::vector<std::uint8_t> Prefixes;
std::uint8_t RexIndexBase;
std::uint8_t Mod;
std::uint8_t Rm;
std::uint8_t Sib;
std::int32_t Displacement;
bool StackBase;
};
[[nodiscard]] MemoryOperand DecodeMemoryOperand(const std::uint8_t* data, std::size_t length, std::size_t modRmOffset, std::uint8_t rex, std::vector<std::uint8_t> prefixes);
void EmitSse(std::vector<std::uint8_t>& out, std::uint8_t prefix, std::initializer_list<std::uint8_t> opcode, std::uint8_t dst, std::uint8_t src);
void EmitShiftImm(std::vector<std::uint8_t>& out, std::uint8_t extension, std::uint8_t reg, std::uint8_t imm);
@ -28,6 +40,7 @@ public:
void ShiftImm(std::uint8_t extension, std::uint8_t reg, std::uint8_t imm);
void ShiftDwordImm(std::uint8_t extension, std::uint8_t reg, std::uint8_t imm);
void RipOperand(std::initializer_list<std::uint8_t> opcode, std::uint8_t reg, const StubConstant& constant);
void Load(std::uint8_t reg, const MemoryOperand& operand);
void Spill(std::uint8_t reg);
void Restore(std::uint8_t reg);
void Raw(std::span<const std::uint8_t> bytes);
@ -43,6 +56,7 @@ private:
std::vector<std::uint8_t> _bytes;
std::vector<Fixup> _fixups;
std::vector<StubConstant> _constants;
std::size_t _stackDepth = 0;
};
}

View File

@ -182,6 +182,16 @@ void _emitMessage2(StubBodyBuilder& body, const Sha256Operands& operands) {
}
void Sha256Lowering::EmitOutOfLine(StubBodyBuilder& body, const Sha256Operands& operands) const {
if (operands.Memory) {
auto loaded = operands;
loaded.Memory.reset();
loaded.Source = _scratch<1>({operands.Operation, operands.Destination, operands.Destination, {}})[0];
body.Spill(loaded.Source);
body.Load(loaded.Source, *operands.Memory);
EmitOutOfLine(body, loaded);
body.Restore(loaded.Source);
return;
}
switch (operands.Operation) {
case Sha256Operation::Rnds2:
_emitRounds(body, operands);

View File

@ -1,6 +1,8 @@
#include <codegen/x86/Sha256Operands.hpp>
#include <codegen/x86/X64OpcodeConstants.hpp>
#include <codegen/CodegenException.hpp>
#include <utility>
#include <vector>
namespace Codegen {
@ -9,6 +11,7 @@ using namespace X64OpcodeConstants;
Sha256Operands DecodeSha256(const std::uint8_t* data, const std::size_t length) {
std::size_t pos = 0;
std::uint8_t rex = 0;
std::vector<std::uint8_t> prefixes;
while (pos < length) {
const std::uint8_t b = data[pos];
@ -25,6 +28,8 @@ Sha256Operands DecodeSha256(const std::uint8_t* data, const std::size_t length)
b != PrefixSegEs && b != PrefixSegFs && b != PrefixSegGs) {
break;
}
if (b != PrefixLock)
prefixes.push_back(b);
rex = 0;
pos += 1;
}
@ -49,11 +54,11 @@ Sha256Operands DecodeSha256(const std::uint8_t* data, const std::size_t length)
}
const std::uint8_t modrm = data[pos + 3];
if (((modrm >> ModRmModShift) & ModRmModMask) != ModRmModRegister) {
throw CodegenException("SHA-256 instruction with a memory operand");
}
operands.Destination = static_cast<std::uint8_t>(((modrm >> ModRmRegShift) & ModRmRegMask) | (((rex & 0x4) != 0) ? 8 : 0));
if (((modrm >> ModRmModShift) & ModRmModMask) != ModRmModRegister) {
operands.Memory = DecodeMemoryOperand(data, length, pos + 3, rex, std::move(prefixes));
return operands;
}
operands.Source = static_cast<std::uint8_t>((modrm & ModRmRmMask) | (((rex & 0x1) != 0) ? 8 : 0));
return operands;
}

View File

@ -1,5 +1,8 @@
#include <codegen/x86/StubBodyBuilder.hpp>
#include <codegen/x86/Amd64OnlySubstitutionTable.hpp>
#include <codegen/x86/X64OpcodeConstants.hpp>
#include <codegen/CodegenException.hpp>
#include <limits>
namespace Codegen {
@ -11,6 +14,7 @@ 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 kRexX = 0x02;
constexpr std::uint8_t kRexB = 0x01;
constexpr std::uint8_t kModRmRegister = 0xC0;
constexpr std::uint8_t kModRmRip = 0x05;
@ -18,6 +22,7 @@ constexpr std::uint8_t kModRmRspBase = 0x04;
constexpr std::uint8_t kSibRsp = 0x24;
constexpr std::uint8_t kShiftDwords = 0x72;
constexpr std::uint8_t kShiftQwords = 0x73;
constexpr std::uint8_t kMovdqu = 0x6F;
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));
@ -32,6 +37,15 @@ void _emit(std::vector<std::uint8_t>& out, const std::uint8_t prefix, const std:
out.push_back(modrm);
}
std::size_t _displacementSize(const std::uint8_t mod, const std::uint8_t rm, const std::uint8_t sib) {
using namespace X64OpcodeConstants;
if (mod == ModRmModDisp8)
return Disp8Size;
if (mod == ModRmModDisp32 || (mod == ModRmModIndirect && rm == ModRmRmSibPresent && (sib & SibBaseMask) == SibBaseDisp32))
return Disp32Size;
return 0;
}
void _shiftImm(std::vector<std::uint8_t>& out, const std::uint8_t opcode, const std::uint8_t extension, const std::uint8_t reg, const std::uint8_t imm) {
_emit(out, kPrefixPacked, 0, reg, {0x0F, opcode}, static_cast<std::uint8_t>(kModRmRegister | (extension << 3) | (reg & 7)));
out.push_back(imm);
@ -39,6 +53,33 @@ void _shiftImm(std::vector<std::uint8_t>& out, const std::uint8_t opcode, const
}
MemoryOperand DecodeMemoryOperand(const std::uint8_t* data, const std::size_t length, const std::size_t modRmOffset, const std::uint8_t rex, std::vector<std::uint8_t> prefixes) {
using namespace X64OpcodeConstants;
if (modRmOffset >= length)
throw CodegenException("Memory operand truncated before its ModRM byte");
const auto modrm = data[modRmOffset];
MemoryOperand operand{std::move(prefixes), static_cast<std::uint8_t>(rex & (kRexX | kRexB)), static_cast<std::uint8_t>((modrm >> ModRmModShift) & ModRmModMask), static_cast<std::uint8_t>(modrm & ModRmRmMask), 0, 0, false};
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");
auto pos = modRmOffset + 1;
if (operand.Rm == ModRmRmSibPresent) {
if (pos >= length)
throw CodegenException("Memory operand truncated before its SIB byte");
operand.Sib = data[pos++];
operand.StackBase = (operand.Sib & SibBaseMask) == ModRmRmSibPresent && (rex & kRexB) == 0;
}
const auto size = _displacementSize(operand.Mod, operand.Rm, operand.Sib);
if (pos + size > length)
throw CodegenException("Memory operand truncated in its displacement");
if (size == Disp8Size)
operand.Displacement = static_cast<std::int8_t>(data[pos]);
for (std::size_t index = 0; size == Disp32Size && index < size; ++index)
operand.Displacement = static_cast<std::int32_t>(static_cast<std::uint32_t>(operand.Displacement) | (static_cast<std::uint32_t>(data[pos + index]) << (index * 8)));
return operand;
}
void EmitSse(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)));
}
@ -79,7 +120,27 @@ void StubBodyBuilder::RipOperand(const std::initializer_list<std::uint8_t> opcod
_constants.push_back(constant);
}
void StubBodyBuilder::Load(const std::uint8_t reg, const MemoryOperand& operand) {
using namespace X64OpcodeConstants;
const auto displacement = static_cast<std::int64_t>(operand.Displacement) + static_cast<std::int64_t>(operand.StackBase ? _stackDepth : 0);
if (displacement > std::numeric_limits<std::int32_t>::max())
throw CodegenException("Stack displacement does not fit after spilling");
const auto mod = operand.StackBase && _stackDepth != 0 ? ModRmModDisp32 : operand.Mod;
_bytes.insert(_bytes.end(), operand.Prefixes.begin(), operand.Prefixes.end());
_bytes.push_back(kPrefixScalar);
const auto rex = static_cast<std::uint8_t>(kRexBase | ((reg & 8) != 0 ? kRexR : 0) | operand.RexIndexBase);
if (rex != kRexBase)
_bytes.push_back(rex);
_bytes.insert(_bytes.end(), {TwoByteOpcodeEscape, kMovdqu, static_cast<std::uint8_t>((mod << ModRmModShift) | ((reg & 7) << ModRmRegShift) | operand.Rm)});
if (operand.Rm == ModRmRmSibPresent)
_bytes.push_back(operand.Sib);
const auto value = static_cast<std::uint32_t>(static_cast<std::int32_t>(displacement));
for (std::size_t index = 0; index < _displacementSize(mod, operand.Rm, operand.Sib); ++index)
_bytes.push_back(static_cast<std::uint8_t>(value >> (index * 8)));
}
void StubBodyBuilder::Spill(const std::uint8_t reg) {
_stackDepth += kRedZoneSpillFrame;
_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);
@ -89,6 +150,7 @@ void StubBodyBuilder::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);
_stackDepth -= kRedZoneSpillFrame;
}
void StubBodyBuilder::Raw(std::span<const std::uint8_t> bytes) {

View File

@ -193,7 +193,16 @@ void sha256Operands() {
check({0x0F, 0x38, 0xCB, 0xCA}, Codegen::Sha256Operation::Rnds2, 1, 2);
check({0x45, 0x0F, 0x38, 0xCC, 0xE1}, Codegen::Sha256Operation::Msg1, 12, 9);
check({0x44, 0x0F, 0x38, 0xCD, 0xC0}, Codegen::Sha256Operation::Msg2, 8, 0);
requireFailure([] { const Bytes bytes = {0x0F, 0x38, 0xCC, 0x08}; (void)Codegen::DecodeSha256(bytes.data(), bytes.size()); }, "SHA-256 memory operand was accepted");
const Bytes memory = {0x65, 0x67, 0x44, 0x0F, 0x38, 0xCD, 0x54, 0x8C, 0xF0};
const auto decoded = Codegen::DecodeSha256(memory.data(), memory.size());
require(decoded.Operation == Codegen::Sha256Operation::Msg2 && decoded.Destination == 10 && decoded.Memory && decoded.Memory->Prefixes == Bytes{0x65, 0x67} && decoded.Memory->Mod == 1 && decoded.Memory->Rm == 4 && decoded.Memory->Sib == 0x8C && decoded.Memory->Displacement == -16 && decoded.Memory->StackBase, "SHA-256 memory operand was decoded incorrectly");
const Bytes r12Base = {0x41, 0x0F, 0x38, 0xCC, 0x14, 0x24};
require(!Codegen::DecodeSha256(r12Base.data(), r12Base.size()).Memory->StackBase, "R12 base was decoded as RSP");
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");
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");
check({0x41, 0x2E, 0x0F, 0x38, 0xCC, 0xCA}, Codegen::Sha256Operation::Msg1, 1, 2);
@ -425,7 +434,12 @@ void converterSha256() {
requireFailure([&] { (void)converter->Convert(beforeReturn, {segmentHeader(text.size())}); }, "Short SHA-256 instruction followed by a return was relocated");
auto memoryForm = file;
memoryForm[0x20F] = 0x28;
require(failureOffset([&] { (void)converter->Convert(memoryForm, {segmentHeader(text.size())}); }, "SHA-256 memory form was accepted") == 0x20C, "SHA-256 operand failure does not carry the file offset");
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};
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");
}
void converterSha1() {
@ -748,20 +762,29 @@ std::array<std::uint64_t, 2> runRegisterFormStub(const Bytes& site, const std::u
alignas(16) std::uint64_t out[2] = {};
alignas(16) std::uint64_t scratchIn[2] = {kStubScratch[0], kStubScratch[1]};
alignas(16) std::uint64_t scratchOut[2] = {};
alignas(16) std::uint64_t spareIn[2] = {kStubScratch[1], kStubScratch[0]};
alignas(16) std::uint64_t spareOut[2] = {};
asm volatile(
"movdqu (%[scratch]), %%xmm0\n\t"
"movdqu (%[spare]), %%xmm1\n\t"
"movdqu (%[dst]), %%xmm2\n\t"
"movdqu (%[ctl]), %%xmm5\n\t"
"mov %[ctl], %%rcx\n\t"
"mov %[ctl], %%r12\n\t"
"mov $16, %%eax\n\t"
"sub $128, %%rsp\n\t"
"movdqu %%xmm5, 0x10(%%rsp)\n\t"
"call *%[code]\n\t"
"add $128, %%rsp\n\t"
"movdqu %%xmm2, (%[out])\n\t"
"movdqu %%xmm0, (%[scratchOut])\n\t"
"movdqu %%xmm1, (%[spareOut])\n\t"
:
: [scratch] "r"(scratchIn), [dst] "r"(destinationIn), [ctl] "r"(sourceIn), [code] "r"(code), [out] "r"(out), [scratchOut] "r"(scratchOut)
: "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "memory", "cc");
: [scratch] "r"(scratchIn), [spare] "r"(spareIn), [dst] "r"(destinationIn), [ctl] "r"(sourceIn), [code] "r"(code), [out] "r"(out), [scratchOut] "r"(scratchOut), [spareOut] "r"(spareOut)
: "rax", "rcx", "r12", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "memory", "cc");
munmap(code, 4096);
require(scratchOut[0] == scratchIn[0] && scratchOut[1] == scratchIn[1], "Register form stub clobbered a scratch register");
require(spareOut[0] == spareIn[0] && spareOut[1] == spareIn[1], "Stub clobbered xmm1");
return {out[0], out[1]};
}
@ -851,6 +874,8 @@ void sha256Execution() {
const Bytes same = {0x0F, 0x38, opcode, 0xD2};
require(runRegisterFormStub(distinct, state, words) == sha256Reference(opcode, state, words, kStubScratch), "SHA-256 stub computed the wrong result");
require(runRegisterFormStub(same, state, state) == sha256Reference(opcode, state, state, kStubScratch), "SHA-256 stub with equal operands computed the wrong result");
for (const auto& memory : {Bytes{0x0F, 0x38, opcode, 0x11}, Bytes{0x0F, 0x38, opcode, 0x54, 0x24, 0x18}, Bytes{0x0F, 0x38, opcode, 0x54, 0x04, 0x08}, Bytes{0x2E, 0x41, 0x0F, 0x38, opcode, 0x14, 0x24}, Bytes{0x0F, 0x38, opcode, 0x91, 0x00, 0x00, 0x00, 0x00}})
require(runRegisterFormStub(memory, state, words) == sha256Reference(opcode, state, words, kStubScratch), "SHA-256 memory form stub computed the wrong result");
}
}