mirror of
https://github.com/FEX-Emu/FEX.git
synced 2026-10-07 12:00:17 +02:00
803 lines
30 KiB
C++
803 lines
30 KiB
C++
// SPDX-License-Identifier: MIT
|
|
/*
|
|
$info$
|
|
tags: Bin|ARM64EC
|
|
desc: Implements the ARM64EC BT module API using FEXCore
|
|
$end_info$
|
|
*/
|
|
|
|
#include <FEXCore/fextl/fmt.h>
|
|
#include <FEXCore/Core/X86Enums.h>
|
|
#include <FEXCore/Core/SignalDelegator.h>
|
|
#include <FEXCore/Core/Context.h>
|
|
#include <FEXCore/Core/CoreState.h>
|
|
#include <FEXCore/Debug/InternalThreadState.h>
|
|
#include <FEXCore/HLE/SyscallHandler.h>
|
|
#include <FEXCore/Config/Config.h>
|
|
#include <FEXCore/Utils/Allocator.h>
|
|
#include <FEXCore/Utils/LogManager.h>
|
|
#include <FEXCore/Utils/Threads.h>
|
|
#include <FEXCore/Utils/EnumOperators.h>
|
|
#include <FEXCore/Utils/EnumUtils.h>
|
|
#include <FEXCore/Utils/FPState.h>
|
|
#include <FEXCore/Utils/ArchHelpers/Arm64.h>
|
|
#include <FEXCore/Utils/MathUtils.h>
|
|
#include <FEXCore/Utils/TypeDefines.h>
|
|
|
|
#include "Common/Config.h"
|
|
#include "Common/HostFeatures.h"
|
|
#include "Common/InvalidationTracker.h"
|
|
#include "Common/TSOHandlerConfig.h"
|
|
#include "Common/CPUFeatures.h"
|
|
#include "Common/Logging.h"
|
|
#include "Common/CRT/CRT.h"
|
|
#include "DummyHandlers.h"
|
|
#include "BTInterface.h"
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <type_traits>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <ntstatus.h>
|
|
#include <windef.h>
|
|
#include <winternl.h>
|
|
#include <winnt.h>
|
|
#include <wine/debug.h>
|
|
|
|
class ECSyscallHandler;
|
|
|
|
extern "C" {
|
|
extern IMAGE_DOS_HEADER __ImageBase; // Provided by the linker
|
|
|
|
extern void* ExitFunctionEC;
|
|
extern void* CheckCall;
|
|
|
|
void* X64ReturnInstr; // See Module.S
|
|
uintptr_t NtDllBase;
|
|
|
|
// Exports on ARM64EC point to x64 fast forward sequences to allow for redirecting to the JIT if functions are hotpatched. This LUT is from their addresses to the relative addresses of the native code exports.
|
|
uint32_t* NtDllRedirectionLUT;
|
|
uint32_t NtDllRedirectionLUTSize;
|
|
|
|
// Wine doesn't support issuing direct system calls with SVC, and unlike Windows it doesn't have a 'stable' syscall number for NtContinue
|
|
void* WineSyscallDispatcher;
|
|
// TODO: this really shouldn't be hardcoded, once wine gains proper syscall thunks this can be dropped.
|
|
uint64_t WineNtContinueSyscallId = 0x1a;
|
|
}
|
|
|
|
struct ThreadCPUArea {
|
|
static constexpr size_t TEBCPUAreaOffset = 0x1788;
|
|
CHPE_V2_CPU_AREA_INFO* Area;
|
|
|
|
explicit ThreadCPUArea(_TEB* TEB)
|
|
: Area(*reinterpret_cast<CHPE_V2_CPU_AREA_INFO**>(reinterpret_cast<uintptr_t>(TEB) + TEBCPUAreaOffset)) {}
|
|
|
|
uint64_t& EmulatorStackLimit() const {
|
|
return Area->EmulatorStackLimit;
|
|
}
|
|
|
|
uint64_t& EmulatorStackBase() const {
|
|
return Area->EmulatorStackBase;
|
|
}
|
|
|
|
ARM64EC_NT_CONTEXT& ContextAmd64() const {
|
|
return *Area->ContextAmd64;
|
|
}
|
|
|
|
FEXCore::Core::CpuStateFrame*& StateFrame() const {
|
|
return reinterpret_cast<FEXCore::Core::CpuStateFrame*&>(Area->EmulatorData[0]);
|
|
}
|
|
|
|
FEXCore::Core::InternalThreadState*& ThreadState() const {
|
|
return reinterpret_cast<FEXCore::Core::InternalThreadState*&>(Area->EmulatorData[1]);
|
|
}
|
|
|
|
uint64_t& DispatcherLoopTopEnterEC() const {
|
|
return reinterpret_cast<uint64_t&>(Area->EmulatorData[2]);
|
|
}
|
|
|
|
uint64_t& DispatcherLoopTopEnterECFillSRA() const {
|
|
return reinterpret_cast<uint64_t&>(Area->EmulatorData[3]);
|
|
}
|
|
};
|
|
|
|
|
|
extern "C" NTSTATUS NtContinueNative(ARM64_NT_CONTEXT* NativeContext, BOOLEAN Alert);
|
|
|
|
namespace {
|
|
fextl::unique_ptr<FEXCore::Context::Context> CTX;
|
|
fextl::unique_ptr<FEX::DummyHandlers::DummySignalDelegator> SignalDelegator;
|
|
fextl::unique_ptr<ECSyscallHandler> SyscallHandler;
|
|
std::optional<FEX::Windows::InvalidationTracker> InvalidationTracker;
|
|
std::optional<FEX::Windows::CPUFeatures> CPUFeatures;
|
|
|
|
std::recursive_mutex ThreadCreationMutex;
|
|
// Map of TIDs to their FEX thread state, `ThreadCreationMutex` must be locked when accessing
|
|
std::unordered_map<DWORD, FEXCore::Core::InternalThreadState*> Threads;
|
|
|
|
// Map from system call numbers to the relative addresses of their native implementations in ntdll
|
|
std::vector<uint32_t> NtDllSyscallLUT;
|
|
|
|
std::pair<NTSTATUS, ThreadCPUArea> GetThreadCPUArea(HANDLE Thread) {
|
|
THREAD_BASIC_INFORMATION Info;
|
|
const NTSTATUS Err = NtQueryInformationThread(Thread, ThreadBasicInformation, &Info, sizeof(Info), nullptr);
|
|
return {Err, ThreadCPUArea(reinterpret_cast<_TEB*>(Info.TebBaseAddress))};
|
|
}
|
|
|
|
ThreadCPUArea GetCPUArea() {
|
|
return ThreadCPUArea(NtCurrentTeb());
|
|
}
|
|
|
|
bool IsEmulatorStackAddress(uint64_t Address) {
|
|
return Address <= GetCPUArea().EmulatorStackBase() && Address >= GetCPUArea().EmulatorStackLimit();
|
|
}
|
|
|
|
bool IsDispatcherAddress(uint64_t Address) {
|
|
const auto& Config = SignalDelegator->GetConfig();
|
|
return Address >= Config.DispatcherBegin && Address < Config.DispatcherEnd;
|
|
}
|
|
|
|
|
|
void FillNtDllLUTs() {
|
|
const HMODULE NtDll = GetModuleHandle("ntdll.dll");
|
|
NtDllBase = reinterpret_cast<uintptr_t>(NtDll);
|
|
ULONG Size;
|
|
const auto* LoadConfig =
|
|
reinterpret_cast<_IMAGE_LOAD_CONFIG_DIRECTORY64*>(RtlImageDirectoryEntryToData(NtDll, true, IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG, &Size));
|
|
const auto* CHPEMetadata = reinterpret_cast<IMAGE_ARM64EC_METADATA*>(LoadConfig->CHPEMetadataPointer);
|
|
const auto* RedirectionTableBegin = reinterpret_cast<IMAGE_ARM64EC_REDIRECTION_ENTRY*>(NtDllBase + CHPEMetadata->RedirectionMetadata);
|
|
const auto* RedirectionTableEnd = RedirectionTableBegin + CHPEMetadata->RedirectionMetadataCount;
|
|
|
|
NtDllRedirectionLUTSize = std::prev(RedirectionTableEnd)->Source + 1;
|
|
NtDllRedirectionLUT = new uint32_t[NtDllRedirectionLUTSize];
|
|
for (auto It = RedirectionTableBegin; It != RedirectionTableEnd; It++) {
|
|
NtDllRedirectionLUT[It->Source] = It->Destination;
|
|
}
|
|
|
|
const auto* Exports = reinterpret_cast<IMAGE_EXPORT_DIRECTORY*>(RtlImageDirectoryEntryToData(NtDll, true, IMAGE_DIRECTORY_ENTRY_EXPORT, &Size));
|
|
const auto* FunctionTableBegin = reinterpret_cast<uint32_t*>(NtDllBase + Exports->AddressOfFunctions);
|
|
const auto* FunctionTableEnd = FunctionTableBegin + Exports->NumberOfFunctions;
|
|
|
|
NtDllSyscallLUT.reserve(0x200);
|
|
for (auto It = FunctionTableBegin; It != FunctionTableEnd; It++) {
|
|
const uint8_t* FunctionAddr = reinterpret_cast<uint8_t*>(NtDllBase + *It);
|
|
// Windows syscall thunks are as follows:
|
|
// 00: mov r10, rcx
|
|
// 03: mov eax, <NUM>
|
|
// <cont into MatchSeq>
|
|
static constexpr std::array<uint8_t, 16> MatchSeq {{
|
|
0xf6, 0x04, 0x25, 0x08, 0x03, 0xfe, 0x7f, 0x01, // 08: test byte ptr ds:7FFE0308h, 1
|
|
0x75, 0x03, // 10: jnz short lbl
|
|
0x0f, 0x05, // 12: syscall
|
|
0xc3, // 14: retn
|
|
0xcd, 0x2e, // 15: lbl: int 2Eh
|
|
0xc3 // 17: retn
|
|
}};
|
|
|
|
const uint8_t* MatchAddr = FunctionAddr + 8;
|
|
if (!memcmp(MatchSeq.data(), MatchAddr, MatchSeq.size())) {
|
|
const uint32_t SyscallNum = *reinterpret_cast<const uint32_t*>(FunctionAddr + 4);
|
|
NtDllSyscallLUT.resize(std::max<size_t>(NtDllSyscallLUT.size(), SyscallNum));
|
|
NtDllSyscallLUT[SyscallNum] = NtDllRedirectionLUT[*It];
|
|
}
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
void WriteModuleRVA(HMODULE Module, LONG RVA, T Data) {
|
|
if (!RVA) {
|
|
return;
|
|
}
|
|
|
|
void* Address = reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(Module) + RVA);
|
|
void* ProtAddress = Address;
|
|
SIZE_T ProtSize = sizeof(T);
|
|
ULONG Prot;
|
|
NtProtectVirtualMemory(NtCurrentProcess(), &ProtAddress, &ProtSize, PAGE_READWRITE, &Prot);
|
|
*reinterpret_cast<T*>(Address) = Data;
|
|
NtProtectVirtualMemory(NtCurrentProcess(), &ProtAddress, &ProtSize, Prot, nullptr);
|
|
}
|
|
|
|
void PatchCallChecker() {
|
|
// See the comment for CheckCall in Module.S for why this is necessary
|
|
const auto Module = reinterpret_cast<HMODULE>(&__ImageBase);
|
|
ULONG Size;
|
|
const auto* LoadConfig =
|
|
reinterpret_cast<_IMAGE_LOAD_CONFIG_DIRECTORY64*>(RtlImageDirectoryEntryToData(Module, true, IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG, &Size));
|
|
const auto* CHPEMetadata = reinterpret_cast<IMAGE_ARM64EC_METADATA*>(LoadConfig->CHPEMetadataPointer);
|
|
WriteModuleRVA(Module, CHPEMetadata->__os_arm64x_dispatch_call, &CheckCall);
|
|
WriteModuleRVA(Module, CHPEMetadata->__os_arm64x_dispatch_icall, &CheckCall);
|
|
WriteModuleRVA(Module, CHPEMetadata->__os_arm64x_dispatch_icall_cfg, &CheckCall);
|
|
}
|
|
} // namespace
|
|
|
|
namespace Exception {
|
|
static std::optional<FEX::Windows::TSOHandlerConfig> HandlerConfig;
|
|
static uintptr_t KiUserExceptionDispatcher;
|
|
|
|
static EXCEPTION_RECORD HandleGuestException(const EXCEPTION_RECORD& Src, ARM64_NT_CONTEXT& Context) {
|
|
auto* Thread = GetCPUArea().ThreadState();
|
|
auto& Fault = Thread->CurrentFrame->SynchronousFaultData;
|
|
EXCEPTION_RECORD Dst = Src;
|
|
Dst.ExceptionAddress = reinterpret_cast<void*>(Context.Pc);
|
|
|
|
// X64 Windows always clears TF, DF and AF when handling an exception, restoring after.
|
|
// Current ARM64EC windows can only restore NZCV+SS when returning from an exception and other flags are left untouched from the handler context.
|
|
// TODO: Can extend wine to support this by mapping the remaining EFlags into reserved cpsr members.
|
|
uint32_t EFlags = CTX->ReconstructCompactedEFLAGS(Thread, true, Context.X, Context.Cpsr);
|
|
EFlags &= (1 << FEXCore::X86State::RFLAG_TF_LOC);
|
|
CTX->SetFlagsFromCompactedEFLAGS(Thread, EFlags);
|
|
|
|
if (!Fault.FaultToTopAndGeneratedException) {
|
|
return Dst;
|
|
}
|
|
Fault.FaultToTopAndGeneratedException = false;
|
|
|
|
Dst.ExceptionFlags = 0;
|
|
Dst.NumberParameters = 0;
|
|
|
|
switch (Fault.Signal) {
|
|
case FEXCore::Core::FAULT_SIGILL: Dst.ExceptionCode = EXCEPTION_ILLEGAL_INSTRUCTION; return Dst;
|
|
case FEXCore::Core::FAULT_SIGTRAP:
|
|
switch (Fault.TrapNo) {
|
|
case FEXCore::X86State::X86_TRAPNO_DB:
|
|
Context.Cpsr &= ~(1 << 21); // PSTATE.SS
|
|
Dst.ExceptionCode = EXCEPTION_SINGLE_STEP;
|
|
return Dst;
|
|
case FEXCore::X86State::X86_TRAPNO_BP:
|
|
Context.Pc -= 1;
|
|
Dst.ExceptionAddress = reinterpret_cast<void*>(Context.Pc);
|
|
Dst.ExceptionCode = EXCEPTION_BREAKPOINT;
|
|
Dst.NumberParameters = 1;
|
|
Dst.ExceptionInformation[0] = 0;
|
|
return Dst;
|
|
default: LogMan::Msg::EFmt("Unknown SIGTRAP trap: {}", Fault.TrapNo); break;
|
|
}
|
|
break;
|
|
case FEXCore::Core::FAULT_SIGSEGV:
|
|
switch (Fault.TrapNo) {
|
|
case FEXCore::X86State::X86_TRAPNO_GP:
|
|
if ((Fault.err_code & 0b111) == 0b010) {
|
|
switch (Fault.err_code >> 3) {
|
|
case 0x2d:
|
|
Context.Pc += 2;
|
|
Dst.ExceptionCode = EXCEPTION_BREAKPOINT;
|
|
Dst.ExceptionAddress = reinterpret_cast<void*>(Context.Pc + 1);
|
|
Dst.NumberParameters = 1;
|
|
Dst.ExceptionInformation[0] = Context.X8; // RAX
|
|
// Note that ExceptionAddress doesn't equal the reported context RIP here, this discrepancy expected and not having it can trigger anti-debug logic.
|
|
return Dst;
|
|
default: LogMan::Msg::EFmt("Unknown interrupt: 0x{:X}", Fault.err_code >> 3); break;
|
|
}
|
|
} else {
|
|
Dst.ExceptionCode = EXCEPTION_PRIV_INSTRUCTION;
|
|
return Dst;
|
|
}
|
|
break;
|
|
case FEXCore::X86State::X86_TRAPNO_OF: Dst.ExceptionCode = EXCEPTION_INT_OVERFLOW; return Dst;
|
|
default: LogMan::Msg::EFmt("Unknown SIGSEGV trap: {}", Fault.TrapNo); break;
|
|
}
|
|
break;
|
|
default: LogMan::Msg::EFmt("Unknown signal type: {}", Fault.Signal); break;
|
|
}
|
|
|
|
// Default to SIGILL
|
|
Dst.ExceptionCode = EXCEPTION_ILLEGAL_INSTRUCTION;
|
|
return Dst;
|
|
}
|
|
|
|
static bool HandleUnalignedAccess(ARM64_NT_CONTEXT& Context) {
|
|
if (!CTX->IsAddressInCodeBuffer(GetCPUArea().ThreadState(), Context.Pc)) {
|
|
return false;
|
|
}
|
|
|
|
const auto Result = FEXCore::ArchHelpers::Arm64::HandleUnalignedAccess(GetCPUArea().ThreadState(),
|
|
HandlerConfig->GetUnalignedHandlerType(), Context.Pc, &Context.X0);
|
|
if (!Result.first) {
|
|
return false;
|
|
}
|
|
|
|
Context.Pc += Result.second;
|
|
return true;
|
|
}
|
|
|
|
static void LoadStateFromECContext(FEXCore::Core::InternalThreadState* Thread, CONTEXT& Context) {
|
|
auto& State = Thread->CurrentFrame->State;
|
|
|
|
if (Context.ContextFlags & CONTEXT_INTEGER) {
|
|
// General register state
|
|
State.gregs[FEXCore::X86State::REG_RAX] = Context.Rax;
|
|
State.gregs[FEXCore::X86State::REG_RCX] = Context.Rcx;
|
|
State.gregs[FEXCore::X86State::REG_RDX] = Context.Rdx;
|
|
State.gregs[FEXCore::X86State::REG_RBX] = Context.Rbx;
|
|
|
|
State.gregs[FEXCore::X86State::REG_RSI] = Context.Rsi;
|
|
State.gregs[FEXCore::X86State::REG_RDI] = Context.Rdi;
|
|
State.gregs[FEXCore::X86State::REG_R8] = Context.R8;
|
|
State.gregs[FEXCore::X86State::REG_R9] = Context.R9;
|
|
State.gregs[FEXCore::X86State::REG_R10] = Context.R10;
|
|
State.gregs[FEXCore::X86State::REG_R11] = Context.R11;
|
|
State.gregs[FEXCore::X86State::REG_R12] = Context.R12;
|
|
State.gregs[FEXCore::X86State::REG_R13] = Context.R13;
|
|
State.gregs[FEXCore::X86State::REG_R14] = Context.R14;
|
|
State.gregs[FEXCore::X86State::REG_R15] = Context.R15;
|
|
}
|
|
|
|
if (Context.ContextFlags & CONTEXT_CONTROL) {
|
|
State.rip = Context.Rip;
|
|
State.gregs[FEXCore::X86State::REG_RSP] = Context.Rsp;
|
|
State.gregs[FEXCore::X86State::REG_RBP] = Context.Rbp;
|
|
CTX->SetFlagsFromCompactedEFLAGS(Thread, Context.EFlags);
|
|
}
|
|
|
|
if (Context.ContextFlags & CONTEXT_SEGMENTS) {
|
|
State.es_idx = Context.SegEs & 0xffff;
|
|
State.cs_idx = Context.SegCs & 0xffff;
|
|
State.ss_idx = Context.SegSs & 0xffff;
|
|
State.ds_idx = Context.SegDs & 0xffff;
|
|
State.fs_idx = Context.SegFs & 0xffff;
|
|
State.gs_idx = Context.SegGs & 0xffff;
|
|
|
|
// The TEB is the only populated GDT entry by default
|
|
const auto TEB = reinterpret_cast<uint64_t>(NtCurrentTeb());
|
|
State.gdt[(Context.SegGs & 0xffff) >> 3].base = TEB;
|
|
State.gs_cached = TEB;
|
|
State.fs_cached = 0;
|
|
State.es_cached = 0;
|
|
State.cs_cached = 0;
|
|
State.ss_cached = 0;
|
|
State.ds_cached = 0;
|
|
}
|
|
|
|
if (Context.ContextFlags & CONTEXT_FLOATING_POINT) {
|
|
// Floating-point register state
|
|
CTX->SetXMMRegistersFromState(Thread, reinterpret_cast<const __uint128_t*>(Context.FltSave.XmmRegisters), nullptr);
|
|
memcpy(State.mm, Context.FltSave.FloatRegisters, sizeof(State.mm));
|
|
|
|
State.FCW = Context.FltSave.ControlWord;
|
|
State.flags[FEXCore::X86State::X87FLAG_C0_LOC] = (Context.FltSave.StatusWord >> 8) & 1;
|
|
State.flags[FEXCore::X86State::X87FLAG_C1_LOC] = (Context.FltSave.StatusWord >> 9) & 1;
|
|
State.flags[FEXCore::X86State::X87FLAG_C2_LOC] = (Context.FltSave.StatusWord >> 10) & 1;
|
|
State.flags[FEXCore::X86State::X87FLAG_C3_LOC] = (Context.FltSave.StatusWord >> 14) & 1;
|
|
State.flags[FEXCore::X86State::X87FLAG_TOP_LOC] = (Context.FltSave.StatusWord >> 11) & 0b111;
|
|
State.AbridgedFTW = Context.FltSave.TagWord;
|
|
}
|
|
}
|
|
|
|
static void ReconstructThreadState(ARM64_NT_CONTEXT& Context) {
|
|
const auto& Config = SignalDelegator->GetConfig();
|
|
auto* Thread = GetCPUArea().ThreadState();
|
|
auto& State = Thread->CurrentFrame->State;
|
|
|
|
State.rip = CTX->RestoreRIPFromHostPC(Thread, Context.Pc);
|
|
|
|
// Spill all SRA GPRs
|
|
for (size_t i = 0; i < Config.SRAGPRCount; i++) {
|
|
State.gregs[i] = Context.X[Config.SRAGPRMapping[i]];
|
|
}
|
|
|
|
// Spill all SRA FPRs
|
|
for (size_t i = 0; i < Config.SRAFPRCount; i++) {
|
|
memcpy(State.xmm.sse.data[i], &Context.V[Config.SRAFPRMapping[i]], sizeof(__uint128_t));
|
|
}
|
|
|
|
// Spill EFlags
|
|
uint32_t EFlags = CTX->ReconstructCompactedEFLAGS(Thread, true, Context.X, Context.Cpsr);
|
|
CTX->SetFlagsFromCompactedEFLAGS(Thread, EFlags);
|
|
}
|
|
|
|
// Reconstructs an x64 context from the input context within the JIT, packed into a regular ARM64 context following the ARM64EC register mapping
|
|
static ARM64_NT_CONTEXT ReconstructPackedECContext(ARM64_NT_CONTEXT& Context) {
|
|
ReconstructThreadState(Context);
|
|
ARM64_NT_CONTEXT ECContext {};
|
|
|
|
ECContext.ContextFlags = CONTEXT_ARM64_FULL;
|
|
|
|
auto* Thread = GetCPUArea().ThreadState();
|
|
auto& State = Thread->CurrentFrame->State;
|
|
|
|
ECContext.X8 = State.gregs[FEXCore::X86State::REG_RAX];
|
|
ECContext.X0 = State.gregs[FEXCore::X86State::REG_RCX];
|
|
ECContext.X1 = State.gregs[FEXCore::X86State::REG_RDX];
|
|
ECContext.X27 = State.gregs[FEXCore::X86State::REG_RBX];
|
|
ECContext.Sp = State.gregs[FEXCore::X86State::REG_RSP];
|
|
ECContext.Fp = State.gregs[FEXCore::X86State::REG_RBP];
|
|
ECContext.X25 = State.gregs[FEXCore::X86State::REG_RSI];
|
|
ECContext.X26 = State.gregs[FEXCore::X86State::REG_RDI];
|
|
ECContext.X2 = State.gregs[FEXCore::X86State::REG_R8];
|
|
ECContext.X3 = State.gregs[FEXCore::X86State::REG_R9];
|
|
ECContext.X4 = State.gregs[FEXCore::X86State::REG_R10];
|
|
ECContext.X5 = State.gregs[FEXCore::X86State::REG_R11];
|
|
ECContext.X19 = State.gregs[FEXCore::X86State::REG_R12];
|
|
ECContext.X20 = State.gregs[FEXCore::X86State::REG_R13];
|
|
ECContext.X21 = State.gregs[FEXCore::X86State::REG_R14];
|
|
ECContext.X22 = State.gregs[FEXCore::X86State::REG_R15];
|
|
|
|
ECContext.Pc = State.rip;
|
|
|
|
CTX->ReconstructXMMRegisters(Thread, reinterpret_cast<__uint128_t*>(&ECContext.V[0]), nullptr);
|
|
|
|
ECContext.Lr = State.mm[0][0];
|
|
ECContext.X6 = State.mm[1][0];
|
|
ECContext.X7 = State.mm[2][0];
|
|
ECContext.X9 = State.mm[3][0];
|
|
ECContext.X16 = (State.mm[3][1] & 0xffff) << 48 | (State.mm[2][1] & 0xffff) << 32 | (State.mm[1][1] & 0xffff) << 16 | (State.mm[0][1] & 0xffff);
|
|
ECContext.X10 = State.mm[4][0];
|
|
ECContext.X11 = State.mm[5][0];
|
|
ECContext.X12 = State.mm[6][0];
|
|
ECContext.X15 = State.mm[7][0];
|
|
ECContext.X17 = (State.mm[7][1] & 0xffff) << 48 | (State.mm[6][1] & 0xffff) << 32 | (State.mm[5][1] & 0xffff) << 16 | (State.mm[4][1] & 0xffff);
|
|
|
|
// Zero all disallowed registers
|
|
ECContext.X13 = 0;
|
|
ECContext.X14 = 0;
|
|
ECContext.X18 = 0;
|
|
ECContext.X23 = 0;
|
|
ECContext.X24 = 0;
|
|
ECContext.X28 = 0;
|
|
|
|
// NZCV+SS will be converted into EFlags by ntdll, the rest are lost during exception handling.
|
|
// See HandleGuestException
|
|
ECContext.Cpsr = Context.Cpsr;
|
|
uint32_t EFlags = CTX->ReconstructCompactedEFLAGS(Thread, false, nullptr, 0);
|
|
if (EFlags & (1U << FEXCore::X86State::RFLAG_TF_LOC)) {
|
|
ECContext.Cpsr |= 1 << 21; // PSTATE.SS
|
|
}
|
|
|
|
ECContext.Fpcr = Context.Fpcr;
|
|
ECContext.Fpsr = Context.Fpsr;
|
|
|
|
return ECContext;
|
|
}
|
|
|
|
static void RethrowGuestException(const EXCEPTION_RECORD& Rec, ARM64_NT_CONTEXT& Context) {
|
|
const auto& Config = SignalDelegator->GetConfig();
|
|
uint64_t GuestSp = Context.X[Config.SRAGPRMapping[static_cast<size_t>(FEXCore::X86State::REG_RSP)]];
|
|
struct DispatchArgs {
|
|
ARM64_NT_CONTEXT Context;
|
|
uint64_t Pad[4]; // Only present on newer Windows versions, likely for SVE.
|
|
EXCEPTION_RECORD Rec;
|
|
uint64_t Align;
|
|
uint64_t Redzone[2];
|
|
}* Args = reinterpret_cast<DispatchArgs*>(FEXCore::AlignDown(GuestSp, 64)) - 1;
|
|
|
|
LogMan::Msg::DFmt("Reconstructing context");
|
|
Args->Context = ReconstructPackedECContext(Context);
|
|
LogMan::Msg::DFmt("pc: {:X} rip: {:X}", Context.Pc, Args->Context.Pc);
|
|
Args->Rec = HandleGuestException(Rec, Args->Context);
|
|
Context.Sp = reinterpret_cast<uint64_t>(Args);
|
|
Context.Pc = KiUserExceptionDispatcher;
|
|
}
|
|
} // namespace Exception
|
|
|
|
class ECSyscallHandler : public FEXCore::HLE::SyscallHandler, public FEXCore::Allocator::FEXAllocOperators {
|
|
public:
|
|
ECSyscallHandler() {
|
|
OSABI = FEXCore::HLE::SyscallOSABI::OS_GENERIC;
|
|
}
|
|
|
|
uint64_t HandleSyscall(FEXCore::Core::CpuStateFrame* Frame, FEXCore::HLE::SyscallArguments* Args) override {
|
|
Frame->State.rip = NtDllBase + NtDllSyscallLUT[Frame->State.gregs[FEXCore::X86State::REG_RAX]];
|
|
Frame->State.gregs[FEXCore::X86State::REG_RCX] = Frame->State.gregs[FEXCore::X86State::REG_R10];
|
|
return 0;
|
|
}
|
|
|
|
FEXCore::HLE::SyscallABI GetSyscallABI(uint64_t Syscall) override {
|
|
return {.NumArgs = 0, .HasReturn = false, .HostSyscallNumber = -1};
|
|
}
|
|
|
|
FEXCore::HLE::AOTIRCacheEntryLookupResult LookupAOTIRCacheEntry(FEXCore::Core::InternalThreadState* Thread, uint64_t GuestAddr) override {
|
|
return {0, 0};
|
|
}
|
|
|
|
void MarkGuestExecutableRange(FEXCore::Core::InternalThreadState* Thread, uint64_t Start, uint64_t Length) override {
|
|
InvalidationTracker->ReprotectRWXIntervals(Start, Length);
|
|
}
|
|
};
|
|
|
|
extern "C" void SyncThreadContext(CONTEXT* Context) {
|
|
auto* Thread = GetCPUArea().ThreadState();
|
|
// All other EFlags bits are lost when converting to/from an ARM64EC context, so merge them in from the current JIT state.
|
|
// This is advisable over dropping their values as thread suspend/resume uses this function, and that can happen at any point in guest code.
|
|
static constexpr uint32_t ECValidEFlagsMask {(1U << FEXCore::X86State::RFLAG_OF_RAW_LOC) | (1U << FEXCore::X86State::RFLAG_CF_RAW_LOC) |
|
|
(1U << FEXCore::X86State::RFLAG_ZF_RAW_LOC) | (1U << FEXCore::X86State::RFLAG_SF_RAW_LOC) |
|
|
(1U << FEXCore::X86State::RFLAG_TF_LOC)};
|
|
|
|
uint32_t StateEFlags = CTX->ReconstructCompactedEFLAGS(Thread, false, nullptr, 0);
|
|
Context->EFlags = (Context->EFlags & ECValidEFlagsMask) | (StateEFlags & ~ECValidEFlagsMask);
|
|
Exception::LoadStateFromECContext(Thread, *Context);
|
|
}
|
|
|
|
NTSTATUS ProcessInit() {
|
|
FEX::Windows::InitCRTProcess();
|
|
FEX::Config::InitializeConfigs();
|
|
FEXCore::Config::Initialize();
|
|
FEXCore::Config::AddLayer(FEX::Config::CreateGlobalMainLayer());
|
|
FEXCore::Config::AddLayer(FEX::Config::CreateMainLayer());
|
|
FEXCore::Config::Load();
|
|
FEXCore::Config::ReloadMetaLayer();
|
|
FEX::Windows::Logging::Init();
|
|
|
|
FEXCore::Config::EraseSet(FEXCore::Config::CONFIG_IS64BIT_MODE, "1");
|
|
|
|
// Not applicable to Windows
|
|
FEXCore::Config::EraseSet(FEXCore::Config::ConfigOption::CONFIG_TSOAUTOMIGRATION, "0");
|
|
|
|
FEXCore::Context::InitializeStaticTables(FEXCore::Context::MODE_64BIT);
|
|
|
|
SignalDelegator = fextl::make_unique<FEX::DummyHandlers::DummySignalDelegator>();
|
|
SyscallHandler = fextl::make_unique<ECSyscallHandler>();
|
|
Exception::HandlerConfig.emplace();
|
|
|
|
{
|
|
auto HostFeatures = FEX::FetchHostFeatures();
|
|
CTX = FEXCore::Context::Context::CreateNewContext(HostFeatures);
|
|
}
|
|
|
|
CTX->SetSignalDelegator(SignalDelegator.get());
|
|
CTX->SetSyscallHandler(SyscallHandler.get());
|
|
CTX->InitCore();
|
|
InvalidationTracker.emplace(*CTX, Threads);
|
|
CPUFeatures.emplace(*CTX);
|
|
|
|
X64ReturnInstr = ::VirtualAlloc(nullptr, FEXCore::Utils::FEX_PAGE_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
|
|
*reinterpret_cast<uint8_t*>(X64ReturnInstr) = 0xc3;
|
|
|
|
FillNtDllLUTs();
|
|
PatchCallChecker();
|
|
const auto NtDll = GetModuleHandle("ntdll.dll");
|
|
const uintptr_t KiUserExceptionDispatcherFFS = reinterpret_cast<uintptr_t>(GetProcAddress(NtDll, "KiUserExceptionDispatcher"));
|
|
Exception::KiUserExceptionDispatcher = NtDllRedirectionLUT[KiUserExceptionDispatcherFFS - NtDllBase] + NtDllBase;
|
|
const auto WineSyscallDispatcherPtr = reinterpret_cast<void**>(GetProcAddress(NtDll, "__wine_syscall_dispatcher"));
|
|
if (WineSyscallDispatcherPtr) {
|
|
WineSyscallDispatcher = *WineSyscallDispatcherPtr;
|
|
}
|
|
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
void ProcessTerm(HANDLE Handle, BOOL After, NTSTATUS Status) {}
|
|
|
|
class ScopedCallbackDisable {
|
|
private:
|
|
bool Prev;
|
|
|
|
public:
|
|
ScopedCallbackDisable() {
|
|
Prev = GetCPUArea().Area->InSyscallCallback;
|
|
GetCPUArea().Area->InSyscallCallback = true;
|
|
}
|
|
|
|
~ScopedCallbackDisable() {
|
|
GetCPUArea().Area->InSyscallCallback = Prev;
|
|
}
|
|
};
|
|
|
|
bool ResetToConsistentStateImpl(EXCEPTION_RECORD* Exception, CONTEXT* GuestContext, ARM64_NT_CONTEXT* NativeContext) {
|
|
LogMan::Msg::DFmt("Exception: Code: {:X} Address: {:X}", Exception->ExceptionCode, reinterpret_cast<uintptr_t>(Exception->ExceptionAddress));
|
|
|
|
const auto CPUArea = GetCPUArea();
|
|
|
|
if (Exception->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
|
|
const auto FaultAddress = static_cast<uint64_t>(Exception->ExceptionInformation[1]);
|
|
|
|
bool HandledRWX = false;
|
|
if (InvalidationTracker && CPUArea.ThreadState()) {
|
|
std::scoped_lock Lock(ThreadCreationMutex);
|
|
HandledRWX = InvalidationTracker->HandleRWXAccessViolation(FaultAddress);
|
|
}
|
|
|
|
if (HandledRWX) {
|
|
LogMan::Msg::DFmt("Handled self-modifying code: pc: {:X} fault: {:X}", NativeContext->Pc, FaultAddress);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!CTX->IsAddressInCodeBuffer(CPUArea.ThreadState(), NativeContext->Pc) && !IsDispatcherAddress(NativeContext->Pc)) {
|
|
LogMan::Msg::DFmt("Passing through exception");
|
|
return false;
|
|
}
|
|
|
|
if (Exception->ExceptionCode == EXCEPTION_DATATYPE_MISALIGNMENT && Exception::HandleUnalignedAccess(*NativeContext)) {
|
|
LogMan::Msg::DFmt("Handled unaligned atomic: new pc: {:X}", NativeContext->Pc);
|
|
return true;
|
|
}
|
|
|
|
|
|
if (IsEmulatorStackAddress(reinterpret_cast<uint64_t>(__builtin_frame_address(0)))) {
|
|
Exception::RethrowGuestException(*Exception, *NativeContext);
|
|
LogMan::Msg::DFmt("Rethrowing onto guest stack: {:X}", NativeContext->Sp);
|
|
return true;
|
|
} else {
|
|
LogMan::Msg::EFmt("Unexpected exception in JIT code on guest stack");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
NTSTATUS ResetToConsistentState(EXCEPTION_RECORD* Exception, CONTEXT* GuestContext, ARM64_NT_CONTEXT* NativeContext) {
|
|
if (!GetCPUArea().ThreadState()) {
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
bool Cont {};
|
|
{
|
|
|
|
ScopedCallbackDisable guard;
|
|
Cont = ResetToConsistentStateImpl(Exception, GuestContext, NativeContext);
|
|
}
|
|
|
|
if (Cont) {
|
|
NtContinueNative(NativeContext, false);
|
|
}
|
|
|
|
GetCPUArea().Area->InSimulation = false;
|
|
GetCPUArea().Area->InSyscallCallback = false;
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
void NotifyMemoryAlloc(void* Address, SIZE_T Size, ULONG Type, ULONG Prot, BOOL After, NTSTATUS Status) {
|
|
if (!InvalidationTracker || !GetCPUArea().ThreadState()) {
|
|
return;
|
|
}
|
|
|
|
if (!After || Status) {
|
|
return;
|
|
}
|
|
|
|
std::scoped_lock Lock(ThreadCreationMutex);
|
|
InvalidationTracker->HandleMemoryProtectionNotification(reinterpret_cast<uint64_t>(Address), static_cast<uint64_t>(Size), Prot);
|
|
}
|
|
|
|
void NotifyMemoryFree(void* Address, SIZE_T Size, ULONG FreeType, BOOL After, NTSTATUS Status) {
|
|
if (!InvalidationTracker || !GetCPUArea().ThreadState()) {
|
|
return;
|
|
}
|
|
|
|
if (After) {
|
|
return;
|
|
}
|
|
|
|
std::scoped_lock Lock(ThreadCreationMutex);
|
|
if (FreeType & MEM_DECOMMIT) {
|
|
InvalidationTracker->InvalidateAlignedInterval(reinterpret_cast<uint64_t>(Address), static_cast<uint64_t>(Size), true);
|
|
} else if (FreeType & MEM_RELEASE) {
|
|
InvalidationTracker->InvalidateContainingSection(reinterpret_cast<uint64_t>(Address), true);
|
|
}
|
|
}
|
|
|
|
void NotifyMemoryProtect(void* Address, SIZE_T Size, ULONG NewProt, BOOL After, NTSTATUS Status) {
|
|
if (!InvalidationTracker || !GetCPUArea().ThreadState()) {
|
|
return;
|
|
}
|
|
|
|
if (!After || Status) {
|
|
return;
|
|
}
|
|
|
|
std::scoped_lock Lock(ThreadCreationMutex);
|
|
InvalidationTracker->HandleMemoryProtectionNotification(reinterpret_cast<uint64_t>(Address), static_cast<uint64_t>(Size), NewProt);
|
|
}
|
|
|
|
NTSTATUS NotifyMapViewOfSection(void* Unk1, void* Address, void* Unk2, SIZE_T Size, ULONG AllocType, ULONG Prot) {
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
void NotifyUnmapViewOfSection(void* Address, BOOL After, NTSTATUS Status) {
|
|
if (!InvalidationTracker || !GetCPUArea().ThreadState()) {
|
|
return;
|
|
}
|
|
|
|
if (After) {
|
|
return;
|
|
}
|
|
|
|
std::scoped_lock Lock(ThreadCreationMutex);
|
|
InvalidationTracker->InvalidateContainingSection(reinterpret_cast<uint64_t>(Address), true);
|
|
}
|
|
|
|
void FlushInstructionCacheHeavy(const void* Address, SIZE_T Size) {
|
|
if (!InvalidationTracker || !GetCPUArea().ThreadState()) {
|
|
return;
|
|
}
|
|
|
|
std::scoped_lock Lock(ThreadCreationMutex);
|
|
InvalidationTracker->InvalidateAlignedInterval(reinterpret_cast<uint64_t>(Address), static_cast<uint64_t>(Size), false);
|
|
}
|
|
|
|
void BTCpu64FlushInstructionCache(const void* Address, SIZE_T Size) {
|
|
if (!InvalidationTracker || !GetCPUArea().ThreadState()) {
|
|
return;
|
|
}
|
|
|
|
std::scoped_lock Lock(ThreadCreationMutex);
|
|
InvalidationTracker->InvalidateAlignedInterval(reinterpret_cast<uint64_t>(Address), static_cast<uint64_t>(Size), false);
|
|
}
|
|
|
|
void BTCpu64NotifyMemoryDirty(void* Address, SIZE_T Size) {
|
|
if (!InvalidationTracker || !GetCPUArea().ThreadState()) {
|
|
return;
|
|
}
|
|
|
|
std::scoped_lock Lock(ThreadCreationMutex);
|
|
InvalidationTracker->InvalidateAlignedInterval(reinterpret_cast<uint64_t>(Address), static_cast<uint64_t>(Size), false);
|
|
}
|
|
|
|
void BTCpu64NotifyReadFile(HANDLE Handle, void* Address, SIZE_T Size, BOOL After, NTSTATUS Status) {}
|
|
|
|
NTSTATUS ThreadInit() {
|
|
FEX::Windows::InitCRTThread();
|
|
static constexpr size_t EmulatorStackSize = 0x40000;
|
|
const uint64_t EmulatorStack = reinterpret_cast<uint64_t>(::VirtualAlloc(nullptr, EmulatorStackSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
|
|
GetCPUArea().EmulatorStackLimit() = EmulatorStack;
|
|
GetCPUArea().EmulatorStackBase() = EmulatorStack + EmulatorStackSize;
|
|
|
|
const auto CPUArea = GetCPUArea();
|
|
|
|
auto* Thread = CTX->CreateThread(0, 0);
|
|
Thread->CurrentFrame->Pointers.Common.ExitFunctionEC = reinterpret_cast<uintptr_t>(&ExitFunctionEC);
|
|
CPUArea.StateFrame() = Thread->CurrentFrame;
|
|
|
|
uint64_t EnterEC = Thread->CurrentFrame->Pointers.Common.DispatcherLoopTopEnterEC;
|
|
CPUArea.DispatcherLoopTopEnterEC() = EnterEC;
|
|
|
|
uint64_t EnterECFillSRA = Thread->CurrentFrame->Pointers.Common.DispatcherLoopTopEnterECFillSRA;
|
|
CPUArea.DispatcherLoopTopEnterECFillSRA() = EnterECFillSRA;
|
|
|
|
CPUArea.ContextAmd64() = {.ContextFlags = CONTEXT_CONTROL | CONTEXT_SEGMENTS | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT,
|
|
.AMD64_SegCs = 0x33,
|
|
.AMD64_SegDs = 0x2b,
|
|
.AMD64_SegEs = 0x2b,
|
|
.AMD64_SegFs = 0x53,
|
|
.AMD64_SegGs = 0x2b,
|
|
.AMD64_SegSs = 0x2b,
|
|
.AMD64_EFlags = 0x202,
|
|
.AMD64_MxCsr = 0x1f80,
|
|
.AMD64_MxCsr_copy = 0x1f80,
|
|
.AMD64_ControlWord = 0x27f};
|
|
Exception::LoadStateFromECContext(Thread, CPUArea.ContextAmd64().AMD64_Context);
|
|
|
|
{
|
|
std::scoped_lock Lock(ThreadCreationMutex);
|
|
Threads.emplace(GetCurrentThreadId(), Thread);
|
|
}
|
|
|
|
CPUArea.ThreadState() = Thread;
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
NTSTATUS ThreadTerm(HANDLE Thread, LONG ExitCode) {
|
|
const auto [Err, CPUArea] = GetThreadCPUArea(Thread);
|
|
if (Err) {
|
|
return Err;
|
|
}
|
|
auto* OldThreadState = CPUArea.ThreadState();
|
|
CPUArea.ThreadState() = nullptr;
|
|
|
|
{
|
|
THREAD_BASIC_INFORMATION Info;
|
|
if (NTSTATUS Err = NtQueryInformationThread(Thread, ThreadBasicInformation, &Info, sizeof(Info), nullptr); Err) {
|
|
return Err;
|
|
}
|
|
|
|
const auto ThreadTID = reinterpret_cast<uint64_t>(Info.ClientId.UniqueThread);
|
|
std::scoped_lock Lock(ThreadCreationMutex);
|
|
Threads.erase(ThreadTID);
|
|
}
|
|
|
|
CTX->DestroyThread(OldThreadState);
|
|
::VirtualFree(reinterpret_cast<void*>(GetCPUArea().EmulatorStackLimit()), 0, MEM_RELEASE);
|
|
FEX::Windows::DeinitCRTThread();
|
|
return STATUS_SUCCESS;
|
|
}
|
|
|
|
BOOLEAN BTCpu64IsProcessorFeaturePresent(UINT Feature) {
|
|
return CPUFeatures->IsFeaturePresent(Feature) ? TRUE : FALSE;
|
|
}
|
|
|
|
void UpdateProcessorInformation(SYSTEM_CPU_INFORMATION* Info) {
|
|
CPUFeatures->UpdateInformation(Info);
|
|
}
|