mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 19:07:01 +02:00
35c6e8fff6
* loader: enable AMD CPU instruction patching on macOS The guest instruction patcher was compiled out on macOS and runtimeLinker forced emulate_amd off, so under Rosetta every SSE4a EXTRQ/INSERTQ took the SIGILL path (about 18 us each). Build the patcher on all hosts and let --amd-cpu enable it on macOS like on Windows and Linux. The host feature probe already reports no SSE4a under Rosetta, so EXTRQ becomes a native trampoline and the remaining fallbacks keep using the x64 instruction emulator through the host exception handler. * tests: macos_sse4a covers the guest instruction patcher on macOS Patch EXTRQ/INSERTQ blobs in an RWX area with a trampoline page behind it, with the host features of Rosetta (no SSE4a/RDPID/CLWB), and compare against the SIGILL emulator: EXTRQ must become a native trampoline without SIGILL, INSERTQ stays a trap handled by the emulator, and nothing is touched without emulate_amd. * loader: emulate the patcher's VRSQRTPS trap and MWAITX on macOS The AMD CPU patcher marks VRSQRTPS it cannot relocate by clearing a reserved VEX.vvvv bit, which raises #UD on every host including Rosetta. The macOS build left TryEmulateReciprocalSquareRoot and TryEmulateMonitorxMwaitx out, so the guest died with an unhandled SIGILL at the first such site. Both are plain SSE2/libc, so enable them on macOS. The Darwin context also gains ClearUpperYmm so the VEX.128 result zeroes YMM[255:128] when the thread has the AVX context. macos_sse4a covers the trapped VRSQRTPS and MWAITX. * tests: macos_sse4a runs a plain VEX.128 VRSQRTPS through the patcher The existing case started from an instruction that was already marked as a trap. The new cases feed the ordinary encoding to PatchGuestInstructions and compare the result with the emulated reference, both through a native trampoline and, when the instruction cannot be relocated, through the trap mark and the signal emulator. * loader: compact macOS instruction patcher coverage Share the emulator dispatch chain while keeping SHA emulation limited to non-Apple hosts and preserving handler order. Remove a duplicate VRSQRTPS signal test already covered by both native and trapped patcher cases, plus blank lines left by removed platform guards. --------- Co-authored-by: nmzik <Nmzik@mail.ru>
1764 lines
66 KiB
C++
1764 lines
66 KiB
C++
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "loader/guestInstructionPatcher.h"
|
|
|
|
#include "common/assert.h"
|
|
#include "common/logging/log.h"
|
|
#include "common/virtualMemory.h"
|
|
|
|
#include <Zydis/Zydis.h>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <bitset>
|
|
#include <climits>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
#include <xbyak/xbyak.h>
|
|
#include <xbyak/xbyak_util.h>
|
|
|
|
#ifdef min
|
|
#undef min
|
|
#endif
|
|
#ifdef max
|
|
#undef max
|
|
#endif
|
|
|
|
using namespace Xbyak::util;
|
|
|
|
namespace Loader {
|
|
|
|
using u8 = uint8_t;
|
|
using s8 = int8_t;
|
|
using u16 = uint16_t;
|
|
using s16 = int16_t;
|
|
using u32 = uint32_t;
|
|
using s32 = int32_t;
|
|
using u64 = uint64_t;
|
|
using s64 = int64_t;
|
|
|
|
#define ASSERT(condition) EXIT_IF(!(condition))
|
|
|
|
constexpr size_t NearJumpSize = 5;
|
|
|
|
struct PatchModule {
|
|
std::mutex mutex {};
|
|
u8* start = nullptr;
|
|
u8* end = nullptr;
|
|
std::set<u8*> patched;
|
|
Xbyak::CodeGenerator patch_gen;
|
|
Xbyak::CodeGenerator trampoline_gen;
|
|
bool trampoline_exhaustion_reported = false;
|
|
|
|
PatchModule(u8* module_ptr, u64 module_size, u8* trampoline_ptr, u64 trampoline_size)
|
|
: start(module_ptr), end(module_ptr + module_size), patch_gen(module_size, module_ptr),
|
|
trampoline_gen(trampoline_size, trampoline_ptr) {}
|
|
};
|
|
|
|
static std::map<u64, PatchModule> g_patch_modules;
|
|
|
|
static PatchModule* GetContainingModule(const void* ptr) {
|
|
auto upper = g_patch_modules.upper_bound(reinterpret_cast<u64>(ptr));
|
|
if (upper == g_patch_modules.begin()) {
|
|
return nullptr;
|
|
}
|
|
auto* module = &std::prev(upper)->second;
|
|
auto* address = static_cast<const u8*>(ptr);
|
|
return address >= module->start && address < module->end ? module : nullptr;
|
|
}
|
|
|
|
static bool HandleTrampolineError(PatchModule* module, int error) {
|
|
if (error != Xbyak::ERR_CODE_IS_TOO_BIG) {
|
|
return false;
|
|
}
|
|
if (!module->trampoline_exhaustion_reported) {
|
|
LOGF("Guest instruction trampoline space exhausted for module %p\n",
|
|
static_cast<void*>(module->start));
|
|
module->trampoline_exhaustion_reported = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static ZydisDecoder& GetDecoder() {
|
|
static ZydisDecoder decoder = [] {
|
|
ZydisDecoder value {};
|
|
EXIT_IF(!ZYAN_SUCCESS(
|
|
ZydisDecoderInit(&value, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64)));
|
|
return value;
|
|
}();
|
|
return decoder;
|
|
}
|
|
|
|
namespace {
|
|
|
|
constexpr size_t GuestRedZoneSize = 128;
|
|
constexpr size_t ShortJumpSize = 2;
|
|
using RedZoneMask = std::bitset<GuestRedZoneSize>;
|
|
|
|
struct DecodedCodeInstruction {
|
|
uintptr_t address {};
|
|
ZydisDecodedInstruction instruction {};
|
|
std::array<ZydisDecodedOperand, ZYDIS_MAX_OPERAND_COUNT> operands {};
|
|
RedZoneMask red_zone_use {};
|
|
RedZoneMask red_zone_def {};
|
|
RedZoneMask red_zone_live {};
|
|
bool accesses_memory {};
|
|
bool uses_stack_pointer {};
|
|
bool has_red_zone_operand {};
|
|
bool has_unmodeled_red_zone_operand {};
|
|
bool changes_stack_pointer {};
|
|
bool replaces_stack_pointer {};
|
|
std::optional<s64> stack_pointer_delta;
|
|
};
|
|
|
|
struct DecodedFunction {
|
|
std::map<uintptr_t, DecodedCodeInstruction> instructions;
|
|
std::set<uintptr_t> branch_targets;
|
|
bool uses_red_zone {};
|
|
bool has_indirect_branch {};
|
|
bool requires_conservative_red_zone_tracking {};
|
|
};
|
|
|
|
enum class InstructionReplacement {
|
|
None,
|
|
ReciprocalSquareRoot,
|
|
ExtractQ,
|
|
InsertQ,
|
|
ReadProcessorId,
|
|
CacheLineWriteBack
|
|
};
|
|
|
|
struct InstructionRewrite {
|
|
bool protect_red_zone {};
|
|
bool protected_indirect_call {};
|
|
InstructionReplacement replacement {};
|
|
};
|
|
|
|
InstructionPatchCounts& ReplacementCounts(GuestInstructionPatchResult& result,
|
|
InstructionReplacement replacement) {
|
|
ASSERT(replacement != InstructionReplacement::None);
|
|
switch (replacement) {
|
|
case InstructionReplacement::ReciprocalSquareRoot: return result.reciprocal_sqrt;
|
|
case InstructionReplacement::ExtractQ: return result.extrq;
|
|
case InstructionReplacement::InsertQ: return result.insertq;
|
|
case InstructionReplacement::ReadProcessorId: return result.rdpid;
|
|
default: return result.clwb;
|
|
}
|
|
}
|
|
|
|
bool UsesInstructionTrap(InstructionReplacement replacement, bool trap_replacements) {
|
|
return replacement != InstructionReplacement::None &&
|
|
(trap_replacements || replacement == InstructionReplacement::InsertQ ||
|
|
replacement == InstructionReplacement::ReadProcessorId);
|
|
}
|
|
|
|
bool IsStackPointerRegister(ZydisRegister reg) {
|
|
return reg != ZYDIS_REGISTER_NONE &&
|
|
ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, reg) == ZYDIS_REGISTER_RSP;
|
|
}
|
|
|
|
bool IsControlFlowTerminator(const ZydisDecodedInstruction& instruction) {
|
|
return instruction.meta.category == ZYDIS_CATEGORY_UNCOND_BR ||
|
|
instruction.meta.category == ZYDIS_CATEGORY_RET ||
|
|
instruction.meta.category == ZYDIS_CATEGORY_INTERRUPT ||
|
|
instruction.meta.category == ZYDIS_CATEGORY_SYSRET ||
|
|
instruction.mnemonic == ZYDIS_MNEMONIC_UD2;
|
|
}
|
|
|
|
uintptr_t GetRelativeTarget(const DecodedCodeInstruction& decoded) {
|
|
for (u8 index = 0; index < decoded.instruction.operand_count_visible; ++index) {
|
|
const auto& operand = decoded.operands[index];
|
|
if (operand.type != ZYDIS_OPERAND_TYPE_IMMEDIATE || !operand.imm.is_relative) {
|
|
continue;
|
|
}
|
|
|
|
ZyanU64 target {};
|
|
if (ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&decoded.instruction, &operand, decoded.address,
|
|
&target))) {
|
|
return target;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
DecodedCodeInstruction DecodeCodeInstruction(uintptr_t address, uintptr_t end) {
|
|
DecodedCodeInstruction decoded {.address = address};
|
|
const auto status = ZydisDecoderDecodeFull(&GetDecoder(), reinterpret_cast<void*>(address),
|
|
end - address, &decoded.instruction,
|
|
decoded.operands.data());
|
|
if (!ZYAN_SUCCESS(status)) {
|
|
decoded.instruction.length = 0;
|
|
return decoded;
|
|
}
|
|
|
|
for (u8 index = 0; index < decoded.instruction.operand_count; ++index) {
|
|
const auto& operand = decoded.operands[index];
|
|
if (operand.type == ZYDIS_OPERAND_TYPE_REGISTER) {
|
|
decoded.uses_stack_pointer |= IsStackPointerRegister(operand.reg.value);
|
|
if (IsStackPointerRegister(operand.reg.value) &&
|
|
(operand.actions & ZYDIS_OPERAND_ACTION_MASK_WRITE) != 0 &&
|
|
decoded.instruction.meta.category != ZYDIS_CATEGORY_CALL &&
|
|
decoded.instruction.meta.category != ZYDIS_CATEGORY_RET) {
|
|
decoded.changes_stack_pointer = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY) {
|
|
continue;
|
|
}
|
|
|
|
const bool stack_relative =
|
|
IsStackPointerRegister(operand.mem.base) || IsStackPointerRegister(operand.mem.index);
|
|
decoded.uses_stack_pointer |= stack_relative;
|
|
constexpr ZydisOperandActions MemoryAccessMask =
|
|
ZYDIS_OPERAND_ACTION_MASK_READ | ZYDIS_OPERAND_ACTION_MASK_WRITE;
|
|
if (decoded.instruction.mnemonic != ZYDIS_MNEMONIC_LEA &&
|
|
decoded.instruction.mnemonic != ZYDIS_MNEMONIC_NOP && !stack_relative &&
|
|
(operand.actions & MemoryAccessMask) != 0) {
|
|
decoded.accesses_memory = true;
|
|
}
|
|
}
|
|
|
|
if (decoded.changes_stack_pointer) {
|
|
const auto& operands = decoded.operands;
|
|
if (operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
|
|
IsStackPointerRegister(operands[0].reg.value) &&
|
|
(decoded.instruction.mnemonic == ZYDIS_MNEMONIC_RDPID ||
|
|
(decoded.instruction.mnemonic == ZYDIS_MNEMONIC_MOV &&
|
|
operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY))) {
|
|
decoded.replaces_stack_pointer = true;
|
|
} else if ((decoded.instruction.mnemonic == ZYDIS_MNEMONIC_ADD ||
|
|
decoded.instruction.mnemonic == ZYDIS_MNEMONIC_SUB) &&
|
|
operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
|
|
IsStackPointerRegister(operands[0].reg.value) &&
|
|
operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE) {
|
|
const s64 immediate = operands[1].imm.is_signed
|
|
? operands[1].imm.value.s
|
|
: static_cast<s64>(operands[1].imm.value.u);
|
|
decoded.stack_pointer_delta =
|
|
decoded.instruction.mnemonic == ZYDIS_MNEMONIC_ADD ? immediate : -immediate;
|
|
} else if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_LEA &&
|
|
operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
|
|
IsStackPointerRegister(operands[0].reg.value) &&
|
|
operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY &&
|
|
IsStackPointerRegister(operands[1].mem.base) &&
|
|
operands[1].mem.index == ZYDIS_REGISTER_NONE) {
|
|
decoded.stack_pointer_delta = operands[1].mem.disp.value;
|
|
} else if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_PUSH ||
|
|
decoded.instruction.mnemonic == ZYDIS_MNEMONIC_PUSHF ||
|
|
decoded.instruction.mnemonic == ZYDIS_MNEMONIC_PUSHFD ||
|
|
decoded.instruction.mnemonic == ZYDIS_MNEMONIC_PUSHFQ) {
|
|
decoded.stack_pointer_delta = -static_cast<s64>(sizeof(u64));
|
|
} else if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_POP ||
|
|
decoded.instruction.mnemonic == ZYDIS_MNEMONIC_POPF ||
|
|
decoded.instruction.mnemonic == ZYDIS_MNEMONIC_POPFD ||
|
|
decoded.instruction.mnemonic == ZYDIS_MNEMONIC_POPFQ) {
|
|
decoded.stack_pointer_delta = sizeof(u64);
|
|
}
|
|
}
|
|
|
|
for (u8 index = 0; index < decoded.instruction.operand_count_visible; ++index) {
|
|
const auto& operand = decoded.operands[index];
|
|
if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY ||
|
|
!IsStackPointerRegister(operand.mem.base) || operand.mem.disp.size == 0 ||
|
|
operand.mem.disp.value >= 0) {
|
|
continue;
|
|
}
|
|
|
|
decoded.has_red_zone_operand = true;
|
|
if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_LEA) {
|
|
if (decoded.stack_pointer_delta.has_value()) {
|
|
decoded.has_red_zone_operand = false;
|
|
continue;
|
|
}
|
|
decoded.has_unmodeled_red_zone_operand = true;
|
|
continue;
|
|
}
|
|
|
|
const s64 access_start = operand.mem.disp.value;
|
|
const s64 access_size = std::max<s64>(operand.size / 8, 1);
|
|
const s64 range_start = std::max(access_start, -static_cast<s64>(GuestRedZoneSize));
|
|
const s64 range_end = std::min<s64>(access_start + access_size, 0);
|
|
for (s64 offset = range_start; offset < range_end; ++offset) {
|
|
const size_t bit = static_cast<size_t>(offset + static_cast<s64>(GuestRedZoneSize));
|
|
if ((operand.actions & ZYDIS_OPERAND_ACTION_MASK_READ) != 0) {
|
|
decoded.red_zone_use.set(bit);
|
|
}
|
|
if ((operand.actions & ZYDIS_OPERAND_ACTION_WRITE) != 0) {
|
|
decoded.red_zone_def.set(bit);
|
|
}
|
|
}
|
|
}
|
|
return decoded;
|
|
}
|
|
|
|
bool IsSameRegister(ZydisRegister lhs, ZydisRegister rhs) {
|
|
return lhs != ZYDIS_REGISTER_NONE && rhs != ZYDIS_REGISTER_NONE &&
|
|
ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, lhs) ==
|
|
ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, rhs);
|
|
}
|
|
|
|
bool WritesRegister(const DecodedCodeInstruction& decoded, ZydisRegister reg) {
|
|
return std::ranges::any_of(
|
|
std::span {decoded.operands}.first(decoded.instruction.operand_count),
|
|
[reg](const ZydisDecodedOperand& operand) {
|
|
return operand.type == ZYDIS_OPERAND_TYPE_REGISTER &&
|
|
(operand.actions & ZYDIS_OPERAND_ACTION_MASK_WRITE) != 0 &&
|
|
IsSameRegister(operand.reg.value, reg);
|
|
});
|
|
}
|
|
|
|
std::optional<std::vector<uintptr_t>>
|
|
ResolveBoundedJumpTable(const DecodedFunction& function, uintptr_t branch_address,
|
|
uintptr_t function_start, uintptr_t function_end, uintptr_t segment_start,
|
|
uintptr_t segment_end) {
|
|
const auto branch = function.instructions.find(branch_address);
|
|
if (branch == function.instructions.end() ||
|
|
branch->second.instruction.mnemonic != ZYDIS_MNEMONIC_JMP ||
|
|
branch->second.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER) {
|
|
return std::nullopt;
|
|
}
|
|
const ZydisRegister target_reg = branch->second.operands[0].reg.value;
|
|
|
|
const auto previous_contiguous = [&function](auto instruction) {
|
|
if (instruction == function.instructions.begin()) {
|
|
return function.instructions.end();
|
|
}
|
|
const auto previous = std::prev(instruction);
|
|
return previous->first + previous->second.instruction.length == instruction->first
|
|
? previous
|
|
: function.instructions.end();
|
|
};
|
|
|
|
constexpr size_t MaxInterveningInstructions = 4;
|
|
auto add = function.instructions.end();
|
|
auto pattern_cursor = branch;
|
|
for (size_t count = 0; count <= MaxInterveningInstructions; ++count) {
|
|
const auto candidate = previous_contiguous(pattern_cursor);
|
|
if (candidate == function.instructions.end()) {
|
|
break;
|
|
}
|
|
const auto& decoded = candidate->second;
|
|
if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_ADD &&
|
|
decoded.operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
|
|
IsSameRegister(decoded.operands[0].reg.value, target_reg) &&
|
|
decoded.operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER) {
|
|
add = candidate;
|
|
break;
|
|
}
|
|
if (WritesRegister(decoded, target_reg) || IsControlFlowTerminator(decoded.instruction)) {
|
|
return std::nullopt;
|
|
}
|
|
pattern_cursor = candidate;
|
|
}
|
|
if (add == function.instructions.end()) {
|
|
return std::nullopt;
|
|
}
|
|
const ZydisRegister table_reg = add->second.operands[1].reg.value;
|
|
|
|
const auto load = previous_contiguous(add);
|
|
if (load == function.instructions.end() ||
|
|
load->second.instruction.mnemonic != ZYDIS_MNEMONIC_MOVSXD ||
|
|
load->second.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER ||
|
|
!IsSameRegister(load->second.operands[0].reg.value, target_reg) ||
|
|
load->second.operands[1].type != ZYDIS_OPERAND_TYPE_MEMORY ||
|
|
!IsSameRegister(load->second.operands[1].mem.base, table_reg) ||
|
|
load->second.operands[1].mem.index == ZYDIS_REGISTER_NONE ||
|
|
load->second.operands[1].mem.scale != sizeof(s32)) {
|
|
return std::nullopt;
|
|
}
|
|
const ZydisRegister index_reg = load->second.operands[1].mem.index;
|
|
|
|
constexpr size_t MaxPatternInstructions = 64;
|
|
std::optional<size_t> table_size;
|
|
std::optional<uintptr_t> guarded_path_start;
|
|
auto cursor = load;
|
|
for (size_t count = 0; count < MaxPatternInstructions; ++count) {
|
|
cursor = previous_contiguous(cursor);
|
|
if (cursor == function.instructions.end()) {
|
|
break;
|
|
}
|
|
const auto& decoded = cursor->second;
|
|
if (decoded.instruction.mnemonic == ZYDIS_MNEMONIC_CMP &&
|
|
decoded.operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER &&
|
|
IsSameRegister(decoded.operands[0].reg.value, index_reg) &&
|
|
decoded.operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE) {
|
|
const uintptr_t next_address = decoded.address + decoded.instruction.length;
|
|
const auto bounds_branch = function.instructions.find(next_address);
|
|
if (bounds_branch == function.instructions.end()) {
|
|
return std::nullopt;
|
|
}
|
|
const s64 bound = decoded.operands[1].imm.is_signed
|
|
? decoded.operands[1].imm.value.s
|
|
: static_cast<s64>(decoded.operands[1].imm.value.u);
|
|
if (bound < 0) {
|
|
return std::nullopt;
|
|
}
|
|
if (bounds_branch->second.instruction.mnemonic == ZYDIS_MNEMONIC_JNBE) {
|
|
table_size = static_cast<size_t>(bound) + 1;
|
|
} else if (bounds_branch->second.instruction.mnemonic == ZYDIS_MNEMONIC_JNB) {
|
|
table_size = static_cast<size_t>(bound);
|
|
} else {
|
|
return std::nullopt;
|
|
}
|
|
guarded_path_start = next_address + bounds_branch->second.instruction.length;
|
|
break;
|
|
}
|
|
if (WritesRegister(decoded, index_reg)) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
if (!table_size) {
|
|
return std::nullopt;
|
|
}
|
|
if (std::ranges::any_of(function.branch_targets, [&](uintptr_t target) {
|
|
return target >= *guarded_path_start && target <= branch_address;
|
|
})) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
const auto decode_table_address =
|
|
[table_reg](const DecodedCodeInstruction& decoded) -> std::optional<uintptr_t> {
|
|
if (decoded.instruction.mnemonic != ZYDIS_MNEMONIC_LEA ||
|
|
decoded.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER ||
|
|
!IsSameRegister(decoded.operands[0].reg.value, table_reg) ||
|
|
decoded.operands[1].type != ZYDIS_OPERAND_TYPE_MEMORY) {
|
|
return std::nullopt;
|
|
}
|
|
ZyanU64 absolute_address {};
|
|
if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&decoded.instruction, &decoded.operands[1],
|
|
decoded.address, &absolute_address))) {
|
|
return std::nullopt;
|
|
}
|
|
return absolute_address;
|
|
};
|
|
|
|
std::set<uintptr_t> table_candidates;
|
|
cursor = load;
|
|
for (size_t count = 0; count < MaxPatternInstructions; ++count) {
|
|
cursor = previous_contiguous(cursor);
|
|
if (cursor == function.instructions.end()) {
|
|
break;
|
|
}
|
|
if (!WritesRegister(cursor->second, table_reg)) {
|
|
continue;
|
|
}
|
|
if (const auto address = decode_table_address(cursor->second)) {
|
|
table_candidates.insert(*address);
|
|
}
|
|
break;
|
|
}
|
|
if (table_candidates.empty()) {
|
|
for (const auto& [address, decoded]: function.instructions) {
|
|
if (address >= branch_address) {
|
|
break;
|
|
}
|
|
if (const auto table_address = decode_table_address(decoded)) {
|
|
table_candidates.insert(*table_address);
|
|
}
|
|
}
|
|
}
|
|
constexpr size_t MaxJumpTableEntries = 4096;
|
|
if (*table_size == 0 || *table_size > MaxJumpTableEntries) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<std::vector<uintptr_t>> resolved_targets;
|
|
for (const uintptr_t table_address: table_candidates) {
|
|
if (table_address < segment_start || table_address > segment_end ||
|
|
*table_size > (segment_end - table_address) / sizeof(s32)) {
|
|
continue;
|
|
}
|
|
|
|
std::vector<uintptr_t> targets;
|
|
targets.reserve(*table_size);
|
|
bool valid = true;
|
|
for (size_t index = 0; index < *table_size; ++index) {
|
|
s32 offset;
|
|
std::memcpy(&offset,
|
|
reinterpret_cast<const void*>(table_address + index * sizeof(offset)),
|
|
sizeof(offset));
|
|
const s64 target = static_cast<s64>(table_address) + offset;
|
|
if (target < static_cast<s64>(function_start) ||
|
|
target >= static_cast<s64>(function_end)) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
targets.push_back(static_cast<uintptr_t>(target));
|
|
}
|
|
if (!valid) {
|
|
continue;
|
|
}
|
|
std::ranges::sort(targets);
|
|
targets.erase(std::ranges::unique(targets).begin(), targets.end());
|
|
if (resolved_targets) {
|
|
return std::nullopt;
|
|
}
|
|
resolved_targets = std::move(targets);
|
|
}
|
|
return resolved_targets;
|
|
}
|
|
|
|
DecodedFunction DecodeFunction(uintptr_t function_start, uintptr_t function_end,
|
|
uintptr_t segment_start, uintptr_t segment_end) {
|
|
DecodedFunction function;
|
|
std::vector<uintptr_t> blocks {function_start};
|
|
std::unordered_set<uintptr_t> visited;
|
|
std::set<uintptr_t> indirect_branches;
|
|
std::set<uintptr_t> resolved_indirect_branches;
|
|
|
|
while (true) {
|
|
while (!blocks.empty()) {
|
|
uintptr_t address = blocks.back();
|
|
blocks.pop_back();
|
|
|
|
while (address >= function_start && address < function_end &&
|
|
!visited.contains(address)) {
|
|
visited.insert(address);
|
|
auto decoded = DecodeCodeInstruction(address, function_end);
|
|
if (decoded.instruction.length == 0) {
|
|
break;
|
|
}
|
|
|
|
function.uses_red_zone |= decoded.has_red_zone_operand;
|
|
function.requires_conservative_red_zone_tracking |=
|
|
decoded.has_unmodeled_red_zone_operand ||
|
|
(decoded.changes_stack_pointer && !decoded.stack_pointer_delta.has_value());
|
|
|
|
const uintptr_t next_address = address + decoded.instruction.length;
|
|
const uintptr_t branch_target = GetRelativeTarget(decoded);
|
|
if (branch_target >= function_start && branch_target < function_end) {
|
|
function.branch_targets.insert(branch_target);
|
|
}
|
|
function.instructions.emplace(address, decoded);
|
|
|
|
if (decoded.instruction.meta.category == ZYDIS_CATEGORY_COND_BR) {
|
|
if (branch_target >= function_start && branch_target < function_end) {
|
|
blocks.push_back(branch_target);
|
|
}
|
|
} else if (IsControlFlowTerminator(decoded.instruction)) {
|
|
if (decoded.instruction.meta.category == ZYDIS_CATEGORY_UNCOND_BR &&
|
|
branch_target >= function_start && branch_target < function_end) {
|
|
blocks.push_back(branch_target);
|
|
} else if (decoded.instruction.meta.category == ZYDIS_CATEGORY_UNCOND_BR &&
|
|
branch_target == 0) {
|
|
indirect_branches.insert(address);
|
|
}
|
|
break;
|
|
}
|
|
address = next_address;
|
|
}
|
|
}
|
|
|
|
bool discovered_block = false;
|
|
for (const uintptr_t branch_address: indirect_branches) {
|
|
if (resolved_indirect_branches.contains(branch_address)) {
|
|
continue;
|
|
}
|
|
const auto targets = ResolveBoundedJumpTable(function, branch_address, function_start,
|
|
function_end, segment_start, segment_end);
|
|
if (!targets) {
|
|
continue;
|
|
}
|
|
resolved_indirect_branches.insert(branch_address);
|
|
for (const uintptr_t target: *targets) {
|
|
function.branch_targets.insert(target);
|
|
if (!visited.contains(target)) {
|
|
blocks.push_back(target);
|
|
discovered_block = true;
|
|
}
|
|
}
|
|
}
|
|
if (!discovered_block) {
|
|
break;
|
|
}
|
|
}
|
|
function.has_indirect_branch = indirect_branches.size() != resolved_indirect_branches.size();
|
|
return function;
|
|
}
|
|
|
|
std::optional<RedZoneMask> TranslateRedZoneMask(const RedZoneMask& mask, s64 stack_pointer_delta) {
|
|
if (stack_pointer_delta == 0) {
|
|
return mask;
|
|
}
|
|
RedZoneMask translated;
|
|
for (size_t bit = 0; bit < GuestRedZoneSize; ++bit) {
|
|
if (!mask.test(bit)) {
|
|
continue;
|
|
}
|
|
const s64 after_offset = static_cast<s64>(bit) - static_cast<s64>(GuestRedZoneSize);
|
|
const s64 before_offset = after_offset + stack_pointer_delta;
|
|
if (before_offset < -static_cast<s64>(GuestRedZoneSize) || before_offset >= 0) {
|
|
// A later inverse RSP adjustment can bring this byte back into the red zone.
|
|
// Losing its stack-slot identity makes the bounded analysis inconclusive.
|
|
return std::nullopt;
|
|
}
|
|
translated.set(static_cast<size_t>(before_offset + static_cast<s64>(GuestRedZoneSize)));
|
|
}
|
|
return translated;
|
|
}
|
|
|
|
void AnalyzeRedZoneLiveness(DecodedFunction& function) {
|
|
if (!function.uses_red_zone) {
|
|
return;
|
|
}
|
|
|
|
const auto protect_function = [&] {
|
|
for (auto& [_, decoded]: function.instructions) {
|
|
decoded.red_zone_live.set();
|
|
}
|
|
};
|
|
if (function.has_indirect_branch || function.requires_conservative_red_zone_tracking ||
|
|
std::ranges::any_of(function.branch_targets, [&function](uintptr_t target) {
|
|
return !function.instructions.contains(target);
|
|
})) {
|
|
protect_function();
|
|
return;
|
|
}
|
|
|
|
std::vector<DecodedCodeInstruction*> instructions;
|
|
instructions.reserve(function.instructions.size());
|
|
std::map<uintptr_t, size_t> indices;
|
|
for (auto& [address, decoded]: function.instructions) {
|
|
indices.emplace(address, instructions.size());
|
|
instructions.push_back(&decoded);
|
|
}
|
|
|
|
std::vector<RedZoneMask> live_in(instructions.size());
|
|
bool changed;
|
|
do {
|
|
changed = false;
|
|
for (size_t reverse_index = instructions.size(); reverse_index-- > 0;) {
|
|
const auto& decoded = *instructions[reverse_index];
|
|
RedZoneMask live_out;
|
|
|
|
const auto add_successor = [&](uintptr_t address) {
|
|
if (const auto successor = indices.find(address); successor != indices.end()) {
|
|
live_out |= live_in[successor->second];
|
|
}
|
|
};
|
|
|
|
const uintptr_t next_address = decoded.address + decoded.instruction.length;
|
|
const uintptr_t branch_target = GetRelativeTarget(decoded);
|
|
if (decoded.instruction.meta.category == ZYDIS_CATEGORY_COND_BR) {
|
|
add_successor(next_address);
|
|
add_successor(branch_target);
|
|
} else if (decoded.instruction.meta.category == ZYDIS_CATEGORY_UNCOND_BR) {
|
|
add_successor(branch_target);
|
|
} else if (!IsControlFlowTerminator(decoded.instruction)) {
|
|
add_successor(next_address);
|
|
}
|
|
|
|
const auto translated_live_out =
|
|
TranslateRedZoneMask(live_out, decoded.stack_pointer_delta.value_or(0));
|
|
if (!translated_live_out) {
|
|
protect_function();
|
|
return;
|
|
}
|
|
const RedZoneMask new_live_in =
|
|
decoded.red_zone_use | (*translated_live_out & ~decoded.red_zone_def);
|
|
if (new_live_in != live_in[reverse_index]) {
|
|
live_in[reverse_index] = new_live_in;
|
|
changed = true;
|
|
}
|
|
}
|
|
} while (changed);
|
|
|
|
for (size_t index = 0; index < instructions.size(); ++index) {
|
|
instructions[index]->red_zone_live = live_in[index];
|
|
}
|
|
}
|
|
|
|
bool EncodeRelocatedInstruction(const DecodedCodeInstruction& decoded,
|
|
Xbyak::CodeGenerator& generator, s64 stack_adjustment = 0,
|
|
ZydisMnemonic replacement = ZYDIS_MNEMONIC_INVALID) {
|
|
ZydisEncoderRequest request;
|
|
if (!ZYAN_SUCCESS(ZydisEncoderDecodedInstructionToEncoderRequest(
|
|
&decoded.instruction, decoded.operands.data(),
|
|
decoded.instruction.operand_count_visible, &request))) {
|
|
return false;
|
|
}
|
|
if (replacement != ZYDIS_MNEMONIC_INVALID) {
|
|
request.mnemonic = replacement;
|
|
}
|
|
|
|
for (u8 index = 0; index < decoded.instruction.operand_count_visible; ++index) {
|
|
const auto& operand = decoded.operands[index];
|
|
if (operand.type == ZYDIS_OPERAND_TYPE_IMMEDIATE && operand.imm.is_relative) {
|
|
ZyanU64 absolute_address {};
|
|
if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&decoded.instruction, &operand,
|
|
decoded.address, &absolute_address))) {
|
|
return false;
|
|
}
|
|
request.operands[index].imm.u = absolute_address;
|
|
} else if (operand.type == ZYDIS_OPERAND_TYPE_MEMORY &&
|
|
(operand.mem.base == ZYDIS_REGISTER_RIP ||
|
|
operand.mem.base == ZYDIS_REGISTER_EIP)) {
|
|
ZyanU64 absolute_address {};
|
|
if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&decoded.instruction, &operand,
|
|
decoded.address, &absolute_address))) {
|
|
return false;
|
|
}
|
|
request.operands[index].mem.displacement = static_cast<ZyanI64>(absolute_address);
|
|
} else if (operand.type == ZYDIS_OPERAND_TYPE_MEMORY &&
|
|
IsStackPointerRegister(operand.mem.base)) {
|
|
request.operands[index].mem.displacement += stack_adjustment;
|
|
}
|
|
}
|
|
|
|
std::array<u8, ZYDIS_MAX_INSTRUCTION_LENGTH> encoded {};
|
|
ZyanUSize encoded_size = encoded.size();
|
|
if (!ZYAN_SUCCESS(ZydisEncoderEncodeInstructionAbsolute(
|
|
&request, encoded.data(), &encoded_size,
|
|
reinterpret_cast<ZyanU64>(generator.getCurr())))) {
|
|
return false;
|
|
}
|
|
generator.db(encoded.data(), encoded_size);
|
|
return true;
|
|
}
|
|
|
|
bool GenerateProtectedIndirectCall(const DecodedCodeInstruction& decoded,
|
|
Xbyak::CodeGenerator& generator) {
|
|
ASSERT(decoded.instruction.meta.category == ZYDIS_CATEGORY_CALL);
|
|
const auto& target = decoded.operands[0];
|
|
ASSERT(target.type == ZYDIS_OPERAND_TYPE_MEMORY && target.size == sizeof(uintptr_t) * CHAR_BIT);
|
|
|
|
ZydisEncoderRequest request {};
|
|
request.machine_mode = ZYDIS_MACHINE_MODE_LONG_64;
|
|
request.mnemonic = ZYDIS_MNEMONIC_MOV;
|
|
request.operand_count = 2;
|
|
request.operands[0].type = ZYDIS_OPERAND_TYPE_REGISTER;
|
|
request.operands[0].reg.value = ZYDIS_REGISTER_R11;
|
|
|
|
ZydisEncoderRequest original_request {};
|
|
if (!ZYAN_SUCCESS(ZydisEncoderDecodedInstructionToEncoderRequest(
|
|
&decoded.instruction, decoded.operands.data(),
|
|
decoded.instruction.operand_count_visible, &original_request))) {
|
|
return false;
|
|
}
|
|
request.prefixes = original_request.prefixes & ZYDIS_ATTRIB_HAS_SEGMENT;
|
|
request.address_size_hint = original_request.address_size_hint;
|
|
request.operands[1] = original_request.operands[0];
|
|
request.operands[1].mem.size = sizeof(uintptr_t);
|
|
|
|
if (target.mem.base == ZYDIS_REGISTER_RIP || target.mem.base == ZYDIS_REGISTER_EIP) {
|
|
ZyanU64 absolute_address {};
|
|
if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&decoded.instruction, &target, decoded.address,
|
|
&absolute_address))) {
|
|
return false;
|
|
}
|
|
request.operands[1].mem.displacement = static_cast<ZyanI64>(absolute_address);
|
|
}
|
|
|
|
generator.lea(rsp, ptr[rsp - GuestRedZoneSize]);
|
|
generator.push(r11);
|
|
|
|
std::array<u8, ZYDIS_MAX_INSTRUCTION_LENGTH> encoded {};
|
|
ZyanUSize encoded_size = encoded.size();
|
|
if (!ZYAN_SUCCESS(ZydisEncoderEncodeInstructionAbsolute(
|
|
&request, encoded.data(), &encoded_size,
|
|
reinterpret_cast<ZyanU64>(generator.getCurr())))) {
|
|
return false;
|
|
}
|
|
generator.db(encoded.data(), encoded_size);
|
|
|
|
generator.xchg(r11, ptr[rsp]);
|
|
generator.lea(rsp, ptr[rsp + GuestRedZoneSize + sizeof(uintptr_t)]);
|
|
generator.call(ptr[rsp - GuestRedZoneSize - sizeof(uintptr_t)]);
|
|
return true;
|
|
}
|
|
|
|
void CollectRedZoneMemoryInstructions(const DecodedFunction& function,
|
|
std::map<uintptr_t, InstructionRewrite>& rewrite_sites,
|
|
GuestInstructionPatchResult& result) {
|
|
if (!function.uses_red_zone) {
|
|
return;
|
|
}
|
|
for (const auto& [address, decoded]: function.instructions) {
|
|
if (!decoded.accesses_memory || !decoded.red_zone_live.any() ||
|
|
rewrite_sites.contains(address)) {
|
|
continue;
|
|
}
|
|
++result.memory_instruction_count;
|
|
if (decoded.instruction.length < NearJumpSize) {
|
|
++result.short_memory_instruction_count;
|
|
}
|
|
if (decoded.instruction.meta.category == ZYDIS_CATEGORY_CALL &&
|
|
decoded.operands[0].type == ZYDIS_OPERAND_TYPE_MEMORY &&
|
|
decoded.operands[0].size == sizeof(uintptr_t) * CHAR_BIT &&
|
|
!IsStackPointerRegister(decoded.operands[0].mem.base) &&
|
|
!IsStackPointerRegister(decoded.operands[0].mem.index)) {
|
|
rewrite_sites[address] = {
|
|
.protect_red_zone = true,
|
|
.protected_indirect_call = true,
|
|
};
|
|
continue;
|
|
}
|
|
if (decoded.uses_stack_pointer && !decoded.replaces_stack_pointer) {
|
|
++result.stack_dependent_memory_instruction_count;
|
|
continue;
|
|
}
|
|
if (decoded.instruction.meta.category == ZYDIS_CATEGORY_CALL ||
|
|
IsControlFlowTerminator(decoded.instruction)) {
|
|
++result.control_flow_memory_instruction_count;
|
|
continue;
|
|
}
|
|
rewrite_sites[address].protect_red_zone = true;
|
|
}
|
|
}
|
|
|
|
// Compute 1/sqrt in double precision, rounded to float, with denormals treated as
|
|
// signed zero. Preserve guest MXCSR, RFLAGS and the scratch register's full YMM.
|
|
void GenerateReciprocalSquareRoot(const DecodedCodeInstruction& decoded,
|
|
Xbyak::CodeGenerator& generator) {
|
|
const int destination = decoded.operands[0].reg.value - ZYDIS_REGISTER_XMM0;
|
|
const int source = decoded.operands[1].reg.value - ZYDIS_REGISTER_XMM0;
|
|
int scratch_index = 0;
|
|
while (scratch_index == destination || scratch_index == source) {
|
|
++scratch_index;
|
|
}
|
|
const Xbyak::Ymm scratch(scratch_index);
|
|
Xbyak::Label one;
|
|
Xbyak::Label done;
|
|
constexpr size_t SpillSize = 48;
|
|
|
|
// Guest code uses the SysV red zone on both hosts; put our spills below it.
|
|
// LEA/MOV/SIMD leave RFLAGS intact, including when the guest red zone is live.
|
|
generator.lea(rsp, ptr[rsp - GuestRedZoneSize - SpillSize]);
|
|
generator.vmovdqu(ptr[rsp], scratch);
|
|
generator.stmxcsr(ptr[rsp + 32]);
|
|
generator.mov(dword[rsp + 36], 0x1fc0); // round to nearest, exceptions masked, DAZ
|
|
generator.ldmxcsr(ptr[rsp + 36]);
|
|
generator.vcvtps2pd(scratch, Xbyak::Xmm(source));
|
|
generator.vsqrtpd(scratch, scratch);
|
|
generator.vbroadcastsd(Xbyak::Ymm(destination), ptr[rip + one]);
|
|
generator.vdivpd(scratch, Xbyak::Ymm(destination), scratch);
|
|
generator.vcvtpd2ps(Xbyak::Xmm(destination), scratch); // Clears upper YMM lanes.
|
|
generator.ldmxcsr(ptr[rsp + 32]);
|
|
generator.vmovdqu(scratch, ptr[rsp]);
|
|
generator.lea(rsp, ptr[rsp + GuestRedZoneSize + SpillSize]);
|
|
generator.jmp(done);
|
|
// A failed label reference can have an incomplete displacement. Do not bind it.
|
|
if (Xbyak::GetError() != 0) {
|
|
return;
|
|
}
|
|
generator.L(one);
|
|
generator.dq(0x3ff0000000000000ULL);
|
|
generator.L(done);
|
|
}
|
|
|
|
bool IsSupportedBitFieldInstruction(const DecodedCodeInstruction& decoded) {
|
|
const bool insert = decoded.instruction.mnemonic == ZYDIS_MNEMONIC_INSERTQ;
|
|
if (!insert && decoded.instruction.mnemonic != ZYDIS_MNEMONIC_EXTRQ) {
|
|
return false;
|
|
}
|
|
const auto is_xmm = [](const ZydisDecodedOperand& operand) {
|
|
return operand.type == ZYDIS_OPERAND_TYPE_REGISTER &&
|
|
operand.reg.value >= ZYDIS_REGISTER_XMM0 &&
|
|
operand.reg.value <= ZYDIS_REGISTER_XMM15;
|
|
};
|
|
const size_t immediate_index = insert ? 2 : 1;
|
|
const bool immediate =
|
|
decoded.instruction.operand_count_visible == immediate_index + 2 &&
|
|
decoded.operands[immediate_index].type == ZYDIS_OPERAND_TYPE_IMMEDIATE &&
|
|
decoded.operands[immediate_index + 1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE;
|
|
if (!is_xmm(decoded.operands[0]) || (insert && !is_xmm(decoded.operands[1])) ||
|
|
(!immediate &&
|
|
!(decoded.instruction.operand_count_visible == 2 && is_xmm(decoded.operands[1])))) {
|
|
return false;
|
|
}
|
|
// Match the trap handler's encoding support: 66/F2 [REX] 0F 78/79 ModRM [imm8 imm8].
|
|
const auto* code = reinterpret_cast<const u8*>(decoded.address);
|
|
const size_t opcode = (code[1] & 0xf0u) == 0x40u ? 2 : 1;
|
|
return code[0] == (insert ? 0xf2 : 0x66) && code[opcode] == 0x0f &&
|
|
code[opcode + 1] == (immediate ? 0x78 : 0x79) &&
|
|
decoded.instruction.length == opcode + (immediate ? 5 : 3);
|
|
}
|
|
|
|
void GenerateExtractQ(const DecodedCodeInstruction& decoded, Xbyak::CodeGenerator& generator) {
|
|
const Xbyak::Xmm destination(decoded.operands[0].reg.value - ZYDIS_REGISTER_XMM0);
|
|
// Keep spills below the guest red zone on both hosts, and restore all modified flags/GPRs.
|
|
generator.lea(rsp, ptr[rsp - GuestRedZoneSize]);
|
|
generator.pushfq();
|
|
generator.push(rax);
|
|
generator.push(rcx);
|
|
generator.push(rdx);
|
|
if (decoded.operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE) {
|
|
const u32 length = decoded.operands[1].imm.value.u & 63u;
|
|
const u32 index = decoded.operands[2].imm.value.u & 63u;
|
|
const u64 mask = length == 0 ? UINT64_MAX : (u64 {1} << length) - 1;
|
|
generator.movq(rax, destination);
|
|
if (index != 0) {
|
|
generator.shr(rax, index);
|
|
}
|
|
generator.mov(rdx, mask);
|
|
} else {
|
|
const Xbyak::Xmm source(decoded.operands[1].reg.value - ZYDIS_REGISTER_XMM0);
|
|
generator.movq(rax, source);
|
|
generator.mov(ecx, eax);
|
|
generator.neg(ecx);
|
|
generator.mov(rdx, UINT64_MAX);
|
|
// CL shifts use six bits: -length gives 64-length, with zero retaining all 64 bits.
|
|
generator.shr(rdx, cl);
|
|
generator.shr(rax, 8);
|
|
generator.mov(ecx, eax);
|
|
generator.movq(rax, destination);
|
|
generator.shr(rax, cl);
|
|
}
|
|
generator.and_(rax, rdx);
|
|
// Match the emulator: clear the upper XMM half, preserving the upper YMM half.
|
|
generator.movq(destination, rax);
|
|
generator.pop(rdx);
|
|
generator.pop(rcx);
|
|
generator.pop(rax);
|
|
generator.popfq();
|
|
generator.lea(rsp, ptr[rsp + GuestRedZoneSize]);
|
|
}
|
|
|
|
void CollectAmdInstructions(const DecodedFunction& function,
|
|
std::map<uintptr_t, InstructionRewrite>& rewrite_sites,
|
|
GuestInstructionPatchResult& result,
|
|
GuestInstructionHostFeatures host_features) {
|
|
for (const auto& [address, decoded]: function.instructions) {
|
|
const auto& instruction = decoded.instruction;
|
|
InstructionReplacement replacement {};
|
|
if (instruction.mnemonic == ZYDIS_MNEMONIC_VRSQRTPS &&
|
|
instruction.encoding == ZYDIS_INSTRUCTION_ENCODING_VEX &&
|
|
instruction.raw.vex.offset == 0 && decoded.operands[0].size == 128 &&
|
|
decoded.operands[1].type == ZYDIS_OPERAND_TYPE_REGISTER) {
|
|
replacement = InstructionReplacement::ReciprocalSquareRoot;
|
|
} else if (!host_features.sse4a && IsSupportedBitFieldInstruction(decoded)) {
|
|
replacement = instruction.mnemonic == ZYDIS_MNEMONIC_EXTRQ
|
|
? InstructionReplacement::ExtractQ
|
|
: InstructionReplacement::InsertQ;
|
|
} else if (!host_features.rdpid && instruction.mnemonic == ZYDIS_MNEMONIC_RDPID) {
|
|
replacement = InstructionReplacement::ReadProcessorId;
|
|
} else if (!host_features.clwb && instruction.mnemonic == ZYDIS_MNEMONIC_CLWB) {
|
|
replacement = InstructionReplacement::CacheLineWriteBack;
|
|
} else {
|
|
continue;
|
|
}
|
|
++ReplacementCounts(result, replacement).found;
|
|
rewrite_sites[address].replacement = replacement;
|
|
}
|
|
}
|
|
|
|
void MarkInstructionTrap(u8* code, const ZydisDecodedInstruction& instruction,
|
|
InstructionReplacement replacement) {
|
|
if (replacement == InstructionReplacement::ReciprocalSquareRoot) {
|
|
// Clear a reserved VEX.vvvv bit, retaining both register operands for the handler.
|
|
code[instruction.raw.vex.size - 1] &= ~0x08u;
|
|
}
|
|
// Other replacements already raise #UD on hosts without their CPU feature.
|
|
}
|
|
|
|
bool GenerateInstructionTrap(const DecodedCodeInstruction& decoded,
|
|
InstructionReplacement replacement, Xbyak::CodeGenerator& generator,
|
|
s64 stack_adjustment) {
|
|
if (replacement == InstructionReplacement::CacheLineWriteBack) {
|
|
return EncodeRelocatedInstruction(decoded, generator, stack_adjustment);
|
|
}
|
|
std::array<u8, ZYDIS_MAX_INSTRUCTION_LENGTH> code {};
|
|
std::memcpy(code.data(), reinterpret_cast<const void*>(decoded.address),
|
|
decoded.instruction.length);
|
|
MarkInstructionTrap(code.data(), decoded.instruction, replacement);
|
|
generator.db(code.data(), decoded.instruction.length);
|
|
return true;
|
|
}
|
|
|
|
bool NeedsTrapRedZoneProtection(const DecodedCodeInstruction& decoded) {
|
|
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
|
return decoded.red_zone_live.any();
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void TrapUnrelocatedInstructions(const PatchModule& module, const DecodedFunction& function,
|
|
const std::map<uintptr_t, InstructionRewrite>& rewrite_sites,
|
|
GuestInstructionPatchResult& result) {
|
|
for (const auto& [address, rewrite]: rewrite_sites) {
|
|
if (rewrite.replacement == InstructionReplacement::None ||
|
|
module.patched.contains(reinterpret_cast<u8*>(address))) {
|
|
continue;
|
|
}
|
|
const auto& decoded = function.instructions.at(address);
|
|
if (NeedsTrapRedZoneProtection(decoded)) {
|
|
if (rewrite.replacement != InstructionReplacement::ReciprocalSquareRoot) {
|
|
// Leaving an unsupported instruction unchanged still traps with a live red zone.
|
|
EXIT("AMD CPU compatibility: cannot safely trap %s at %p (guest red zone is "
|
|
"live)\n",
|
|
ZydisMnemonicGetString(decoded.instruction.mnemonic),
|
|
reinterpret_cast<void*>(address));
|
|
}
|
|
continue;
|
|
}
|
|
MarkInstructionTrap(reinterpret_cast<u8*>(address), decoded.instruction,
|
|
rewrite.replacement);
|
|
++ReplacementCounts(result, rewrite.replacement).trapped;
|
|
}
|
|
}
|
|
|
|
void RelocateGuestInstructions(PatchModule* module, const DecodedFunction& function,
|
|
const std::map<uintptr_t, InstructionRewrite>& rewrite_sites,
|
|
GuestInstructionPatchResult& result) {
|
|
struct RelocationSpan {
|
|
std::vector<const DecodedCodeInstruction*> instructions;
|
|
uintptr_t patch_start {};
|
|
uintptr_t continuation {};
|
|
size_t patch_size {};
|
|
bool trap_replacements {};
|
|
};
|
|
|
|
const auto emit_span = [&](RelocationSpan& span) -> std::optional<size_t> {
|
|
auto& generator = module->trampoline_gen;
|
|
const size_t trampoline_offset = generator.getSize();
|
|
const bool has_replacements =
|
|
std::ranges::any_of(span.instructions, [&](const auto* decoded) {
|
|
const auto rewrite = rewrite_sites.find(decoded->address);
|
|
return rewrite != rewrite_sites.end() &&
|
|
rewrite->second.replacement != InstructionReplacement::None;
|
|
});
|
|
for (bool trap_replacements: {false, true}) {
|
|
span.trap_replacements = trap_replacements;
|
|
Xbyak::ClearError();
|
|
bool encoded = true;
|
|
for (const auto* decoded: span.instructions) {
|
|
const auto rewrite = rewrite_sites.find(decoded->address);
|
|
const auto replacement = rewrite != rewrite_sites.end()
|
|
? rewrite->second.replacement
|
|
: InstructionReplacement::None;
|
|
const bool uses_trap = UsesInstructionTrap(replacement, trap_replacements);
|
|
const bool protected_indirect_call =
|
|
rewrite != rewrite_sites.end() && rewrite->second.protected_indirect_call;
|
|
const bool protect_red_zone =
|
|
(rewrite != rewrite_sites.end() && rewrite->second.protect_red_zone &&
|
|
!protected_indirect_call) ||
|
|
((uses_trap || replacement == InstructionReplacement::CacheLineWriteBack) &&
|
|
NeedsTrapRedZoneProtection(*decoded));
|
|
if (protect_red_zone) {
|
|
generator.lea(rsp, ptr[rsp - GuestRedZoneSize]);
|
|
}
|
|
if (replacement != InstructionReplacement::None) {
|
|
if (uses_trap) {
|
|
encoded = GenerateInstructionTrap(*decoded, replacement, generator,
|
|
protect_red_zone ? GuestRedZoneSize : 0);
|
|
} else if (replacement == InstructionReplacement::ReciprocalSquareRoot) {
|
|
GenerateReciprocalSquareRoot(*decoded, generator);
|
|
} else if (replacement == InstructionReplacement::ExtractQ) {
|
|
GenerateExtractQ(*decoded, generator);
|
|
} else {
|
|
// CLFLUSH writes back dirty data too; invalidation is a permitted stronger
|
|
// action.
|
|
encoded = EncodeRelocatedInstruction(
|
|
*decoded, generator, protect_red_zone ? GuestRedZoneSize : 0,
|
|
ZYDIS_MNEMONIC_CLFLUSH);
|
|
}
|
|
} else if (protected_indirect_call) {
|
|
encoded = GenerateProtectedIndirectCall(*decoded, generator);
|
|
} else {
|
|
encoded = EncodeRelocatedInstruction(*decoded, generator);
|
|
}
|
|
if (protect_red_zone && !decoded->replaces_stack_pointer) {
|
|
generator.lea(rsp, ptr[rsp + GuestRedZoneSize]);
|
|
}
|
|
if (!encoded || Xbyak::GetError() != 0) {
|
|
break;
|
|
}
|
|
}
|
|
if (encoded && Xbyak::GetError() == 0) {
|
|
generator.jmp(reinterpret_cast<void*>(span.continuation));
|
|
if (Xbyak::GetError() == 0) {
|
|
return trampoline_offset;
|
|
}
|
|
}
|
|
const int error = Xbyak::GetError();
|
|
// Roll back before retrying this uncommitted span with smaller trap replacements.
|
|
generator.reset();
|
|
generator.setSize(trampoline_offset);
|
|
if (error != 0 && !HandleTrampolineError(module, error)) {
|
|
EXIT("Guest instruction trampoline generation failed: %s\n",
|
|
Xbyak::ConvertErrorToString(error));
|
|
}
|
|
if (error != Xbyak::ERR_CODE_IS_TOO_BIG || !has_replacements) {
|
|
break;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
|
|
const auto record_rewrites = [&](const RelocationSpan& span) {
|
|
for (const auto* decoded: span.instructions) {
|
|
module->patched.insert(reinterpret_cast<u8*>(decoded->address));
|
|
const auto rewrite = rewrite_sites.find(decoded->address);
|
|
if (rewrite == rewrite_sites.end()) {
|
|
continue;
|
|
}
|
|
if (rewrite->second.protect_red_zone && decoded->accesses_memory) {
|
|
++result.patched_memory_instruction_count;
|
|
}
|
|
if (rewrite->second.replacement != InstructionReplacement::None) {
|
|
auto& counts = ReplacementCounts(result, rewrite->second.replacement);
|
|
if (UsesInstructionTrap(rewrite->second.replacement, span.trap_replacements)) {
|
|
++counts.trapped;
|
|
} else {
|
|
++counts.native;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
std::vector<std::pair<uintptr_t, uintptr_t>> patched_spans;
|
|
std::vector<uintptr_t> relay_slots;
|
|
std::vector<uintptr_t> short_relay_slots;
|
|
std::vector<uintptr_t> unresolved_sites;
|
|
uintptr_t covered_until {};
|
|
for (auto site_it = rewrite_sites.begin(); site_it != rewrite_sites.end(); ++site_it) {
|
|
const uintptr_t site = site_it->first;
|
|
if (site < covered_until) {
|
|
continue;
|
|
}
|
|
|
|
const auto collect_forward_span = [&]() -> std::optional<RelocationSpan> {
|
|
RelocationSpan span {.patch_start = site, .continuation = site};
|
|
while (span.patch_size < NearJumpSize) {
|
|
const auto decoded_it = function.instructions.find(span.continuation);
|
|
if (decoded_it == function.instructions.end() ||
|
|
(span.continuation != site &&
|
|
function.branch_targets.contains(span.continuation))) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
const auto& decoded = decoded_it->second;
|
|
span.instructions.push_back(&decoded);
|
|
span.patch_size += decoded.instruction.length;
|
|
span.continuation += decoded.instruction.length;
|
|
if (span.patch_size < NearJumpSize &&
|
|
IsControlFlowTerminator(decoded.instruction)) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
return span;
|
|
};
|
|
|
|
const auto collect_backward_span = [&]() -> std::optional<RelocationSpan> {
|
|
const auto site_instruction = function.instructions.find(site);
|
|
ASSERT(site_instruction != function.instructions.end());
|
|
RelocationSpan span {
|
|
.instructions = {&site_instruction->second},
|
|
.patch_start = site,
|
|
.continuation = site + site_instruction->second.instruction.length,
|
|
.patch_size = site_instruction->second.instruction.length,
|
|
};
|
|
|
|
while (span.patch_size < NearJumpSize) {
|
|
const auto previous_end = function.instructions.lower_bound(span.patch_start);
|
|
if (previous_end == function.instructions.begin() ||
|
|
function.branch_targets.contains(span.patch_start)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
const auto previous = std::prev(previous_end);
|
|
const auto& decoded = previous->second;
|
|
if (previous->first + decoded.instruction.length != span.patch_start ||
|
|
previous->first < covered_until ||
|
|
IsControlFlowTerminator(decoded.instruction)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
span.instructions.insert(span.instructions.begin(), &decoded);
|
|
span.patch_start = previous->first;
|
|
span.patch_size += decoded.instruction.length;
|
|
}
|
|
return span;
|
|
};
|
|
|
|
std::optional<RelocationSpan> selected_span;
|
|
std::optional<size_t> trampoline_offset;
|
|
const auto& site_instruction = function.instructions.at(site);
|
|
const bool can_relocate_neighbors = !function.has_indirect_branch;
|
|
if (can_relocate_neighbors || site_instruction.instruction.length >= NearJumpSize) {
|
|
auto forward_span = collect_forward_span();
|
|
if (forward_span) {
|
|
trampoline_offset = emit_span(*forward_span);
|
|
if (trampoline_offset) {
|
|
selected_span = std::move(forward_span);
|
|
}
|
|
}
|
|
}
|
|
if (!selected_span && can_relocate_neighbors) {
|
|
if (auto backward_span = collect_backward_span()) {
|
|
trampoline_offset = emit_span(*backward_span);
|
|
if (trampoline_offset) {
|
|
selected_span = std::move(backward_span);
|
|
}
|
|
}
|
|
}
|
|
if (!selected_span) {
|
|
unresolved_sites.push_back(site);
|
|
continue;
|
|
}
|
|
|
|
const auto& span = *selected_span;
|
|
const auto* trampoline = module->trampoline_gen.getCode() + *trampoline_offset;
|
|
|
|
auto& patch_gen = module->patch_gen;
|
|
patch_gen.reset();
|
|
patch_gen.setSize(span.patch_start - reinterpret_cast<uintptr_t>(patch_gen.getCode()));
|
|
patch_gen.jmp(trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR);
|
|
patch_gen.nop(span.patch_size - NearJumpSize);
|
|
EXIT_IF(Xbyak::GetError() != 0);
|
|
|
|
record_rewrites(span);
|
|
patched_spans.emplace_back(span.patch_start, span.continuation);
|
|
if (span.patch_size >= NearJumpSize * 2) {
|
|
relay_slots.push_back(span.patch_start + NearJumpSize);
|
|
}
|
|
if (span.patch_size >= NearJumpSize + ShortJumpSize) {
|
|
short_relay_slots.push_back(span.patch_start + NearJumpSize);
|
|
}
|
|
covered_until = span.continuation;
|
|
}
|
|
|
|
const auto record_unsupported = [&](uintptr_t site) {
|
|
const auto& decoded = function.instructions.at(site);
|
|
const auto& rewrite = rewrite_sites.at(site);
|
|
if (rewrite.protect_red_zone && decoded.accesses_memory) {
|
|
++result.unrelocatable_memory_instruction_count;
|
|
}
|
|
};
|
|
const auto overlaps_patched_span = [&patched_spans](uintptr_t start, uintptr_t end) {
|
|
return std::ranges::any_of(patched_spans, [start, end](const auto& patched) {
|
|
return start < patched.second && patched.first < end;
|
|
});
|
|
};
|
|
|
|
for (const uintptr_t site: unresolved_sites) {
|
|
if (module->patched.contains(reinterpret_cast<u8*>(site))) {
|
|
continue;
|
|
}
|
|
|
|
const auto site_instruction = function.instructions.find(site);
|
|
ASSERT(site_instruction != function.instructions.end());
|
|
if (function.has_indirect_branch ||
|
|
site_instruction->second.instruction.length < ShortJumpSize) {
|
|
record_unsupported(site);
|
|
continue;
|
|
}
|
|
|
|
RelocationSpan site_span {
|
|
.instructions = {&site_instruction->second},
|
|
.patch_start = site,
|
|
.continuation = site + site_instruction->second.instruction.length,
|
|
.patch_size = site_instruction->second.instruction.length,
|
|
};
|
|
const size_t trampoline_start = module->trampoline_gen.getSize();
|
|
const auto site_trampoline_offset = emit_span(site_span);
|
|
if (!site_trampoline_offset) {
|
|
record_unsupported(site);
|
|
continue;
|
|
}
|
|
|
|
constexpr s64 ShortJumpMin = std::numeric_limits<s8>::min();
|
|
constexpr s64 ShortJumpMax = std::numeric_limits<s8>::max();
|
|
const auto relay_slot = std::ranges::find_if(relay_slots, [site](uintptr_t address) {
|
|
const s64 displacement =
|
|
static_cast<s64>(address) - static_cast<s64>(site + ShortJumpSize);
|
|
return displacement >= ShortJumpMin && displacement <= ShortJumpMax;
|
|
});
|
|
if (relay_slot != relay_slots.end()) {
|
|
const auto* site_trampoline =
|
|
module->trampoline_gen.getCode() + *site_trampoline_offset;
|
|
|
|
auto& patch_gen = module->patch_gen;
|
|
patch_gen.reset();
|
|
patch_gen.setSize(*relay_slot - reinterpret_cast<uintptr_t>(patch_gen.getCode()));
|
|
patch_gen.jmp(site_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR);
|
|
EXIT_IF(Xbyak::GetError() != 0);
|
|
|
|
patch_gen.reset();
|
|
patch_gen.setSize(site - reinterpret_cast<uintptr_t>(patch_gen.getCode()));
|
|
patch_gen.jmp(reinterpret_cast<void*>(*relay_slot),
|
|
Xbyak::CodeGenerator::LabelType::T_SHORT);
|
|
patch_gen.nop(site_span.patch_size - ShortJumpSize);
|
|
EXIT_IF(Xbyak::GetError() != 0);
|
|
|
|
record_rewrites(site_span);
|
|
patched_spans.emplace_back(site_span.patch_start, site_span.continuation);
|
|
std::erase(short_relay_slots, *relay_slot);
|
|
relay_slots.erase(relay_slot);
|
|
continue;
|
|
}
|
|
|
|
const auto find_host = [&](uintptr_t short_jump_address) {
|
|
std::pair<std::optional<RelocationSpan>, std::optional<size_t>> result;
|
|
const s64 minimum_host = static_cast<s64>(short_jump_address) +
|
|
static_cast<s64>(ShortJumpSize) -
|
|
static_cast<s64>(NearJumpSize) + ShortJumpMin;
|
|
const s64 maximum_host = static_cast<s64>(short_jump_address) +
|
|
static_cast<s64>(ShortJumpSize) -
|
|
static_cast<s64>(NearJumpSize) + ShortJumpMax;
|
|
|
|
auto candidate = function.instructions.lower_bound(
|
|
static_cast<uintptr_t>(std::max<s64>(minimum_host, 0)));
|
|
for (; candidate != function.instructions.end() &&
|
|
static_cast<s64>(candidate->first) <= maximum_host;
|
|
++candidate) {
|
|
RelocationSpan span {.patch_start = candidate->first,
|
|
.continuation = candidate->first};
|
|
while (span.patch_size < NearJumpSize * 2) {
|
|
const auto instruction = function.instructions.find(span.continuation);
|
|
if (instruction == function.instructions.end() ||
|
|
(span.continuation != span.patch_start &&
|
|
function.branch_targets.contains(span.continuation))) {
|
|
span.instructions.clear();
|
|
break;
|
|
}
|
|
span.instructions.push_back(&instruction->second);
|
|
span.patch_size += instruction->second.instruction.length;
|
|
span.continuation += instruction->second.instruction.length;
|
|
if (span.patch_size < NearJumpSize * 2 &&
|
|
IsControlFlowTerminator(instruction->second.instruction)) {
|
|
span.instructions.clear();
|
|
break;
|
|
}
|
|
}
|
|
if (span.instructions.empty() ||
|
|
!(span.continuation <= site ||
|
|
span.patch_start >= site_span.continuation) ||
|
|
overlaps_patched_span(span.patch_start, span.continuation)) {
|
|
continue;
|
|
}
|
|
|
|
const uintptr_t relay_address = span.patch_start + NearJumpSize;
|
|
const s64 displacement = static_cast<s64>(relay_address) -
|
|
static_cast<s64>(short_jump_address + ShortJumpSize);
|
|
if (displacement < ShortJumpMin || displacement > ShortJumpMax) {
|
|
continue;
|
|
}
|
|
|
|
const auto offset = emit_span(span);
|
|
if (!offset) {
|
|
continue;
|
|
}
|
|
result.first = std::move(span);
|
|
result.second = offset;
|
|
break;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
auto [host_span, host_trampoline_offset] = find_host(site);
|
|
std::optional<uintptr_t> final_relay_slot;
|
|
uintptr_t final_short_jump = site;
|
|
std::map<uintptr_t, uintptr_t> relay_parent {{site, site}};
|
|
std::vector<uintptr_t> relay_queue {site};
|
|
for (size_t queue_index = 0;
|
|
!host_span && !final_relay_slot && queue_index < relay_queue.size();
|
|
++queue_index) {
|
|
const uintptr_t current = relay_queue[queue_index];
|
|
if (current != site) {
|
|
if (std::ranges::find(relay_slots, current) != relay_slots.end()) {
|
|
final_relay_slot = current;
|
|
final_short_jump = relay_parent.at(current);
|
|
break;
|
|
}
|
|
const auto near_slot =
|
|
std::ranges::find_if(relay_slots, [current](uintptr_t address) {
|
|
const s64 displacement = static_cast<s64>(address) -
|
|
static_cast<s64>(current + ShortJumpSize);
|
|
return address != current && displacement >= ShortJumpMin &&
|
|
displacement <= ShortJumpMax;
|
|
});
|
|
if (near_slot != relay_slots.end()) {
|
|
final_relay_slot = *near_slot;
|
|
final_short_jump = current;
|
|
break;
|
|
}
|
|
|
|
auto bridge_host = find_host(current);
|
|
if (bridge_host.first) {
|
|
host_span = std::move(bridge_host.first);
|
|
host_trampoline_offset = bridge_host.second;
|
|
final_short_jump = current;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (const uintptr_t slot: short_relay_slots) {
|
|
if (relay_parent.contains(slot)) {
|
|
continue;
|
|
}
|
|
const s64 displacement =
|
|
static_cast<s64>(slot) - static_cast<s64>(current + ShortJumpSize);
|
|
if (displacement < ShortJumpMin || displacement > ShortJumpMax) {
|
|
continue;
|
|
}
|
|
relay_parent.emplace(slot, current);
|
|
relay_queue.push_back(slot);
|
|
}
|
|
}
|
|
|
|
if (!host_span && !final_relay_slot) {
|
|
module->trampoline_gen.setSize(trampoline_start);
|
|
record_unsupported(site);
|
|
continue;
|
|
}
|
|
|
|
const auto* site_trampoline =
|
|
module->trampoline_gen.getCode() + *site_trampoline_offset;
|
|
auto& patch_gen = module->patch_gen;
|
|
uintptr_t relay_address {};
|
|
if (final_relay_slot) {
|
|
relay_address = *final_relay_slot;
|
|
patch_gen.reset();
|
|
patch_gen.setSize(relay_address - reinterpret_cast<uintptr_t>(patch_gen.getCode()));
|
|
patch_gen.jmp(site_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR);
|
|
EXIT_IF(Xbyak::GetError() != 0);
|
|
std::erase(relay_slots, relay_address);
|
|
std::erase(short_relay_slots, relay_address);
|
|
} else {
|
|
ASSERT(host_span && host_trampoline_offset);
|
|
const auto* host_trampoline =
|
|
module->trampoline_gen.getCode() + *host_trampoline_offset;
|
|
relay_address = host_span->patch_start + NearJumpSize;
|
|
|
|
patch_gen.reset();
|
|
patch_gen.setSize(host_span->patch_start -
|
|
reinterpret_cast<uintptr_t>(patch_gen.getCode()));
|
|
patch_gen.jmp(host_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR);
|
|
patch_gen.jmp(site_trampoline, Xbyak::CodeGenerator::LabelType::T_NEAR);
|
|
patch_gen.nop(host_span->patch_size - NearJumpSize * 2);
|
|
EXIT_IF(Xbyak::GetError() != 0);
|
|
}
|
|
|
|
uintptr_t jump_target = relay_address;
|
|
while (final_short_jump != site) {
|
|
patch_gen.reset();
|
|
patch_gen.setSize(final_short_jump -
|
|
reinterpret_cast<uintptr_t>(patch_gen.getCode()));
|
|
patch_gen.jmp(reinterpret_cast<void*>(jump_target),
|
|
Xbyak::CodeGenerator::LabelType::T_SHORT);
|
|
EXIT_IF(Xbyak::GetError() != 0);
|
|
jump_target = final_short_jump;
|
|
const auto parent = relay_parent.find(final_short_jump);
|
|
ASSERT(parent != relay_parent.end());
|
|
final_short_jump = parent->second;
|
|
std::erase(relay_slots, jump_target);
|
|
std::erase(short_relay_slots, jump_target);
|
|
}
|
|
|
|
patch_gen.reset();
|
|
patch_gen.setSize(site - reinterpret_cast<uintptr_t>(patch_gen.getCode()));
|
|
patch_gen.jmp(reinterpret_cast<void*>(jump_target),
|
|
Xbyak::CodeGenerator::LabelType::T_SHORT);
|
|
patch_gen.nop(site_span.patch_size - ShortJumpSize);
|
|
EXIT_IF(Xbyak::GetError() != 0);
|
|
|
|
if (host_span) {
|
|
record_rewrites(*host_span);
|
|
patched_spans.emplace_back(host_span->patch_start, host_span->continuation);
|
|
if (host_span->patch_size >= NearJumpSize * 3) {
|
|
relay_slots.push_back(host_span->patch_start + NearJumpSize * 2);
|
|
}
|
|
if (host_span->patch_size >= NearJumpSize * 2 + ShortJumpSize) {
|
|
short_relay_slots.push_back(host_span->patch_start + NearJumpSize * 2);
|
|
}
|
|
}
|
|
record_rewrites(site_span);
|
|
patched_spans.emplace_back(site_span.patch_start, site_span.continuation);
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
GuestInstructionHostFeatures GetGuestInstructionHostFeatures() {
|
|
static const auto features = [] {
|
|
const Xbyak::util::Cpu cpu;
|
|
uint32_t leaf[4] {};
|
|
Xbyak::util::Cpu::getCpuid(0, leaf);
|
|
bool rdpid = false;
|
|
if (leaf[0] >= 7) {
|
|
Xbyak::util::Cpu::getCpuidEx(7, 0, leaf);
|
|
rdpid = (leaf[2] & (1u << 22)) != 0;
|
|
}
|
|
return GuestInstructionHostFeatures {cpu.has(Xbyak::util::Cpu::tSSE4a), rdpid,
|
|
cpu.has(Xbyak::util::Cpu::tCLWB)};
|
|
}();
|
|
return features;
|
|
}
|
|
|
|
GuestInstructionPatchResult PatchGuestInstructions(u64 segment_addr, u64 segment_size,
|
|
std::span<const uintptr_t> function_starts,
|
|
bool protect_memory, bool emulate_amd,
|
|
GuestInstructionHostFeatures host_features) {
|
|
GuestInstructionPatchResult result {};
|
|
if (!protect_memory && !emulate_amd) {
|
|
return result;
|
|
}
|
|
auto* module = GetContainingModule(reinterpret_cast<void*>(segment_addr));
|
|
if (module == nullptr || function_starts.empty()) {
|
|
return result;
|
|
}
|
|
|
|
const uintptr_t segment_end = segment_addr + segment_size;
|
|
std::vector<uintptr_t> starts;
|
|
starts.reserve(function_starts.size());
|
|
for (const uintptr_t start: function_starts) {
|
|
if (start >= segment_addr && start < segment_end) {
|
|
starts.push_back(start);
|
|
}
|
|
}
|
|
std::ranges::sort(starts);
|
|
const auto unique_end = std::ranges::unique(starts).begin();
|
|
starts.erase(unique_end, starts.end());
|
|
|
|
std::unique_lock lock {module->mutex};
|
|
const size_t trampoline_begin = module->trampoline_gen.getSize();
|
|
bool analyze_red_zone = protect_memory;
|
|
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
|
|
analyze_red_zone |= emulate_amd;
|
|
#endif
|
|
for (size_t function_index = 0; function_index < starts.size(); ++function_index) {
|
|
const uintptr_t function_start = starts[function_index];
|
|
const uintptr_t function_end =
|
|
function_index + 1 < starts.size() ? starts[function_index + 1] : segment_end;
|
|
if (function_end <= function_start) {
|
|
continue;
|
|
}
|
|
|
|
++result.function_count;
|
|
auto function = DecodeFunction(function_start, function_end, segment_addr, segment_end);
|
|
if (analyze_red_zone) {
|
|
AnalyzeRedZoneLiveness(function);
|
|
}
|
|
result.instruction_count += function.instructions.size();
|
|
|
|
std::map<uintptr_t, InstructionRewrite> rewrite_sites;
|
|
|
|
if (function.uses_red_zone) {
|
|
++result.red_zone_function_count;
|
|
result.indirect_red_zone_function_count += function.has_indirect_branch;
|
|
}
|
|
if (protect_memory) {
|
|
CollectRedZoneMemoryInstructions(function, rewrite_sites, result);
|
|
}
|
|
if (emulate_amd) {
|
|
CollectAmdInstructions(function, rewrite_sites, result, host_features);
|
|
}
|
|
if (!rewrite_sites.empty()) {
|
|
RelocateGuestInstructions(module, function, rewrite_sites, result);
|
|
TrapUnrelocatedInstructions(*module, function, rewrite_sites, result);
|
|
}
|
|
}
|
|
const auto trampoline_addr =
|
|
reinterpret_cast<u64>(module->trampoline_gen.getCode()) + trampoline_begin;
|
|
const auto trampoline_size = module->trampoline_gen.getSize() - trampoline_begin;
|
|
Common::VirtualMemory::FlushInstructionCache(segment_addr, segment_size);
|
|
if (trampoline_size != 0) {
|
|
Common::VirtualMemory::FlushInstructionCache(trampoline_addr, trampoline_size);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
namespace {
|
|
|
|
constexpr uint8_t DW_EH_PE_FORMAT_MASK = 0x0f;
|
|
constexpr uint8_t DW_EH_PE_APPLICATION_MASK = 0x70;
|
|
constexpr uint8_t DW_EH_PE_PCREL = 0x10;
|
|
constexpr uint8_t DW_EH_PE_DATAREL = 0x30;
|
|
constexpr uint8_t DW_EH_PE_INDIRECT = 0x80;
|
|
constexpr uint8_t DW_EH_PE_OMIT = 0xff;
|
|
|
|
template <typename T>
|
|
bool ReadEhValue(const uint8_t** cursor, const uint8_t* end, T* value) {
|
|
if (cursor == nullptr || *cursor == nullptr || value == nullptr ||
|
|
static_cast<size_t>(end - *cursor) < sizeof(T)) {
|
|
return false;
|
|
}
|
|
std::memcpy(value, *cursor, sizeof(T));
|
|
*cursor += sizeof(T);
|
|
return true;
|
|
}
|
|
|
|
bool ReadEncodedEhPointer(const uint8_t** cursor, const uint8_t* end, uint8_t encoding,
|
|
uintptr_t datarel_base, uintptr_t* value) {
|
|
if (cursor == nullptr || *cursor == nullptr || value == nullptr || encoding == DW_EH_PE_OMIT) {
|
|
return false;
|
|
}
|
|
|
|
const auto field_address = reinterpret_cast<uintptr_t>(*cursor);
|
|
uint64_t raw = 0;
|
|
bool is_signed = false;
|
|
switch (encoding & DW_EH_PE_FORMAT_MASK) {
|
|
case 0x00: {
|
|
uintptr_t decoded = 0;
|
|
if (!ReadEhValue(cursor, end, &decoded)) {
|
|
return false;
|
|
}
|
|
raw = decoded;
|
|
break;
|
|
}
|
|
case 0x02: {
|
|
uint16_t decoded = 0;
|
|
if (!ReadEhValue(cursor, end, &decoded)) {
|
|
return false;
|
|
}
|
|
raw = decoded;
|
|
break;
|
|
}
|
|
case 0x03: {
|
|
uint32_t decoded = 0;
|
|
if (!ReadEhValue(cursor, end, &decoded)) {
|
|
return false;
|
|
}
|
|
raw = decoded;
|
|
break;
|
|
}
|
|
case 0x04:
|
|
if (!ReadEhValue(cursor, end, &raw)) {
|
|
return false;
|
|
}
|
|
break;
|
|
case 0x0a: {
|
|
int16_t decoded = 0;
|
|
if (!ReadEhValue(cursor, end, &decoded)) {
|
|
return false;
|
|
}
|
|
raw = static_cast<uint64_t>(static_cast<int64_t>(decoded));
|
|
is_signed = true;
|
|
break;
|
|
}
|
|
case 0x0b: {
|
|
int32_t decoded = 0;
|
|
if (!ReadEhValue(cursor, end, &decoded)) {
|
|
return false;
|
|
}
|
|
raw = static_cast<uint64_t>(static_cast<int64_t>(decoded));
|
|
is_signed = true;
|
|
break;
|
|
}
|
|
case 0x0c: {
|
|
int64_t decoded = 0;
|
|
if (!ReadEhValue(cursor, end, &decoded)) {
|
|
return false;
|
|
}
|
|
raw = static_cast<uint64_t>(decoded);
|
|
is_signed = true;
|
|
break;
|
|
}
|
|
default: return false;
|
|
}
|
|
|
|
uint64_t base = 0;
|
|
switch (encoding & DW_EH_PE_APPLICATION_MASK) {
|
|
case 0x00: break;
|
|
case DW_EH_PE_PCREL: base = field_address; break;
|
|
case DW_EH_PE_DATAREL: base = datarel_base; break;
|
|
default: return false;
|
|
}
|
|
|
|
uint64_t decoded = 0;
|
|
if (is_signed) {
|
|
decoded = static_cast<uint64_t>(static_cast<int64_t>(base) + static_cast<int64_t>(raw));
|
|
} else {
|
|
if (raw > UINT64_MAX - base) {
|
|
return false;
|
|
}
|
|
decoded = base + raw;
|
|
}
|
|
if ((encoding & DW_EH_PE_INDIRECT) != 0) {
|
|
const auto* indirect = reinterpret_cast<const uint8_t*>(decoded);
|
|
uintptr_t target = 0;
|
|
std::memcpy(&target, indirect, sizeof(target));
|
|
decoded = target;
|
|
}
|
|
*value = static_cast<uintptr_t>(decoded);
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_frame_header_size,
|
|
std::vector<uintptr_t>* function_starts) {
|
|
if (function_starts == nullptr) {
|
|
return false;
|
|
}
|
|
function_starts->clear();
|
|
if (eh_frame_header_addr == 0 || eh_frame_header_size < 4 ||
|
|
eh_frame_header_size > UINTPTR_MAX - eh_frame_header_addr) {
|
|
return false;
|
|
}
|
|
|
|
const auto* start = reinterpret_cast<const uint8_t*>(eh_frame_header_addr);
|
|
const auto* end = start + eh_frame_header_size;
|
|
const auto* cursor = start;
|
|
uint8_t version = 0;
|
|
uint8_t eh_frame_encoding = 0;
|
|
uint8_t count_encoding = 0;
|
|
uint8_t table_encoding = 0;
|
|
if (!ReadEhValue(&cursor, end, &version) || !ReadEhValue(&cursor, end, &eh_frame_encoding) ||
|
|
!ReadEhValue(&cursor, end, &count_encoding) ||
|
|
!ReadEhValue(&cursor, end, &table_encoding) || version != 1) {
|
|
return false;
|
|
}
|
|
|
|
uintptr_t ignored_eh_frame = 0;
|
|
if (!ReadEncodedEhPointer(&cursor, end, eh_frame_encoding, eh_frame_header_addr,
|
|
&ignored_eh_frame)) {
|
|
return false;
|
|
}
|
|
if (count_encoding == DW_EH_PE_OMIT || table_encoding == DW_EH_PE_OMIT) {
|
|
return true;
|
|
}
|
|
|
|
uintptr_t fde_count = 0;
|
|
if (!ReadEncodedEhPointer(&cursor, end, count_encoding, eh_frame_header_addr, &fde_count)) {
|
|
return false;
|
|
}
|
|
if (fde_count > eh_frame_header_size / 2u) {
|
|
return false;
|
|
}
|
|
|
|
function_starts->reserve(static_cast<size_t>(fde_count));
|
|
for (uintptr_t index = 0; index < fde_count; ++index) {
|
|
uintptr_t function_start = 0;
|
|
uintptr_t ignored_fde = 0;
|
|
if (!ReadEncodedEhPointer(&cursor, end, table_encoding, eh_frame_header_addr,
|
|
&function_start) ||
|
|
!ReadEncodedEhPointer(&cursor, end, table_encoding, eh_frame_header_addr,
|
|
&ignored_fde)) {
|
|
function_starts->clear();
|
|
return false;
|
|
}
|
|
function_starts->push_back(function_start);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void RegisterGuestInstructionPatchModule(void* module_ptr, uint64_t module_size,
|
|
void* trampoline_area_ptr, uint64_t trampoline_area_size) {
|
|
EXIT_IF(module_ptr == nullptr || module_size == 0 || trampoline_area_ptr == nullptr ||
|
|
trampoline_area_size == 0);
|
|
const auto module_addr = reinterpret_cast<u64>(module_ptr);
|
|
g_patch_modules.erase(module_addr);
|
|
g_patch_modules.emplace(std::piecewise_construct, std::forward_as_tuple(module_addr),
|
|
std::forward_as_tuple(static_cast<u8*>(module_ptr), module_size,
|
|
static_cast<u8*>(trampoline_area_ptr),
|
|
trampoline_area_size));
|
|
}
|
|
|
|
void UnregisterGuestInstructionPatchModule(void* module_ptr) {
|
|
g_patch_modules.erase(reinterpret_cast<u64>(module_ptr));
|
|
}
|
|
|
|
#undef ASSERT
|
|
|
|
} // namespace Loader
|