mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
Merge pull request #8142 from Kernel-Hunter/fix/rosetta-masked-stores
fix(relinker): probe masked store elements before the store under --to-rosetta
This commit is contained in:
7 files changed
+523
No files matched your search
@@ -52,6 +52,7 @@ add_library(relinker_core STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/MaskedStoreLowering.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/AndnLowering.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/SyscallScanner.cpp
|
||||
@@ -98,6 +99,11 @@ if(BUILD_TESTING)
|
||||
add_test(NAME andn_lowering COMMAND andn_lowering_tests)
|
||||
set_tests_properties(andn_lowering PROPERTIES TIMEOUT 20)
|
||||
|
||||
add_test_executable(masked_store_lowering_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/MaskedStoreLoweringTests.cpp)
|
||||
target_link_libraries(masked_store_lowering_tests PRIVATE relinker_core)
|
||||
add_test(NAME masked_store_lowering COMMAND masked_store_lowering_tests)
|
||||
set_tests_properties(masked_store_lowering PROPERTIES TIMEOUT 20)
|
||||
|
||||
add_test_executable(relinker_conversion_tests ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ConversionTests.cpp)
|
||||
target_link_libraries(relinker_conversion_tests PRIVATE relinker_core)
|
||||
add_test(NAME relinker_conversion COMMAND relinker_conversion_tests)
|
||||
@@ -203,6 +209,7 @@ if(WIN32)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/MaskedStoreLowering.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/AndnLowering.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)
|
||||
@@ -432,6 +439,7 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreOperands.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/WideStoreLowering.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/ScalarCompareOperands.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/MaskedStoreLowering.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/AndnLowering.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/linux/LinuxElfPatcher.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SegmentFilter.cpp
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef CODEGEN_X86_MASKEDSTORELOWERING_HPP
|
||||
#define CODEGEN_X86_MASKEDSTORELOWERING_HPP
|
||||
|
||||
#include <codegen/x86/StubBodyBuilder.hpp>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace Codegen {
|
||||
|
||||
struct MaskedStoreOperands {
|
||||
const char* Name;
|
||||
std::uint8_t ElementSize;
|
||||
bool Wide;
|
||||
std::uint8_t Mask;
|
||||
MemoryOperand Memory;
|
||||
std::vector<std::uint8_t> Instruction;
|
||||
std::size_t DisplacementOffset;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::optional<MaskedStoreOperands> DecodeMaskedStore(const std::uint8_t* data, std::size_t length);
|
||||
void EmitMaskedStore(StubBodyBuilder& body, const MaskedStoreOperands& operands);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <codegen/x86/ScalarCompareOperands.hpp>
|
||||
#include <codegen/x86/WideStoreLowering.hpp>
|
||||
#include <codegen/x86/WideStoreOperands.hpp>
|
||||
#include <codegen/x86/MaskedStoreLowering.hpp>
|
||||
#include <codegen/x86/AndnLowering.hpp>
|
||||
#include <codegen/x86/X64InstructionDecoder.hpp>
|
||||
#include <codegen/x86/X64OpcodeConstants.hpp>
|
||||
@@ -267,6 +268,8 @@ std::optional<Amd64OnlyMatch> Amd64OnlyInstructionMatcher::_matchRosetta(const D
|
||||
return _trampoline(kWideStore.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _wideStoreLowering.EmitOutOfLine(body, *wide); }), false, true);
|
||||
if (const auto compare = DecodeScalarCompare(instr.Data, instr.Length))
|
||||
return _trampoline(kScalarCompare.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _emitScalarCompare(body, instr, *compare); }), true);
|
||||
if (const auto masked = DecodeMaskedStore(instr.Data, instr.Length))
|
||||
return _trampoline(masked->Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { EmitMaskedStore(body, *masked); }));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -326,6 +329,10 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c
|
||||
_emitScalarCompare(body, instr, *compare);
|
||||
return kScalarCompare.Name;
|
||||
}
|
||||
if (const auto masked = _target == Amd64OnlyTarget::Rosetta ? DecodeMaskedStore(instr.Data, instr.Length) : std::nullopt) {
|
||||
EmitMaskedStore(body, *masked);
|
||||
return masked->Name;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#include <codegen/x86/MaskedStoreLowering.hpp>
|
||||
#include <codegen/CodegenException.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <limits>
|
||||
|
||||
namespace Codegen {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::int32_t kFrameSize = 128 + 3 * 8;
|
||||
constexpr std::uint8_t kAddress = 0;
|
||||
constexpr std::uint8_t kVexMap0F38 = 0x02;
|
||||
constexpr std::uint8_t kVexPp66 = 0x01;
|
||||
|
||||
bool _prefix(std::uint8_t value) {
|
||||
return value == 0x2E || value == 0x36 || value == 0x3E || value == 0x26 || value == 0x64 || value == 0x65 || value == 0x67;
|
||||
}
|
||||
|
||||
void _append32(std::vector<std::uint8_t>& bytes, std::int32_t value) {
|
||||
for (unsigned shift = 0; shift < 32; shift += 8)
|
||||
bytes.push_back(static_cast<std::uint8_t>(static_cast<std::uint32_t>(value) >> shift));
|
||||
}
|
||||
|
||||
void _address(StubBodyBuilder& body, const MemoryOperand& memory) {
|
||||
auto displacement = static_cast<std::int64_t>(memory.Displacement);
|
||||
if (memory.StackBase) displacement += kFrameSize;
|
||||
const bool address32 = std::find(memory.Prefixes.begin(), memory.Prefixes.end(), 0x67) != memory.Prefixes.end();
|
||||
if (!address32 && displacement > std::numeric_limits<std::int32_t>::max())
|
||||
throw CodegenException("Masked store stack displacement does not fit after spilling");
|
||||
const auto mod = memory.StackBase ? 2 : memory.Mod;
|
||||
auto bytes = memory.Prefixes;
|
||||
bytes.push_back(static_cast<std::uint8_t>(0x48 | memory.RexIndexBase));
|
||||
bytes.push_back(0x8D);
|
||||
bytes.push_back(static_cast<std::uint8_t>((mod << 6) | (kAddress << 3) | memory.Rm));
|
||||
if (memory.Rm == 4) bytes.push_back(memory.Sib);
|
||||
const auto offset = bytes.size();
|
||||
if (mod == 1) bytes.push_back(static_cast<std::uint8_t>(displacement));
|
||||
if (mod == 2 || (mod == 0 && (memory.RipRelative || (memory.Rm == 4 && (memory.Sib & 7) == 5))))
|
||||
_append32(bytes, static_cast<std::int32_t>(displacement));
|
||||
if (memory.RipRelative)
|
||||
body.RipRelative(bytes, offset, static_cast<std::int64_t>(memory.InstructionLength) + memory.Displacement);
|
||||
else
|
||||
body.Raw(bytes);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::optional<MaskedStoreOperands> DecodeMaskedStore(const std::uint8_t* data, const std::size_t length) {
|
||||
std::size_t vex = 0;
|
||||
while (vex < length && _prefix(data[vex])) ++vex;
|
||||
if (vex + 5 > length || data[vex] != 0xC4 || (data[vex + 1] & 0x1F) != kVexMap0F38 || (data[vex + 2] & 3) != kVexPp66 || (data[vex + 4] >> 6) == 3)
|
||||
return std::nullopt;
|
||||
const bool w = (data[vex + 2] & 0x80) != 0;
|
||||
const char* name = nullptr;
|
||||
std::uint8_t elementSize = 4;
|
||||
if (data[vex + 3] == 0x2E && !w) {
|
||||
name = "VMASKMOVPS";
|
||||
} else if (data[vex + 3] == 0x2F && !w) {
|
||||
name = "VMASKMOVPD";
|
||||
elementSize = 8;
|
||||
} else if (data[vex + 3] == 0x8E) {
|
||||
name = w ? "VPMASKMOVQ" : "VPMASKMOVD";
|
||||
elementSize = w ? 8 : 4;
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto extensions = static_cast<std::uint8_t>(~data[vex + 1]);
|
||||
const auto rex = static_cast<std::uint8_t>(((extensions & 0x20) >> 5) | ((extensions & 0x40) >> 5));
|
||||
auto memory = DecodeMemoryOperand(data, length, vex + 4, rex, {data, data + vex});
|
||||
if (vex + 4 + memory.EncodedSize != length)
|
||||
throw CodegenException("Masked store memory operand has trailing bytes");
|
||||
const auto displacementOffset = vex + 5 + (memory.Rm == 4 ? 1 : 0);
|
||||
return MaskedStoreOperands{name, elementSize, (data[vex + 2] & 4) != 0, static_cast<std::uint8_t>((~data[vex + 2] >> 3) & 15),
|
||||
std::move(memory), std::vector<std::uint8_t>(data, data + length), displacementOffset};
|
||||
}
|
||||
|
||||
void EmitMaskedStore(StubBodyBuilder& body, const MaskedStoreOperands& operands) {
|
||||
const auto& memory = operands.Memory;
|
||||
body.Raw(std::array<std::uint8_t, 8>{0x48, 0x8D, 0x64, 0x24, 0x80, 0x9C, 0x50, 0x51});
|
||||
_address(body, memory);
|
||||
body.Raw(std::array<std::uint8_t, 5>{0xC4, static_cast<std::uint8_t>(operands.Mask >= 8 ? 0xC1 : 0xE1),
|
||||
static_cast<std::uint8_t>(0x78 | (operands.Wide ? 4 : 0) | (operands.ElementSize == 8 ? 1 : 0)), 0x50,
|
||||
static_cast<std::uint8_t>(0xC8 | (operands.Mask & 7))});
|
||||
const auto elements = (operands.Wide ? 32u : 16u) / operands.ElementSize;
|
||||
for (unsigned element = 0; element < elements; ++element) {
|
||||
std::vector<std::uint8_t> probes;
|
||||
for (const auto offset : {element * operands.ElementSize, element * operands.ElementSize + operands.ElementSize - 1u}) {
|
||||
probes.insert(probes.end(), memory.Prefixes.begin(), memory.Prefixes.end());
|
||||
probes.insert(probes.end(), {0xF0, 0x80, 0x48, static_cast<std::uint8_t>(offset), 0x00});
|
||||
}
|
||||
body.Raw(std::array<std::uint8_t, 5>{0xF6, 0xC1, static_cast<std::uint8_t>(1u << element), 0x74, static_cast<std::uint8_t>(probes.size())});
|
||||
body.Raw(probes);
|
||||
}
|
||||
body.Raw(std::array<std::uint8_t, 11>{0x59, 0x58, 0x9D, 0x48, 0x8D, 0xA4, 0x24, 0x80, 0x00, 0x00, 0x00});
|
||||
if (memory.RipRelative)
|
||||
body.RipRelative(operands.Instruction, operands.DisplacementOffset, static_cast<std::int64_t>(memory.InstructionLength) + memory.Displacement);
|
||||
else
|
||||
body.Raw(operands.Instruction);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
#include <codegen/x86/MaskedStoreLowering.hpp>
|
||||
#include <codegen/x86/IAmd64OnlyInstructionMatcher.hpp>
|
||||
#include <codegen/CodegenException.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#if (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__)
|
||||
#include <signal.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using Bytes = std::vector<std::uint8_t>;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
struct Form {
|
||||
std::uint8_t Opcode;
|
||||
bool W;
|
||||
const char* Name;
|
||||
std::uint8_t ElementSize;
|
||||
};
|
||||
|
||||
constexpr std::array kForms{
|
||||
Form{0x2E, false, "VMASKMOVPS", 4},
|
||||
Form{0x2F, false, "VMASKMOVPD", 8},
|
||||
Form{0x8E, false, "VPMASKMOVD", 4},
|
||||
Form{0x8E, true, "VPMASKMOVQ", 8},
|
||||
};
|
||||
|
||||
Bytes maskedStore(const Form& form, bool wide, std::uint8_t mask, std::uint8_t source, std::uint8_t rexIndexBase, Bytes memory, const Bytes& prefixes = {}) {
|
||||
Bytes bytes = prefixes;
|
||||
bytes.insert(bytes.end(), {0xC4,
|
||||
static_cast<std::uint8_t>(((source & 8) != 0 ? 0 : 0x80) | ((rexIndexBase & 2) != 0 ? 0 : 0x40) | ((rexIndexBase & 1) != 0 ? 0 : 0x20) | 0x02),
|
||||
static_cast<std::uint8_t>((form.W ? 0x80 : 0) | ((~mask & 15) << 3) | (wide ? 4 : 0) | 0x01), form.Opcode});
|
||||
memory.at(0) = static_cast<std::uint8_t>(memory[0] | ((source & 7) << 3));
|
||||
bytes.insert(bytes.end(), memory.begin(), memory.end());
|
||||
return bytes;
|
||||
}
|
||||
|
||||
bool rejected(const Bytes& bytes) {
|
||||
try {
|
||||
(void)Codegen::DecodeMaskedStore(bytes.data(), bytes.size());
|
||||
} catch (const Codegen::CodegenException&) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void decoding() {
|
||||
for (const auto& form : kForms) {
|
||||
for (const bool wide : {false, true}) {
|
||||
for (std::uint8_t mask = 0; mask < 16; ++mask) {
|
||||
for (std::uint8_t source = 0; source < 16; ++source) {
|
||||
const auto bytes = maskedStore(form, wide, mask, source, 0, {0x07});
|
||||
const auto operands = Codegen::DecodeMaskedStore(bytes.data(), bytes.size());
|
||||
require(operands && std::string(operands->Name) == form.Name && operands->ElementSize == form.ElementSize && operands->Wide == wide && operands->Mask == mask, "Masked store decoding changed the form, width or mask register");
|
||||
require(operands->Memory.Rm == 7 && operands->Memory.Mod == 0 && operands->Memory.RexIndexBase == 0 && operands->Instruction == bytes, "Masked store decoding changed the memory operand");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto extended = maskedStore(kForms[0], true, 3, 4, 3, {0x84, 0xD1, 0x78, 0x56, 0x34, 0x12}, {0x64, 0x67});
|
||||
const auto operands = Codegen::DecodeMaskedStore(extended.data(), extended.size());
|
||||
require(operands && operands->Memory.RexIndexBase == 3 && operands->Memory.Sib == 0xD1 && operands->Memory.Displacement == 0x12345678 && operands->Memory.Prefixes == Bytes{0x64, 0x67}, "Masked store extensions, SIB or prefixes were lost");
|
||||
const auto rip = maskedStore(kForms[1], false, 1, 2, 0, {0x05, 0x10, 0x00, 0x00, 0x00});
|
||||
const auto ripOperands = Codegen::DecodeMaskedStore(rip.data(), rip.size());
|
||||
require(ripOperands && ripOperands->Memory.RipRelative && ripOperands->DisplacementOffset == 5 && ripOperands->Memory.Displacement == 0x10, "RIP-relative masked store was not recognised");
|
||||
const auto stack = maskedStore(kForms[2], false, 1, 2, 0, {0x44, 0x24, 0x08});
|
||||
const auto stackOperands = Codegen::DecodeMaskedStore(stack.data(), stack.size());
|
||||
require(stackOperands && stackOperands->Memory.StackBase && stackOperands->DisplacementOffset == 6, "RSP-based masked store was not recognised");
|
||||
for (const auto& bytes : {
|
||||
Bytes{0xC4, 0xE2, 0x75, 0x2C, 0x07}, Bytes{0xC4, 0xE2, 0x75, 0x2D, 0x07}, Bytes{0xC4, 0xE2, 0x75, 0x8C, 0x07},
|
||||
Bytes{0xC4, 0xE2, 0xF5, 0x2E, 0x07}, Bytes{0xC4, 0xE2, 0xF5, 0x2F, 0x07}, Bytes{0xC4, 0xE2, 0x75, 0x2E, 0xC7},
|
||||
Bytes{0xC4, 0xE1, 0x75, 0x2E, 0x07}, Bytes{0xC4, 0xE2, 0x74, 0x2E, 0x07}, Bytes{0xC5, 0xF5, 0x2E, 0x07},
|
||||
Bytes{0xC4, 0xE2, 0x75, 0x2E}, Bytes{0x66, 0xC4, 0xE2, 0x75, 0x2E, 0x07}})
|
||||
require(!Codegen::DecodeMaskedStore(bytes.data(), bytes.size()), "A load, register form, other map or prefix matched a masked store");
|
||||
require(rejected(Bytes{0xC4, 0xE2, 0x75, 0x2E, 0x07, 0x00}), "A masked store with trailing bytes was accepted");
|
||||
require(rejected(Bytes{0xC4, 0xE2, 0x75, 0x2E, 0x47}), "A truncated masked store was accepted");
|
||||
}
|
||||
|
||||
void body() {
|
||||
const auto site = maskedStore(kForms[1], false, 1, 2, 0, {0x07});
|
||||
Codegen::StubBodyBuilder builder;
|
||||
Codegen::EmitMaskedStore(builder, *Codegen::DecodeMaskedStore(site.data(), site.size()));
|
||||
const auto lowered = builder.Finish();
|
||||
const Bytes expected{
|
||||
0x48, 0x8D, 0x64, 0x24, 0x80, 0x9C, 0x50, 0x51, 0x48, 0x8D, 0x07, 0xC4, 0xE1, 0x79, 0x50, 0xC9,
|
||||
0xF6, 0xC1, 0x01, 0x74, 0x0A, 0xF0, 0x80, 0x48, 0x00, 0x00, 0xF0, 0x80, 0x48, 0x07, 0x00,
|
||||
0xF6, 0xC1, 0x02, 0x74, 0x0A, 0xF0, 0x80, 0x48, 0x08, 0x00, 0xF0, 0x80, 0x48, 0x0F, 0x00,
|
||||
0x59, 0x58, 0x9D, 0x48, 0x8D, 0xA4, 0x24, 0x80, 0x00, 0x00, 0x00, 0xC4, 0xE2, 0x71, 0x2F, 0x17};
|
||||
require(Bytes(lowered.Bytes.begin(), lowered.Bytes.begin() + static_cast<std::ptrdiff_t>(lowered.ReturnBranchOffset)) == expected, "VMASKMOVPD stub does not probe its elements before the store");
|
||||
const auto segment = maskedStore(kForms[0], true, 9, 2, 0, {0x44, 0x24, 0x08}, {0x64});
|
||||
Codegen::StubBodyBuilder segmentBuilder;
|
||||
Codegen::EmitMaskedStore(segmentBuilder, *Codegen::DecodeMaskedStore(segment.data(), segment.size()));
|
||||
const auto segmentBody = segmentBuilder.Finish().Bytes;
|
||||
const Bytes lea{0x64, 0x48, 0x8D, 0x84, 0x24, 0xA0, 0x00, 0x00, 0x00, 0xC4, 0xC1, 0x7C, 0x50, 0xC9};
|
||||
require(std::search(segmentBody.begin(), segmentBody.end(), lea.begin(), lea.end()) == segmentBody.begin() + 8, "RSP-based masked store address does not skip the spill frame");
|
||||
const Bytes probe{0x64, 0xF0, 0x80, 0x48, 0x1F, 0x00};
|
||||
require(std::search(segmentBody.begin(), segmentBody.end(), probe.begin(), probe.end()) != segmentBody.end(), "Masked store probes lost the segment prefix");
|
||||
const auto overflow = maskedStore(kForms[0], false, 1, 2, 0, {0x84, 0x24, 0xFF, 0xFF, 0xFF, 0x7F});
|
||||
bool thrown = false;
|
||||
try {
|
||||
Codegen::StubBodyBuilder overflowBuilder;
|
||||
Codegen::EmitMaskedStore(overflowBuilder, *Codegen::DecodeMaskedStore(overflow.data(), overflow.size()));
|
||||
} catch (const Codegen::CodegenException&) {
|
||||
thrown = true;
|
||||
}
|
||||
require(thrown, "An overflowing stack displacement was silently wrapped");
|
||||
}
|
||||
|
||||
void matching() {
|
||||
const auto intel = Codegen::MakeAmd64OnlyInstructionMatcher();
|
||||
const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta);
|
||||
for (const auto& form : kForms) {
|
||||
const auto bytes = maskedStore(form, true, 1, 0, 0, {0x00});
|
||||
require(!intel->Match(bytes.data(), bytes.size()), "Intel mode lowered a masked store");
|
||||
const auto match = rosetta->Match(bytes.data(), bytes.size());
|
||||
require(match && match->InstructionName == form.Name && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline && !match->Optional, "Rosetta mode did not require a masked store trampoline");
|
||||
const std::array<std::span<const std::uint8_t>, 2> sequence{bytes, bytes};
|
||||
require(rosetta->MatchSequence(sequence, {}).has_value(), "A masked store sequence could not move out of line");
|
||||
}
|
||||
const Bytes load{0xC4, 0xE2, 0x75, 0x2C, 0x00};
|
||||
require(!rosetta->Match(load.data(), load.size()), "Rosetta mode lowered a masked load");
|
||||
}
|
||||
|
||||
#if (defined(__linux__) || defined(__APPLE__)) && defined(__x86_64__)
|
||||
|
||||
constexpr std::size_t kPage = 4096;
|
||||
constexpr std::size_t kDataPages = 16;
|
||||
constexpr std::uint8_t kFill = 0xA5;
|
||||
constexpr std::uint8_t kSentinel = 0x5A;
|
||||
|
||||
std::uint8_t* protectedPage = nullptr;
|
||||
volatile sig_atomic_t faults = 0;
|
||||
|
||||
void onFault(int number, siginfo_t* info, void*) {
|
||||
const auto* address = static_cast<const std::uint8_t*>(info->si_addr);
|
||||
if (address < protectedPage || address >= protectedPage + kPage) {
|
||||
signal(number, SIG_DFL);
|
||||
return;
|
||||
}
|
||||
faults = faults + 1;
|
||||
mprotect(protectedPage, kPage, PROT_READ | PROT_WRITE);
|
||||
}
|
||||
|
||||
struct alignas(32) Machine {
|
||||
std::uint8_t Ymm[16][32];
|
||||
std::uint64_t Gpr[7];
|
||||
std::uint64_t Flags;
|
||||
std::uint64_t Stack;
|
||||
std::uint64_t Code;
|
||||
};
|
||||
|
||||
static_assert(offsetof(Machine, Gpr) == 512 && offsetof(Machine, Flags) == 568 && offsetof(Machine, Stack) == 576 && offsetof(Machine, Code) == 584);
|
||||
|
||||
void run(Machine& machine) {
|
||||
asm volatile(
|
||||
"mov %%rsp, %%r12\n\t"
|
||||
"mov %[machine], %%r13\n\t"
|
||||
"vmovdqu 0(%%r13), %%ymm0\n\t"
|
||||
"vmovdqu 32(%%r13), %%ymm1\n\t"
|
||||
"vmovdqu 64(%%r13), %%ymm2\n\t"
|
||||
"vmovdqu 96(%%r13), %%ymm3\n\t"
|
||||
"vmovdqu 128(%%r13), %%ymm4\n\t"
|
||||
"vmovdqu 160(%%r13), %%ymm5\n\t"
|
||||
"vmovdqu 192(%%r13), %%ymm6\n\t"
|
||||
"vmovdqu 224(%%r13), %%ymm7\n\t"
|
||||
"vmovdqu 256(%%r13), %%ymm8\n\t"
|
||||
"vmovdqu 288(%%r13), %%ymm9\n\t"
|
||||
"vmovdqu 320(%%r13), %%ymm10\n\t"
|
||||
"vmovdqu 352(%%r13), %%ymm11\n\t"
|
||||
"vmovdqu 384(%%r13), %%ymm12\n\t"
|
||||
"vmovdqu 416(%%r13), %%ymm13\n\t"
|
||||
"vmovdqu 448(%%r13), %%ymm14\n\t"
|
||||
"vmovdqu 480(%%r13), %%ymm15\n\t"
|
||||
"mov 512(%%r13), %%rax\n\t"
|
||||
"mov 520(%%r13), %%rcx\n\t"
|
||||
"mov 528(%%r13), %%rbx\n\t"
|
||||
"mov 536(%%r13), %%rsi\n\t"
|
||||
"mov 544(%%r13), %%rdi\n\t"
|
||||
"mov 552(%%r13), %%r9\n\t"
|
||||
"mov 560(%%r13), %%r10\n\t"
|
||||
"mov 576(%%r13), %%rsp\n\t"
|
||||
"pushq 568(%%r13)\n\t"
|
||||
"popfq\n\t"
|
||||
"call *584(%%r13)\n\t"
|
||||
"pushfq\n\t"
|
||||
"popq 568(%%r13)\n\t"
|
||||
"mov %%rsp, 576(%%r13)\n\t"
|
||||
"mov %%r12, %%rsp\n\t"
|
||||
"mov %%rax, 512(%%r13)\n\t"
|
||||
"mov %%rcx, 520(%%r13)\n\t"
|
||||
"mov %%rbx, 528(%%r13)\n\t"
|
||||
"mov %%rsi, 536(%%r13)\n\t"
|
||||
"mov %%rdi, 544(%%r13)\n\t"
|
||||
"mov %%r9, 552(%%r13)\n\t"
|
||||
"mov %%r10, 560(%%r13)\n\t"
|
||||
"vmovdqu %%ymm0, 0(%%r13)\n\t"
|
||||
"vmovdqu %%ymm1, 32(%%r13)\n\t"
|
||||
"vmovdqu %%ymm2, 64(%%r13)\n\t"
|
||||
"vmovdqu %%ymm3, 96(%%r13)\n\t"
|
||||
"vmovdqu %%ymm4, 128(%%r13)\n\t"
|
||||
"vmovdqu %%ymm5, 160(%%r13)\n\t"
|
||||
"vmovdqu %%ymm6, 192(%%r13)\n\t"
|
||||
"vmovdqu %%ymm7, 224(%%r13)\n\t"
|
||||
"vmovdqu %%ymm8, 256(%%r13)\n\t"
|
||||
"vmovdqu %%ymm9, 288(%%r13)\n\t"
|
||||
"vmovdqu %%ymm10, 320(%%r13)\n\t"
|
||||
"vmovdqu %%ymm11, 352(%%r13)\n\t"
|
||||
"vmovdqu %%ymm12, 384(%%r13)\n\t"
|
||||
"vmovdqu %%ymm13, 416(%%r13)\n\t"
|
||||
"vmovdqu %%ymm14, 448(%%r13)\n\t"
|
||||
"vmovdqu %%ymm15, 480(%%r13)\n\t"
|
||||
"vzeroupper\n\t"
|
||||
:
|
||||
: [machine] "r"(&machine)
|
||||
: "rax", "rcx", "rbx", "rsi", "rdi", "r9", "r10", "r12", "r13",
|
||||
"xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7",
|
||||
"xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15", "memory", "cc");
|
||||
}
|
||||
|
||||
struct Addressing {
|
||||
Bytes Memory;
|
||||
std::uint8_t RexIndexBase;
|
||||
std::uint8_t Mask;
|
||||
std::uint8_t Source;
|
||||
};
|
||||
|
||||
enum Register { Rax, Rcx, Rbx, Rsi, Rdi, R9, R10 };
|
||||
|
||||
const std::array kAddressings{
|
||||
Addressing{{0x07}, 0, 1, 2},
|
||||
Addressing{{0x44, 0x5E, 0x7F}, 0, 9, 14},
|
||||
Addressing{{0x84, 0x88, 0x00, 0xF0, 0xFF, 0xFF}, 0, 15, 0},
|
||||
Addressing{{0x04, 0xD1}, 3, 3, 8},
|
||||
Addressing{{0x44, 0x24, 0x40}, 0, 12, 5},
|
||||
Addressing{{0x05, 0x00, 0x00, 0x00, 0x00}, 0, 6, 11},
|
||||
};
|
||||
|
||||
void execution() {
|
||||
const auto region = static_cast<std::uint8_t*>(mmap(nullptr, (kDataPages + 2) * kPage, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
|
||||
require(region != MAP_FAILED, "Cannot map masked store test pages");
|
||||
auto* const code = region;
|
||||
auto* const data = region + kPage;
|
||||
protectedPage = data + kDataPages * kPage;
|
||||
struct sigaction action {};
|
||||
action.sa_sigaction = onFault;
|
||||
action.sa_flags = SA_SIGINFO;
|
||||
sigemptyset(&action.sa_mask);
|
||||
struct sigaction previousSegv {};
|
||||
struct sigaction previousBus {};
|
||||
sigaction(SIGSEGV, &action, &previousSegv);
|
||||
sigaction(SIGBUS, &action, &previousBus);
|
||||
const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta);
|
||||
const std::array<std::uint8_t, 6> patterns{0xFF, 0x00, 0x01, 0x80, 0x55, 0xFE};
|
||||
std::uint64_t runs = 0;
|
||||
for (const auto& form : kForms) {
|
||||
for (const bool wide : {false, true}) {
|
||||
const auto elements = (wide ? 32u : 16u) / form.ElementSize;
|
||||
for (std::size_t mode = 0; mode < kAddressings.size(); ++mode) {
|
||||
const auto& addressing = kAddressings[mode];
|
||||
const auto site = maskedStore(form, wide, addressing.Mask, addressing.Source, addressing.RexIndexBase, addressing.Memory);
|
||||
const auto match = matcher->Match(site.data(), site.size());
|
||||
require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "Masked store stub was not produced");
|
||||
for (const int offset : {-40, -16, -4, -2, 0, 8}) {
|
||||
auto* const target = protectedPage + offset;
|
||||
const bool stackBased = mode == 4;
|
||||
auto* const stack = stackBased ? target + 8 - 0x40 : data + kDataPages / 2 * kPage;
|
||||
auto body = match->StubBody;
|
||||
const auto siteAddress = mode == 5 ? reinterpret_cast<std::uint64_t>(target) - site.size() : reinterpret_cast<std::uint64_t>(code);
|
||||
Codegen::ApplyStubRelocations(body, match->Relocations, siteAddress, reinterpret_cast<std::uint64_t>(code), 0);
|
||||
body.at(match->ReturnBranchOffset) = 0xC3;
|
||||
require(body.size() <= kPage, "Masked store stub exceeds its code page");
|
||||
std::memcpy(code, body.data(), body.size());
|
||||
for (const auto pattern : patterns) {
|
||||
Machine machine{};
|
||||
for (unsigned reg = 0; reg < 16; ++reg)
|
||||
for (unsigned byte = 0; byte < 32; ++byte)
|
||||
machine.Ymm[reg][byte] = static_cast<std::uint8_t>(reg * 37 + byte * 11 + 1);
|
||||
for (unsigned element = 0; element < 32u / form.ElementSize; ++element) {
|
||||
const bool active = element >= elements || (pattern >> element & 1) != 0;
|
||||
auto* lane = machine.Ymm[addressing.Mask] + element * form.ElementSize;
|
||||
std::memset(lane, active ? 0x00 : 0xFF, form.ElementSize);
|
||||
lane[0] = 0x01;
|
||||
lane[form.ElementSize - 1] = active ? 0x80 : 0x7F;
|
||||
}
|
||||
machine.Gpr[Rax] = 0x1111111111111111ull;
|
||||
machine.Gpr[Rcx] = 0x2222222222222222ull;
|
||||
machine.Gpr[Rbx] = 8;
|
||||
machine.Gpr[Rsi] = reinterpret_cast<std::uint64_t>(target) - 0x7F - 16;
|
||||
machine.Gpr[Rdi] = reinterpret_cast<std::uint64_t>(target);
|
||||
machine.Gpr[R9] = reinterpret_cast<std::uint64_t>(target) - 5 * 8;
|
||||
machine.Gpr[R10] = 5;
|
||||
if (mode == 2) {
|
||||
machine.Gpr[Rcx] = 3;
|
||||
machine.Gpr[Rax] = reinterpret_cast<std::uint64_t>(target) + 0x1000 - 12;
|
||||
}
|
||||
machine.Flags = (runs & 1) != 0 ? 0x8D7 : 0x002;
|
||||
machine.Stack = reinterpret_cast<std::uint64_t>(stack);
|
||||
machine.Code = reinterpret_cast<std::uint64_t>(code);
|
||||
const auto input = machine;
|
||||
require(mprotect(protectedPage, kPage, PROT_READ | PROT_WRITE) == 0, "Cannot unprotect the watched page");
|
||||
std::memset(target - 16, kFill, 64);
|
||||
std::memset(stack - 8 - 128, kSentinel, 128);
|
||||
require(mprotect(protectedPage, kPage, PROT_READ) == 0, "Cannot protect the watched page");
|
||||
faults = 0;
|
||||
run(machine);
|
||||
++runs;
|
||||
bool touchesProtected = false;
|
||||
for (unsigned element = 0; element < elements; ++element) {
|
||||
const bool active = (pattern >> element & 1) != 0;
|
||||
for (unsigned byte = 0; byte < form.ElementSize; ++byte) {
|
||||
const auto at = element * form.ElementSize + byte;
|
||||
require(target[at] == (active ? input.Ymm[addressing.Source][at] : kFill), "Masked store wrote an inactive element or lost an active one");
|
||||
touchesProtected = touchesProtected || (active && target + at >= protectedPage);
|
||||
}
|
||||
}
|
||||
for (int at = -16; at < 0; ++at)
|
||||
require(target[at] == kFill, "Masked store wrote before its operand");
|
||||
for (unsigned at = elements * form.ElementSize; at < 48; ++at)
|
||||
require(target[at] == kFill, "Masked store wrote past its operand");
|
||||
require(faults == (touchesProtected ? 1 : 0), "Masked store stub did not fault exactly once on the protected page");
|
||||
for (unsigned at = 0; at < 128; ++at)
|
||||
require(stack[-8 - 128 + static_cast<int>(at)] == kSentinel, "Masked store stub wrote into the red zone");
|
||||
require(std::memcmp(machine.Ymm, input.Ymm, sizeof(input.Ymm)) == 0, "Masked store stub changed a vector register");
|
||||
require(std::memcmp(machine.Gpr, input.Gpr, sizeof(input.Gpr)) == 0, "Masked store stub changed a general register");
|
||||
require((machine.Flags & 0x8D5) == (input.Flags & 0x8D5), "Masked store stub changed the flags");
|
||||
require(machine.Stack == input.Stack, "Masked store stub did not restore RSP");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sigaction(SIGSEGV, &previousSegv, nullptr);
|
||||
sigaction(SIGBUS, &previousBus, nullptr);
|
||||
munmap(region, (kDataPages + 2) * kPage);
|
||||
std::cout << runs << " masked store executions\n";
|
||||
}
|
||||
#else
|
||||
void execution() { std::cout << "Masked store execution checks require a Linux/macOS x86-64 build\n"; }
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
decoding();
|
||||
body();
|
||||
matching();
|
||||
execution();
|
||||
std::cout << "Masked store lowering checks passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,16 @@ def main():
|
||||
assert (andn not in output.read_bytes()) == lowered, f"{options} {'did not lower' if lowered else 'changed'} ANDN"
|
||||
if lowered:
|
||||
assert "ANDN" in result.stdout, result.stdout
|
||||
masked = bytes.fromhex("c4e2752e07c3")
|
||||
for target in ((), ("--macos",)):
|
||||
for conversion in ((), ("--to-intel",), ("--to-rosetta",)):
|
||||
options = target + conversion
|
||||
result, output = relink(relinker, directory, "masked" + "".join(options), *options, code=masked)
|
||||
assert result.returncode == 0, (options, result.stdout, result.stderr)
|
||||
lowered = conversion == ("--to-rosetta",)
|
||||
assert (masked not in output.read_bytes()) == lowered, f"{options} {'did not lower' if lowered else 'changed'} VMASKMOVPS"
|
||||
if lowered:
|
||||
assert "VMASKMOVPS" in result.stdout, result.stdout
|
||||
print("--to-rosetta integration tests passed")
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
- Read-only data can also sit between the unwind functions of such images. There, a `syscall`, `sysenter` or `int 0x80` pattern fails the [syscall scan](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) only when a constant syscall number is set before it; the others, and every `sysret`, are skipped and counted in one message. The number is looked for in the decoded instructions and, whatever alignment the decoding took, in the bytes before the pattern: a `mov eax, imm32`, `mov rax, imm32`, `mov rax, imm64` or `xor eax, eax`, optionally followed by `mov al, imm8` or `mov ax, imm16`, then by any number of register moves or zeroings that leave `rax` alone. A real syscall without an unwind entry whose number is set otherwise is not refused and runs on the host: from memory or another register (`mov eax, ecx`), by `push`/`pop` or arithmetic (`or eax, imm32`), or with any other instruction between the load and the pattern.
|
||||
- In the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs for such images, a relocation or `lea` target outside every unwind function is followed as code only when its linear decoding, up to the first jump, return or trap, the next unwind function or 256 instructions, holds an `fs:[disp32]` access, or when it is 16-byte aligned, starts less than 256 bytes after an unwind function and ends in a jump, return or trap. Every other such target is taken as data and counted in one message, and the Windows TLS rewrite fails instead of overwriting one. A TLS access of another form, such as `mov rax, fs:[rax]`, or a `gs` instruction in code reached only through such a target is therefore not rewritten or trapped. Other fs and gs instructions do not tell code from data there: string bytes decode as these prefixes, and in two measured titles about 2,500 of about 5,500 such targets decode to one before their first jump.
|
||||
- In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP`, or of the `/libexec/ld-elf.so.1` string that begins the segment when no `PT_INTERP` covers it, as in OpenOrbis modules, to the end of the PLT after the last function of the unwind table, or to the end of that function when the PLT comes before the code, as in the `libSceFios2.prx` and `libc.prx` that CUSA02636 bundles (PLT at 0xc8); the pipeline's scans of executables above stop at this same end, also when the PLT comes before the code. A guest module with PLT relocations and no PLT before or after its unwind functions, or with a PLT that overlaps one, fails as the executable does. Code after that end is not analyzed, such as the `DT_FINI` routine without an unwind entry that follows the last unwind function in both modules and that the Windows and macOS outputs call when the program exits: a TLS access in it is not rewritten, the ELF entry point, `DT_INIT`, `DT_FINI`, a branch or a pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion.
|
||||
- Under Rosetta (M5, macOS 27.0.1), a VMASKMOVPS, VMASKMOVPD, VPMASKMOVD or VPMASKMOVQ store or load that faults on an element other than an active first one ends the process (`unexpectedly need to EmulateForward on a synchronous exception`) instead of raising SIGSEGV or SIGBUS, so a masked store into a page the macOS write watch keeps read-only ends the title. `--to-rosetta` moves each masked store to a [stub](../../../core/relinker/codegen/src/x86/MaskedStoreLowering.cpp) that first writes to the first and last byte of every active element with `LOCK OR` of 0, which faults normally and changes nothing; each store then costs up to 16 locked byte writes. A page made read-only again by another thread between those writes and the store still ends the process. A stack-relative displacement that exceeds signed disp32 after the spill frame throws. Masked loads are not converted.
|
||||
- A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)).
|
||||
- In macOS output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`.
|
||||
- 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.
|
||||
|
||||
Reference in new issue
Block a user