From a4d26cf81abb38b8fd541482bc66d134ad135c4b Mon Sep 17 00:00:00 2001 From: Ewen trividic <268282751+ew-sudo@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:40:51 +0200 Subject: [PATCH] loader: replace SSE4a EXTRQ with native trampolines on hosts without SSE4a (#839) loader: add native EXTRQ patches with trap fallback Co-authored-by: nmzik --- src/loader/guestInstructionPatcher.cpp | 203 ++++++++--- src/loader/guestInstructionPatcher.h | 42 ++- src/loader/runtimeLinker.cpp | 41 ++- tests/VirtualMemoryAllocationTests.cpp | 451 +++++++++++++++++++++---- 4 files changed, 585 insertions(+), 152 deletions(-) diff --git a/src/loader/guestInstructionPatcher.cpp b/src/loader/guestInstructionPatcher.cpp index 87be0f97d..f70265eda 100644 --- a/src/loader/guestInstructionPatcher.cpp +++ b/src/loader/guestInstructionPatcher.cpp @@ -132,12 +132,21 @@ struct DecodedFunction { bool requires_conservative_red_zone_tracking {}; }; +enum class InstructionReplacement { None, ReciprocalSquareRoot, ExtractQ }; + struct InstructionRewrite { - bool protect_red_zone {}; - bool protected_indirect_call {}; - bool emulate_rsqrt {}; + bool protect_red_zone {}; + bool protected_indirect_call {}; + InstructionReplacement replacement {}; }; +InstructionPatchCounts& ReplacementCounts(GuestInstructionPatchResult& result, + InstructionReplacement replacement) { + ASSERT(replacement != InstructionReplacement::None); + return replacement == InstructionReplacement::ReciprocalSquareRoot ? result.reciprocal_sqrt + : result.extrq; +} + bool IsStackPointerRegister(ZydisRegister reg) { return reg != ZYDIS_REGISTER_NONE && ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, reg) == ZYDIS_REGISTER_RSP; @@ -814,33 +823,108 @@ void GenerateReciprocalSquareRoot(const DecodedCodeInstruction& decoded, generator.L(done); } -void CollectReciprocalSquareRoots(const DecodedFunction& function, - std::map& rewrite_sites, - GuestInstructionPatchResult& result) { +bool IsSupportedExtractQ(const DecodedCodeInstruction& decoded) { + if (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 bool immediate = decoded.instruction.operand_count_visible == 3 && + decoded.operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE && + decoded.operands[2].type == ZYDIS_OPERAND_TYPE_IMMEDIATE; + if (!is_xmm(decoded.operands[0]) || + (!immediate && + !(decoded.instruction.operand_count_visible == 2 && is_xmm(decoded.operands[1])))) { + return false; + } + // Match the trap handler's encoding support: 66 [REX] 0F 78/79 ModRM [imm8 imm8]. + const auto* code = reinterpret_cast(decoded.address); + const size_t opcode = (code[1] & 0xf0u) == 0x40u ? 2 : 1; + return code[0] == 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& rewrite_sites, + GuestInstructionPatchResult& result) { + static const bool host_lacks_sse4a = !Xbyak::util::Cpu().has(Xbyak::util::Cpu::tSSE4a); for (const auto& [address, decoded]: function.instructions) { - const auto& instruction = decoded.instruction; - 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) { + 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_lacks_sse4a && IsSupportedExtractQ(decoded)) { + replacement = InstructionReplacement::ExtractQ; + } else { continue; } - ++result.reciprocal_sqrt_instruction_count; - rewrite_sites[address].emulate_rsqrt = true; + ++ReplacementCounts(result, replacement).found; + rewrite_sites[address].replacement = replacement; } } -void MarkReciprocalSquareRootTrap(u8* code, const ZydisDecodedInstruction& instruction) { - // Clear a reserved VEX.vvvv bit, retaining both register operands for the handler. - code[instruction.raw.vex.size - 1] &= ~0x08u; +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; + } + // EXTRQ already raises #UD on hosts without SSE4a; keep its original encoding. } -void GenerateReciprocalSquareRootTrap(const DecodedCodeInstruction& decoded, - Xbyak::CodeGenerator& generator) { +void GenerateInstructionTrap(const DecodedCodeInstruction& decoded, + InstructionReplacement replacement, Xbyak::CodeGenerator& generator) { std::array code {}; std::memcpy(code.data(), reinterpret_cast(decoded.address), decoded.instruction.length); - MarkReciprocalSquareRootTrap(code.data(), decoded.instruction); + MarkInstructionTrap(code.data(), decoded.instruction, replacement); generator.db(code.data(), decoded.instruction.length); } @@ -852,20 +936,28 @@ bool NeedsTrapRedZoneProtection(const DecodedCodeInstruction& decoded) { #endif } -void TrapUnrelocatedReciprocalSquareRoots( - const PatchModule& module, const DecodedFunction& function, - const std::map& rewrite_sites, - GuestInstructionPatchResult& result) { +void TrapUnrelocatedInstructions(const PatchModule& module, const DecodedFunction& function, + const std::map& rewrite_sites, + GuestInstructionPatchResult& result) { for (const auto& [address, rewrite]: rewrite_sites) { - if (!rewrite.emulate_rsqrt || module.patched.contains(reinterpret_cast(address))) { + if (rewrite.replacement == InstructionReplacement::None || + module.patched.contains(reinterpret_cast(address))) { continue; } const auto& decoded = function.instructions.at(address); if (NeedsTrapRedZoneProtection(decoded)) { + if (rewrite.replacement == InstructionReplacement::ExtractQ) { + // Unlike VRSQRTPS, leaving EXTRQ unchanged would still trap and corrupt the red + // zone. + EXIT("AMD CPU compatibility: cannot safely trap EXTRQ at %p (guest red zone is " + "live)\n", + reinterpret_cast(address)); + } continue; } - MarkReciprocalSquareRootTrap(reinterpret_cast(address), decoded.instruction); - ++result.trapped_reciprocal_sqrt_instruction_count; + MarkInstructionTrap(reinterpret_cast(address), decoded.instruction, + rewrite.replacement); + ++ReplacementCounts(result, rewrite.replacement).trapped; } } @@ -877,38 +969,44 @@ void RelocateGuestInstructions(PatchModule* module, const DecodedFunction& funct uintptr_t patch_start {}; uintptr_t continuation {}; size_t patch_size {}; - bool trap_rsqrt {}; + bool trap_replacements {}; }; const auto emit_span = [&](RelocationSpan& span) -> std::optional { auto& generator = module->trampoline_gen; const size_t trampoline_offset = generator.getSize(); - const bool has_rsqrt = std::ranges::any_of(span.instructions, [&](const auto* decoded) { - const auto rewrite = rewrite_sites.find(decoded->address); - return rewrite != rewrite_sites.end() && rewrite->second.emulate_rsqrt; - }); - for (bool trap_rsqrt: {false, true}) { - span.trap_rsqrt = trap_rsqrt; + 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 bool emulate_rsqrt = - rewrite != rewrite_sites.end() && rewrite->second.emulate_rsqrt; + const auto rewrite = rewrite_sites.find(decoded->address); + const auto replacement = rewrite != rewrite_sites.end() + ? rewrite->second.replacement + : InstructionReplacement::None; 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) || - (emulate_rsqrt && trap_rsqrt && NeedsTrapRedZoneProtection(*decoded)); + (replacement != InstructionReplacement::None && trap_replacements && + NeedsTrapRedZoneProtection(*decoded)); if (protect_red_zone) { generator.lea(rsp, ptr[rsp - GuestRedZoneSize]); } - if (emulate_rsqrt) { - if (trap_rsqrt) { - GenerateReciprocalSquareRootTrap(*decoded, generator); - } else { + if (replacement != InstructionReplacement::None) { + if (trap_replacements) { + GenerateInstructionTrap(*decoded, replacement, generator); + } else if (replacement == InstructionReplacement::ReciprocalSquareRoot) { GenerateReciprocalSquareRoot(*decoded, generator); + } else { + GenerateExtractQ(*decoded, generator); } } else if (protected_indirect_call) { encoded = GenerateProtectedIndirectCall(*decoded, generator); @@ -936,7 +1034,7 @@ void RelocateGuestInstructions(PatchModule* module, const DecodedFunction& funct EXIT("Guest instruction trampoline generation failed: %s\n", Xbyak::ConvertErrorToString(error)); } - if (error != Xbyak::ERR_CODE_IS_TOO_BIG || !has_rsqrt) { + if (error != Xbyak::ERR_CODE_IS_TOO_BIG || !has_replacements) { break; } } @@ -953,11 +1051,12 @@ void RelocateGuestInstructions(PatchModule* module, const DecodedFunction& funct if (rewrite->second.protect_red_zone && decoded->accesses_memory) { ++result.patched_memory_instruction_count; } - if (rewrite->second.emulate_rsqrt) { - if (span.trap_rsqrt) { - ++result.trapped_reciprocal_sqrt_instruction_count; + if (rewrite->second.replacement != InstructionReplacement::None) { + auto& counts = ReplacementCounts(result, rewrite->second.replacement); + if (span.trap_replacements) { + ++counts.trapped; } else { - ++result.patched_reciprocal_sqrt_instruction_count; + ++counts.native; } } } @@ -1328,9 +1427,9 @@ void RelocateGuestInstructions(PatchModule* module, const DecodedFunction& funct GuestInstructionPatchResult PatchGuestInstructions(u64 segment_addr, u64 segment_size, std::span function_starts, - bool protect_memory, bool emulate_rsqrt) { + bool protect_memory, bool emulate_amd) { GuestInstructionPatchResult result {}; - if (!protect_memory && !emulate_rsqrt) { + if (!protect_memory && !emulate_amd) { return result; } auto* module = GetContainingModule(reinterpret_cast(segment_addr)); @@ -1354,7 +1453,7 @@ GuestInstructionPatchResult PatchGuestInstructions(u64 segment_addr, u64 segment 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_rsqrt; + 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]; @@ -1380,12 +1479,12 @@ GuestInstructionPatchResult PatchGuestInstructions(u64 segment_addr, u64 segment if (protect_memory) { CollectRedZoneMemoryInstructions(function, rewrite_sites, result); } - if (emulate_rsqrt) { - CollectReciprocalSquareRoots(function, rewrite_sites, result); + if (emulate_amd) { + CollectAmdInstructions(function, rewrite_sites, result); } if (!rewrite_sites.empty()) { RelocateGuestInstructions(module, function, rewrite_sites, result); - TrapUnrelocatedReciprocalSquareRoots(*module, function, rewrite_sites, result); + TrapUnrelocatedInstructions(*module, function, rewrite_sites, result); } } const auto trampoline_addr = diff --git a/src/loader/guestInstructionPatcher.h b/src/loader/guestInstructionPatcher.h index b09031c56..68cf5b49e 100644 --- a/src/loader/guestInstructionPatcher.h +++ b/src/loader/guestInstructionPatcher.h @@ -11,20 +11,34 @@ namespace Loader { +struct InstructionPatchCounts { + uint64_t found = 0; + uint64_t native = 0; + uint64_t trapped = 0; + + uint64_t Skipped() const { return found - native - trapped; } + + InstructionPatchCounts& operator+=(const InstructionPatchCounts& other) { + found += other.found; + native += other.native; + trapped += other.trapped; + return *this; + } +}; + struct GuestInstructionPatchResult { - uint64_t function_count = 0; - uint64_t instruction_count = 0; - uint64_t red_zone_function_count = 0; - uint64_t memory_instruction_count = 0; - uint64_t short_memory_instruction_count = 0; - uint64_t patched_memory_instruction_count = 0; - uint64_t stack_dependent_memory_instruction_count = 0; - uint64_t control_flow_memory_instruction_count = 0; - uint64_t unrelocatable_memory_instruction_count = 0; - uint64_t indirect_red_zone_function_count = 0; - uint64_t reciprocal_sqrt_instruction_count = 0; - uint64_t patched_reciprocal_sqrt_instruction_count = 0; - uint64_t trapped_reciprocal_sqrt_instruction_count = 0; + uint64_t function_count = 0; + uint64_t instruction_count = 0; + uint64_t red_zone_function_count = 0; + uint64_t memory_instruction_count = 0; + uint64_t short_memory_instruction_count = 0; + uint64_t patched_memory_instruction_count = 0; + uint64_t stack_dependent_memory_instruction_count = 0; + uint64_t control_flow_memory_instruction_count = 0; + uint64_t unrelocatable_memory_instruction_count = 0; + uint64_t indirect_red_zone_function_count = 0; + InstructionPatchCounts reciprocal_sqrt; + InstructionPatchCounts extrq; }; void RegisterGuestInstructionPatchModule(void* module_ptr, uint64_t module_size, @@ -34,7 +48,7 @@ void UnregisterGuestInstructionPatchModule(void* module_ptr); // Apply enabled instruction fixes using native trampolines or safe trap fallbacks. GuestInstructionPatchResult PatchGuestInstructions(uint64_t segment_addr, uint64_t segment_size, std::span function_starts, - bool protect_memory, bool emulate_rsqrt); + bool protect_memory, bool emulate_amd); bool DecodeEhFrameFunctionStarts(uint64_t eh_frame_header_addr, uint64_t eh_frame_header_size, std::vector* function_starts); diff --git a/src/loader/runtimeLinker.cpp b/src/loader/runtimeLinker.cpp index f34b213f2..fcaef06ce 100644 --- a/src/loader/runtimeLinker.cpp +++ b/src/loader/runtimeLinker.cpp @@ -1813,16 +1813,16 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) { EXIT_IF(tls_handler_size > UINT64_MAX - program->base_size_aligned); program->mapped_size = program->base_size_aligned + tls_handler_size; #if !defined(__APPLE__) - const bool emulate_rsqrt = Config::AmdCpuEnabled(); + const bool emulate_amd = Config::AmdCpuEnabled(); #else - const bool emulate_rsqrt = false; + const bool emulate_amd = false; #endif #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS const bool protect_memory_faults = Config::RedZoneProtectionEnabled(); #else const bool protect_memory_faults = false; #endif - const bool patch_guest_instructions = protect_memory_faults || emulate_rsqrt; + const bool patch_guest_instructions = protect_memory_faults || emulate_amd; constexpr uint64_t INSTRUCTION_TRAMPOLINE_SIZE = 8u * 1024u * 1024u; if (patch_guest_instructions) { @@ -1940,18 +1940,15 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) { if (!have_function_starts) { Log::WriteToConsoleAndLog( fmt::format("{}: {} not patched (function boundaries unavailable)\n", - emulate_rsqrt ? "AMD CPU compatibility" : "Guest red-zone protection", + emulate_amd ? "AMD CPU compatibility" : "Guest red-zone protection", module_name)); } GuestInstructionPatchResult totals {}; for (const auto& [segment_addr, segment_size]: executable_segments) { const auto result = PatchGuestInstructions(segment_addr, segment_size, function_starts, - protect_memory_faults, emulate_rsqrt); - totals.reciprocal_sqrt_instruction_count += result.reciprocal_sqrt_instruction_count; - totals.patched_reciprocal_sqrt_instruction_count += - result.patched_reciprocal_sqrt_instruction_count; - totals.trapped_reciprocal_sqrt_instruction_count += - result.trapped_reciprocal_sqrt_instruction_count; + protect_memory_faults, emulate_amd); + totals.reciprocal_sqrt += result.reciprocal_sqrt; + totals.extrq += result.extrq; if (protect_memory_faults) { LOGF("Windows guest red-zone patching: %s, functions=%" PRIu64 ", red_zone=%" PRIu64 ", memory=%" PRIu64 ", patched=%" PRIu64 ", short=%" PRIu64 ", stack=%" PRIu64 @@ -1965,19 +1962,19 @@ void RuntimeLinker::LoadProgramToMemory(Program* program) { result.unrelocatable_memory_instruction_count); } } - if (emulate_rsqrt && have_function_starts) { - const auto patched = totals.patched_reciprocal_sqrt_instruction_count + - totals.trapped_reciprocal_sqrt_instruction_count; - const auto skipped = totals.reciprocal_sqrt_instruction_count - patched; - const char* status = totals.reciprocal_sqrt_instruction_count == 0 - ? "no matching instructions" - : patched == 0 ? "not patched" - : skipped != 0 ? "partially patched" - : "patched"; + if (emulate_amd && have_function_starts) { + const auto found = totals.reciprocal_sqrt.found + totals.extrq.found; + const auto skipped = totals.reciprocal_sqrt.Skipped() + totals.extrq.Skipped(); + const char* status = found == 0 ? "no matching instructions" + : skipped == found ? "not patched" + : skipped != 0 ? "partially patched" + : "patched"; Log::WriteToConsoleAndLog(fmt::format( - "AMD CPU compatibility: {} {} (VRSQRTPS: native={}, trapped={}, skipped={})\n", - module_name, status, totals.patched_reciprocal_sqrt_instruction_count, - totals.trapped_reciprocal_sqrt_instruction_count, skipped)); + "AMD CPU compatibility: {} {} (VRSQRTPS: native={}, trapped={}, skipped={}; " + "EXTRQ: native={}, trapped={}, skipped={})\n", + module_name, status, totals.reciprocal_sqrt.native, totals.reciprocal_sqrt.trapped, + totals.reciprocal_sqrt.Skipped(), totals.extrq.native, totals.extrq.trapped, + totals.extrq.Skipped())); } } diff --git a/tests/VirtualMemoryAllocationTests.cpp b/tests/VirtualMemoryAllocationTests.cpp index ef7db3f73..237d71b0b 100644 --- a/tests/VirtualMemoryAllocationTests.cpp +++ b/tests/VirtualMemoryAllocationTests.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,7 @@ #include #include #include +#include #endif #if defined(__linux__) @@ -2827,9 +2829,9 @@ void TestModuleRelocationUsesWritableHostMapping() { } #if defined(__linux__) || KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS -volatile sig_atomic_t g_rsqrt_traps = 0; +volatile sig_atomic_t g_instruction_traps = 0; -bool EmulateReciprocalSquareRootContext(void* context) { +bool EmulateInstructionContext(void* context) { const auto host_mxcsr = _mm_getcsr(); _mm_setcsr(0x7fe1); // Exercise the handler under a distinct rounding mode and raised flags. const bool emulated = Loader::X64InstructionEmulator::TryEmulate(context); @@ -2838,26 +2840,66 @@ bool EmulateReciprocalSquareRootContext(void* context) { if (!emulated || !preserved_mxcsr) { return false; } - g_rsqrt_traps = g_rsqrt_traps + 1; + g_instruction_traps = g_instruction_traps + 1; return true; } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS -LONG CALLBACK ReciprocalSquareRootHandler(EXCEPTION_POINTERS* exception) { +LONG CALLBACK InstructionHandler(EXCEPTION_POINTERS* exception) { if (exception->ExceptionRecord->ExceptionCode != EXCEPTION_ILLEGAL_INSTRUCTION || - !EmulateReciprocalSquareRootContext(exception->ContextRecord)) { + !EmulateInstructionContext(exception->ContextRecord)) { return EXCEPTION_CONTINUE_SEARCH; } return EXCEPTION_CONTINUE_EXECUTION; } #else -void ReciprocalSquareRootHandler(int, siginfo_t*, void* context) { - if (!EmulateReciprocalSquareRootContext(context)) { +void InstructionHandler(int, siginfo_t*, void* context) { + if (!EmulateInstructionContext(context)) { _exit(190); } } #endif +struct InstructionTestScope { + uint64_t mapping; + uint64_t size; + uint32_t mxcsr = _mm_getcsr(); +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + void* handler = nullptr; +#else + struct sigaction previous {}; + bool installed = false; +#endif + ~InstructionTestScope() { + _mm_setcsr(mxcsr); +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + if (handler != nullptr) { + RemoveVectoredExceptionHandler(handler); + } +#else + if (installed) { + sigaction(SIGILL, &previous, nullptr); + } +#endif + Loader::UnregisterGuestInstructionPatchModule(reinterpret_cast(mapping)); + Libs::LibKernel::Memory::FreeGuestMemory(mapping, size); + } + + void InstallHandler(const char* test) { +#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + handler = AddVectoredExceptionHandler(1, InstructionHandler); + Check(test, handler != nullptr, "failed to install instruction handler"); +#else + struct sigaction action {}; + action.sa_sigaction = InstructionHandler; + action.sa_flags = SA_SIGINFO; + sigemptyset(&action.sa_mask); + installed = sigaction(SIGILL, &action, &previous) == 0; + Check(test, installed, "failed to install instruction handler"); +#endif + } +}; + void TestPackedReciprocalSquareRoot() { const char* test = "PackedReciprocalSquareRoot"; constexpr uint64_t code_size = 0x4000; @@ -2865,42 +2907,8 @@ void TestPackedReciprocalSquareRoot() { const auto mapping = Libs::LibKernel::Memory::AllocateProgramMemory( 0x902000000, allocation_size, Common::VirtualMemory::Mode::ExecuteReadWrite, "rsqrt_test"); Check(test, mapping != 0, "failed to allocate instruction test code"); - struct RestoreState { - uint64_t mapping; - uint64_t size; - uint32_t mxcsr; -#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS - void* handler = nullptr; -#else - struct sigaction previous {}; - bool installed = false; -#endif - ~RestoreState() { - _mm_setcsr(mxcsr); -#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS - if (handler != nullptr) { - RemoveVectoredExceptionHandler(handler); - } -#else - if (installed) { - sigaction(SIGILL, &previous, nullptr); - } -#endif - Loader::UnregisterGuestInstructionPatchModule(reinterpret_cast(mapping)); - Libs::LibKernel::Memory::FreeGuestMemory(mapping, size); - } - } restore {mapping, allocation_size, _mm_getcsr()}; -#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS - restore.handler = AddVectoredExceptionHandler(1, ReciprocalSquareRootHandler); - Check(test, restore.handler != nullptr, "failed to install instruction handler"); -#else - struct sigaction action {}; - action.sa_sigaction = ReciprocalSquareRootHandler; - action.sa_flags = SA_SIGINFO; - sigemptyset(&action.sa_mask); - restore.installed = sigaction(SIGILL, &action, &restore.previous) == 0; - Check(test, restore.installed, "failed to install instruction handler"); -#endif + InstructionTestScope restore {mapping, allocation_size}; + restore.InstallHandler(test); using GuestFunction = void(KYTY_SYSV_ABI*)(const uint32_t*, uint32_t*); const auto function = reinterpret_cast(mapping); const std::array function_starts {mapping}; @@ -2987,9 +2995,9 @@ void TestPackedReciprocalSquareRoot() { code.getCode() + allocation_size); const auto disabled = Loader::PatchGuestInstructions( mapping, code.getSize(), function_starts, false, false); - Check(test, Xbyak::GetError() == 0 && disabled.reciprocal_sqrt_instruction_count == 0 && - disabled.patched_reciprocal_sqrt_instruction_count == 0 && - disabled.trapped_reciprocal_sqrt_instruction_count == 0 && + Check(test, Xbyak::GetError() == 0 && disabled.reciprocal_sqrt.found == 0 && + disabled.reciprocal_sqrt.native == 0 && + disabled.reciprocal_sqrt.trapped == 0 && std::equal(original.begin(), original.end(), code.getCode()) && std::equal(original_trampoline.begin(), original_trampoline.end(), code.getCode() + code_size), @@ -3005,9 +3013,9 @@ void TestPackedReciprocalSquareRoot() { #else const uint64_t expected_traps = instruction_count; #endif - Check(test, exhausted.reciprocal_sqrt_instruction_count == instruction_count && - exhausted.patched_reciprocal_sqrt_instruction_count == 0 && - exhausted.trapped_reciprocal_sqrt_instruction_count == expected_traps, + Check(test, exhausted.reciprocal_sqrt.found == instruction_count && + exhausted.reciprocal_sqrt.native == 0 && + exhausted.reciprocal_sqrt.trapped == expected_traps, "exhausted reciprocal-root generation lost fallback coverage"); Check(test, Xbyak::GetError() == 0, "failed trampoline generation leaked Xbyak's thread-local error"); @@ -3015,9 +3023,9 @@ void TestPackedReciprocalSquareRoot() { Check(test, std::equal(original.begin(), original.end(), code.getCode()), "unsafe fallback modified a live guest red zone site"); } - const auto traps_before = g_rsqrt_traps; + const auto traps_before = g_instruction_traps; function(input.data(), output.data()); - Check(test, static_cast(g_rsqrt_traps - traps_before) == expected_traps && + Check(test, static_cast(g_instruction_traps - traps_before) == expected_traps && red_zone_intact() && output[0] == (expected_traps ? expected : std::bit_cast(native)), "exhaustion fallback changed guest state or failed to execute"); @@ -3031,9 +3039,9 @@ void TestPackedReciprocalSquareRoot() { const auto memory_only = Loader::PatchGuestInstructions( mapping, code.getSize(), function_starts, true, false); Check(test, Xbyak::GetError() == 0 && - memory_only.reciprocal_sqrt_instruction_count == 0 && - memory_only.patched_reciprocal_sqrt_instruction_count == 0 && - memory_only.trapped_reciprocal_sqrt_instruction_count == 0 && + memory_only.reciprocal_sqrt.found == 0 && + memory_only.reciprocal_sqrt.native == 0 && + memory_only.reciprocal_sqrt.trapped == 0 && memory_only.patched_memory_instruction_count > 0, "memory protection enabled reciprocal-root replacement"); function(input.data(), output.data()); @@ -3051,18 +3059,18 @@ void TestPackedReciprocalSquareRoot() { const auto patched = Loader::PatchGuestInstructions( mapping, code.getSize(), function_starts, protect_memory_sites, true); Check(test, Xbyak::GetError() == 0 && - patched.reciprocal_sqrt_instruction_count == instruction_count && - patched.patched_reciprocal_sqrt_instruction_count == + patched.reciprocal_sqrt.found == instruction_count && + patched.reciprocal_sqrt.native == (trap_fallback ? 0 : instruction_count) && - patched.trapped_reciprocal_sqrt_instruction_count == + patched.reciprocal_sqrt.trapped == (trap_fallback ? instruction_count : 0) && patched.unrelocatable_memory_instruction_count == 0 && (protect_memory_sites ? patched.patched_memory_instruction_count > 0 : patched.patched_memory_instruction_count == 0), "instruction pass lost reciprocal root or memory patch coverage"); - const auto traps_before = g_rsqrt_traps; + const auto traps_before = g_instruction_traps; function(input.data(), output.data()); - Check(test, static_cast(g_rsqrt_traps - traps_before) == + Check(test, static_cast(g_instruction_traps - traps_before) == (trap_fallback ? instruction_count : 0), "reciprocal-root execution did not match its native/fallback patch count"); Check(test, (output[72] & 0x8d5u) == 0x45u, "patched instruction corrupted RFLAGS"); @@ -3139,15 +3147,15 @@ void TestPackedReciprocalSquareRoot() { register_module(code_size); const auto unsupported = Loader::PatchGuestInstructions( mapping, unsupported_code.getSize(), function_starts, false, true); - Check(test, Xbyak::GetError() == 0 && unsupported.reciprocal_sqrt_instruction_count == 1 && - unsupported.patched_reciprocal_sqrt_instruction_count == 0 && - unsupported.trapped_reciprocal_sqrt_instruction_count == 1 && + Check(test, Xbyak::GetError() == 0 && unsupported.reciprocal_sqrt.found == 1 && + unsupported.reciprocal_sqrt.native == 0 && + unsupported.reciprocal_sqrt.trapped == 1 && std::equal(expected_fallback.begin(), expected_fallback.end(), unsupported_code.getCode()), "unsafe native relocation did not select the in-place trap fallback"); - const auto traps_before = g_rsqrt_traps; + const auto traps_before = g_instruction_traps; function(input.data(), output.data()); - Check(test, g_rsqrt_traps == traps_before + 1 && output[0] == 0x3f800000 && + Check(test, g_instruction_traps == traps_before + 1 && output[0] == 0x3f800000 && std::all_of(output.begin() + 4, output.begin() + 8, [](uint32_t lane) { return lane == 0; }), "unsafe native relocation fallback did not execute correctly"); @@ -3220,6 +3228,316 @@ void TestPackedReciprocalSquareRoot() { #endif std::printf("[host] %-48s ok\n", test); } + +void TestPackedBitFieldExtract() { + const char* test = "PackedBitFieldExtract"; + constexpr uint64_t code_size = 0x80000; + const auto mapping = Libs::LibKernel::Memory::AllocateProgramMemory( + 0x904000000, code_size * 2, Common::VirtualMemory::Mode::ExecuteReadWrite, "extrq_test"); + Check(test, mapping != 0, "failed to allocate EXTRQ test code"); + InstructionTestScope restore {mapping, code_size * 2}; + restore.InstallHandler(test); + const bool host_sse4a = Xbyak::util::Cpu().has(Xbyak::util::Cpu::tSSE4a); + const auto register_module = [&](uint64_t capacity) { + Loader::RegisterGuestInstructionPatchModule(reinterpret_cast(mapping), code_size, + reinterpret_cast(mapping + code_size), + capacity); + }; + // Xbyak has no EXTRQ mnemonic. Emit the two canonical SSE4a encodings directly. + const auto extrq = [](Xbyak::CodeGenerator& code, int destination, int source, + uint8_t length = 0, uint8_t index = 0) { + code.db(0x66); + const int rex = source < 0 ? (destination >> 3) : ((destination >> 3) << 2) | (source >> 3); + if (rex != 0) { + code.db(0x40 | rex); + } + code.db(0x0f); + code.db(source < 0 ? 0x78 : 0x79); + code.db(source < 0 ? 0xc0 | (destination & 7) + : 0xc0 | ((destination & 7) << 3) | (source & 7)); + if (source < 0) { + code.db(length); + code.db(index); + } + }; + // Copy individual source bits so the oracle is independent of the emitter's shift/mask. + const auto extract = [](uint64_t value, uint32_t length, uint32_t index) { + const uint32_t count = (length & 63u) == 0 ? 64 : length & 63u; + const uint32_t first = index & 63u; + uint64_t result = 0; + for (uint32_t bit = 0; bit < count && first + bit < 64; ++bit) { + result |= ((value >> (first + bit)) & 1u) << bit; + } + return result; + }; + struct Result { + std::array destination; + std::array source; + std::array gprs; + uint64_t flags; + uint64_t stack_before; + uint64_t stack_after; + std::array red_zone; + }; + constexpr uint64_t gpr_sentinel = 0x8192a3b4c5d6e700ull; + constexpr uint64_t red_zone_sentinel = 0xa1b2c3d4e5f60000ull; + std::array input {0, + 0xdeadbeef12345678, + 0x8877665544332211, + 0x1122334455667788, + 0, + 0x123456789abcdef0, + 0x13579bdf2468ace0, + 0xfdb97531eca86420, + 0x3f8000003f800000, + 0x3f8000003f800000, + 0x3f8000003f800000, + 0x3f8000003f800000}; + const std::array function_starts {mapping}; + using GuestFunction = void(KYTY_SYSV_ABI*)(const uint64_t*, void*); + const auto function = reinterpret_cast(mapping); + for (const auto registers: {std::array {1, 8}, std::array {8, 9}, std::array {3, 3}, + std::array {15, 0}, std::array {8, -1}}) { + const auto destination = registers[0]; + const auto source = registers[1]; + Xbyak::CodeGenerator code(code_size, reinterpret_cast(mapping)); + const std::array saved {code.rbx, code.rbp, code.r12, code.r13, code.r14, code.r15}; + const std::array gprs {code.rax, code.rcx, code.rdx, code.rbx, code.rbp, code.r8, code.r9, + code.r10, code.r11, code.r12, code.r13, code.r14, code.r15}; + for (const auto& reg: saved) { + code.push(reg); + } + code.vmovups(Xbyak::Ymm(destination), code.ptr[code.rdi]); + if (source >= 0 && source != destination) { + code.vmovups(Xbyak::Ymm(source), code.ptr[code.rdi + 32]); + } + for (uint32_t offset = 8; offset <= 128; offset += 8) { + code.mov(code.rax, red_zone_sentinel | offset); + code.mov(code.qword[code.rsp - offset], code.rax); + } + code.mov(code.eax, 0x7fffffff); + code.add(code.eax, 1); + code.stc(); // CF, PF, AF, SF and OF set; ZF clear. + for (size_t index = 0; index < gprs.size(); ++index) { + code.mov(gprs[index], gpr_sentinel | index); + } + code.mov(code.qword[code.rsi + offsetof(Result, stack_before)], code.rsp); + extrq(code, destination, source, 0xc8, 0xc4); + code.mov(code.qword[code.rsi + offsetof(Result, stack_after)], code.rsp); + for (size_t index = 0; index < gprs.size(); ++index) { + code.mov(code.qword[code.rsi + offsetof(Result, gprs) + index * 8], gprs[index]); + } + code.lea(code.rsp, code.ptr[code.rsp - 128]); + code.pushfq(); + code.pop(code.rax); + code.lea(code.rsp, code.ptr[code.rsp + 128]); + code.mov(code.qword[code.rsi + offsetof(Result, flags)], code.rax); + for (uint32_t offset = 8; offset <= 128; offset += 8) { + code.mov(code.rax, code.qword[code.rsp - offset]); + code.mov(code.qword[code.rsi + offsetof(Result, red_zone) + offset - 8], code.rax); + } + code.vmovups(code.ptr[code.rsi + offsetof(Result, destination)], Xbyak::Ymm(destination)); + code.vmovups(code.ptr[code.rsi + offsetof(Result, source)], + Xbyak::Ymm(source < 0 ? destination : source)); + code.vzeroupper(); + for (auto reg = saved.rbegin(); reg != saved.rend(); ++reg) { + code.pop(*reg); + } + code.ret(); + Check(test, Xbyak::GetError() == 0, "failed to generate EXTRQ state fixture"); + const std::vector original(code.getCode(), code.getCurr()); + register_module(code_size); + const std::vector trampoline(code.getCode() + code_size, + code.getCode() + code_size * 2); + const auto disabled = + Loader::PatchGuestInstructions(mapping, code.getSize(), function_starts, false, false); + Check(test, + disabled.extrq.found == 0 && + std::equal(original.begin(), original.end(), code.getCode()) && + std::equal(trampoline.begin(), trampoline.end(), code.getCode() + code_size), + "disabled AMD patching changed EXTRQ code or trampolines"); + for (const bool fallback: {false, true}) { + std::memcpy(reinterpret_cast(mapping), original.data(), original.size()); + register_module(fallback ? 32 : code_size); + const auto counts = Loader::PatchGuestInstructions(mapping, code.getSize(), + function_starts, false, true) + .extrq; + if (host_sse4a) { + Check(test, + counts.found == 0 && + std::equal(original.begin(), original.end(), code.getCode()), + "SSE4a host received unnecessary EXTRQ patches"); + std::printf("[host] %-48s skipped (native SSE4a host)\n", test); + return; + } + std::printf("[host] EXTRQ xmm%d,%d %s found=%" PRIu64 " native=%" PRIu64 + " trapped=%" PRIu64 "\n", + destination, source, fallback ? "fallback" : "replacement", counts.found, + counts.native, counts.trapped); + Check(test, + Xbyak::GetError() == 0 && counts.found == 1 && counts.Skipped() == 0 && + counts.native == (fallback ? 0u : 1u) && + counts.trapped == (fallback ? 1u : 0u), + "EXTRQ native/fallback patch counts differ"); + const auto traps_before = g_instruction_traps; + uint64_t calls = 0; + for (uint32_t length = 0; length < 64; ++length) { + for (uint32_t index = 0; index < 64; ++index) { + if (source < 0 && (length != 8 || index != 4)) { + continue; + } + for (const uint64_t pattern: {UINT64_MAX, uint64_t {0x0123456789abcdef}}) { + const uint64_t controls = (pattern & ~uint64_t {0xffff}) | + ((index | 0xc0u) << 8) | (length | 0xc0u); + input[0] = source == destination ? controls : pattern; + input[4] = controls; + Result output {}; + _mm_setcsr(0x5fa5); + function(input.data(), &output); + const auto mxcsr = _mm_getcsr(); + _mm_setcsr(restore.mxcsr); + ++calls; + Check(test, + output.destination[0] == extract(input[0], length, index) && + output.destination[1] == 0 && output.destination[2] == input[2] && + output.destination[3] == input[3], + "EXTRQ result or destination lanes differ"); + Check(test, + source < 0 || source == destination + ? output.source == output.destination + : std::equal(output.source.begin(), output.source.end(), + input.begin() + 4), + "EXTRQ changed its separate source register"); + Check(test, + mxcsr == 0x5fa5 && (output.flags & 0x8d5u) == 0x895u && + output.stack_before == output.stack_after, + "EXTRQ changed MXCSR, RFLAGS or RSP"); + for (size_t reg = 0; reg < output.gprs.size(); ++reg) { + Check(test, output.gprs[reg] == (gpr_sentinel | reg), + "EXTRQ corrupted a guest GPR"); + } + for (size_t slot = 0; slot < output.red_zone.size(); ++slot) { + Check(test, + output.red_zone[slot] == (red_zone_sentinel | ((slot + 1) * 8)), + "EXTRQ corrupted the guest red zone"); + } + } + } + } + Check(test, + static_cast(g_instruction_traps - traps_before) == + (fallback ? calls : 0), + "EXTRQ execution did not match native/fallback selection"); + } + } + + // Cover every immediate length/index pair without re-decoding thousands of large fixtures. + Xbyak::CodeGenerator code(code_size, reinterpret_cast(mapping)); + std::vector starts; + for (uint32_t length = 0; length < 64; ++length) { + for (uint32_t index = 0; index < 64; ++index) { + starts.push_back(mapping + code.getSize()); + code.vmovups(code.ymm8, code.ptr[code.rdi]); + extrq(code, 8, -1, length | 0xc0, index | 0xc0); + code.vmovups(code.ptr[code.rsi], code.ymm8); + code.vzeroupper(); + code.ret(); + } + } + Check(test, Xbyak::GetError() == 0, "failed to generate exhaustive immediate EXTRQ fixture"); + const std::vector original(code.getCode(), code.getCurr()); + for (const bool fallback: {false, true}) { + std::memcpy(reinterpret_cast(mapping), original.data(), original.size()); + register_module(fallback ? 1 : code_size); + const auto counts = + Loader::PatchGuestInstructions(mapping, code.getSize(), starts, false, true).extrq; + Check(test, + Xbyak::GetError() == 0 && counts.found == starts.size() && counts.Skipped() == 0 && + counts.native == (fallback ? 0 : starts.size()) && + counts.trapped == (fallback ? starts.size() : 0), + "immediate EXTRQ coverage was lost"); + const auto traps_before = g_instruction_traps; + for (size_t entry = 0; entry < starts.size(); ++entry) { + for (const uint64_t pattern: {UINT64_MAX, uint64_t {0x0123456789abcdef}}) { + input[0] = pattern; + std::array output {}; + reinterpret_cast(starts[entry])(input.data(), output.data()); + Check(test, + output[0] == extract(pattern, entry / 64, entry % 64) && output[1] == 0 && + output[2] == input[2] && output[3] == input[3], + "immediate EXTRQ bit slice differs"); + } + } + Check(test, + static_cast(g_instruction_traps - traps_before) == + (fallback ? starts.size() * 2 : 0), + "immediate EXTRQ did not execute its selected path"); + } + + // An indirect branch prevents relocating this four-byte register form; retain its natural trap. + code.reset(); + Xbyak::Label continuation; + code.vmovups(code.ymm1, code.ptr[code.rdi]); + code.vmovups(code.ymm2, code.ptr[code.rdi + 32]); + code.lea(code.rax, code.ptr[code.rip + continuation]); + extrq(code, 1, 2); + code.jmp(code.rax); + code.L(continuation); + code.vmovups(code.ptr[code.rsi], code.ymm1); + code.vzeroupper(); + code.ret(); + Check(test, Xbyak::GetError() == 0, "failed to generate unrelocatable EXTRQ fixture"); + const std::vector unsafe_original(code.getCode(), code.getCurr()); + register_module(code_size); + const auto unsafe = + Loader::PatchGuestInstructions(mapping, code.getSize(), function_starts, false, true).extrq; + Check(test, + unsafe.found == 1 && unsafe.native == 0 && unsafe.trapped == 1 && + std::equal(unsafe_original.begin(), unsafe_original.end(), code.getCode()), + "unrelocatable EXTRQ did not retain its natural trap"); + input[0] = 0x0123456789abcdef; + input[4] = 0xffffc4c8; + std::array output {}; + const auto traps_before = g_instruction_traps; + function(input.data(), output.data()); + Check(test, g_instruction_traps == traps_before + 1 && output[0] == extract(input[0], 8, 4), + "unrelocatable EXTRQ fallback did not execute"); + + // Neighboring replacement kinds must share the same relocation/fallback span safely. + code.reset(); + code.vmovups(code.ymm1, code.ptr[code.rdi]); + code.vmovups(code.ymm2, code.ptr[code.rdi + 32]); + code.vmovups(code.ymm3, code.ptr[code.rdi + 64]); + extrq(code, 1, 2); + code.vrsqrtps(code.xmm4, code.xmm3); + code.vmovups(code.ptr[code.rsi], code.ymm1); + code.vmovups(code.ptr[code.rsi + 32], code.ymm4); + code.vzeroupper(); + code.ret(); + Check(test, Xbyak::GetError() == 0, "failed to generate mixed AMD instruction fixture"); + const std::vector mixed_original(code.getCode(), code.getCurr()); + for (const bool fallback: {false, true}) { + std::memcpy(reinterpret_cast(mapping), mixed_original.data(), mixed_original.size()); + register_module(fallback ? 48 : code_size); + const auto mixed = + Loader::PatchGuestInstructions(mapping, code.getSize(), function_starts, false, true); + for (const auto counts: {mixed.extrq, mixed.reciprocal_sqrt}) { + Check(test, + counts.found == 1 && counts.native == (fallback ? 0u : 1u) && + counts.trapped == (fallback ? 1u : 0u), + "mixed AMD instruction counts differ"); + } + const auto before = g_instruction_traps; + function(input.data(), output.data()); + Check(test, + g_instruction_traps == before + (fallback ? 2 : 0) && + output[0] == extract(input[0], 8, 4) && output[4] == 0x3f8000003f800000 && + output[5] == 0x3f8000003f800000 && output[6] == 0 && output[7] == 0, + "mixed native/fallback AMD instructions lost their results"); + } + std::printf("[host] %-48s ok (all 4096 length/index pairs)\n", test); +} + #endif #if defined(__x86_64__) || defined(_M_X64) @@ -3396,6 +3714,10 @@ int main(int argc, char** argv) { } #endif #if defined(__linux__) || KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS + if (argc == 2 && std::strcmp(argv[1], "--extrq-only") == 0) { + RunTest(TestPackedBitFieldExtract); + return g_failed_tests == 0 ? 0 : 1; + } if (argc == 2 && std::strcmp(argv[1], "--rsqrt-only") == 0) { RunTest(TestPackedReciprocalSquareRoot); return g_failed_tests == 0 ? 0 : 1; @@ -3411,6 +3733,7 @@ int main(int argc, char** argv) { #endif #if defined(__linux__) || KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS RunTest(TestPackedReciprocalSquareRoot); + RunTest(TestPackedBitFieldExtract); #endif RunTest(TestWindowsGuestRedZoneStaticPatcher); RunTest(TestProsperoArgumentAndInfoSizeContracts);