// SPDX-License-Identifier: MIT /* $info$ tags: Bin|ARM64EC desc: Implements the ARM64EC BT module API using FEXCore $end_info$ */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Common/Config.h" #include "Common/InvalidationTracker.h" #include "Common/TSOHandlerConfig.h" #include "Common/CPUFeatures.h" #include "DummyHandlers.h" #include "BTInterface.h" #include #include #include #include #include #include #include #include #include #include #include #include class ECSyscallHandler; void* X64ReturnInstr; // See Module.S extern void* ExitFunctionEC; struct ThreadCPUArea { static constexpr size_t TEBCPUAreaOffset = 0x1788; CHPE_V2_CPU_AREA_INFO* Area; explicit ThreadCPUArea(_TEB* TEB) : Area(*reinterpret_cast(reinterpret_cast(TEB) + TEBCPUAreaOffset)) {} uint64_t EmulatorStackLimit() const { return Area->EmulatorStackLimit; } uint64_t EmulatorStackBase() const { return Area->EmulatorStackBase; } FEXCore::Core::CpuStateFrame*& StateFrame() const { return reinterpret_cast(Area->EmulatorData[0]); } FEXCore::Core::InternalThreadState*& ThreadState() const { return reinterpret_cast(Area->EmulatorData[1]); } uint64_t& DispatcherLoopTopEnterEC() const { return reinterpret_cast(Area->EmulatorData[2]); } uint64_t& DispatcherLoopTopEnterECFillSRA() const { return reinterpret_cast(Area->EmulatorData[3]); } }; namespace { fextl::unique_ptr CTX; fextl::unique_ptr SignalDelegator; fextl::unique_ptr SyscallHandler; std::optional InvalidationTracker; std::optional CPUFeatures; std::recursive_mutex ThreadCreationMutex; // Map of TIDs to their FEX thread state, `ThreadCreationMutex` must be locked when accessing std::unordered_map Threads; std::pair 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; } // GetProcAddress on ARM64EC returns a pointer to an x64 fast forward sequence to allow for redirecting to the JIT if functions are hotpatched. // This looks up the procedure address of the native code even if the fast forward sequence has been patched. uintptr_t GetRedirectedProcAddress(HMODULE Module, const char* ProcName) { const uintptr_t Proc = reinterpret_cast(GetProcAddress(Module, ProcName)); if (!Proc) { return 0; } 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(LoadConfig->CHPEMetadataPointer); const uintptr_t ModuleBase = reinterpret_cast(Module); const uintptr_t ProcRVA = Proc - ModuleBase; const auto* RedirectionTableBegin = reinterpret_cast(ModuleBase + CHPEMetadata->RedirectionMetadata); const auto* RedirectionTableEnd = RedirectionTableBegin + CHPEMetadata->RedirectionMetadataCount; const auto* It = std::lower_bound(RedirectionTableBegin, RedirectionTableEnd, ProcRVA, [](const auto& Entry, uintptr_t RVA) { return Entry.Source < RVA; }); if (It->Source != ProcRVA) { return 0; } return ModuleBase + It->Destination; } } // namespace namespace Exception { static std::optional HandlerConfig; 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; } } // namespace Exception namespace Logging { static void MsgHandler(LogMan::DebugLevels Level, const char* Message) { const auto Output = fextl::fmt::format("[{}][{:X}] {}\n", LogMan::DebugLevelStr(Level), GetCurrentThreadId(), Message); __wine_dbg_output(Output.c_str()); } static void AssertHandler(const char* Message) { const auto Output = fextl::fmt::format("[ASSERT] {}\n", Message); __wine_dbg_output(Output.c_str()); } static void Init() { LogMan::Throw::InstallHandler(AssertHandler); LogMan::Msg::InstallHandler(MsgHandler); } } // namespace Logging class ECSyscallHandler : public FEXCore::HLE::SyscallHandler, public FEXCore::Allocator::FEXAllocOperators { public: ECSyscallHandler() { OSABI = FEXCore::HLE::SyscallOSABI::OS_WIN32; } uint64_t HandleSyscall(FEXCore::Core::CpuStateFrame* Frame, FEXCore::HLE::SyscallArguments* Args) override { 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); } }; void ProcessInit() { Logging::Init(); FEX::Config::InitializeConfigs(); FEXCore::Config::Initialize(); FEXCore::Config::AddLayer(FEX::Config::CreateGlobalMainLayer()); FEXCore::Config::AddLayer(FEX::Config::CreateMainLayer()); FEXCore::Config::Load(); FEXCore::Config::ReloadMetaLayer(); 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(); SyscallHandler = fextl::make_unique(); Exception::HandlerConfig.emplace(); CTX = FEXCore::Context::Context::CreateNewContext(); 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(X64ReturnInstr) = 0xc3; } void ProcessTerm() {} class ScopedCallbackDisable { private: bool Prev; public: ScopedCallbackDisable() { Prev = GetCPUArea().Area->InSyscallCallback; GetCPUArea().Area->InSyscallCallback = true; } ~ScopedCallbackDisable() { GetCPUArea().Area->InSyscallCallback = Prev; } }; NTSTATUS ResetToConsistentState(EXCEPTION_POINTERS* Ptrs, ARM64_NT_CONTEXT* Context, BOOLEAN* Continue) { ScopedCallbackDisable Guard; const auto* Exception = Ptrs->ExceptionRecord; if (Exception->ExceptionCode == EXCEPTION_DATATYPE_MISALIGNMENT && Exception::HandleUnalignedAccess(*Context)) { LogMan::Msg::DFmt("Handled unaligned atomic: new pc: {:X}", Context->Pc); *Continue = true; return STATUS_SUCCESS; } if (Exception->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) { const auto FaultAddress = static_cast(Exception->ExceptionInformation[1]); bool HandledRWX = false; if (InvalidationTracker && GetCPUArea().ThreadState()) { std::scoped_lock Lock(ThreadCreationMutex); HandledRWX = InvalidationTracker->HandleRWXAccessViolation(FaultAddress); } if (HandledRWX) { LogMan::Msg::DFmt("Handled self-modifying code: pc: {:X} fault: {:X}", Context->Pc, FaultAddress); *Continue = true; return STATUS_SUCCESS; } } if (!CTX->IsAddressInCodeBuffer(GetCPUArea().ThreadState(), Context->Pc) && !IsDispatcherAddress(Context->Pc)) { return STATUS_SUCCESS; } LogMan::Msg::EFmt("Exception rethrow is unimplemented"); return STATUS_SUCCESS; } void NotifyMemoryAlloc(void* Address, SIZE_T Size, ULONG Type, ULONG Prot) { if (!InvalidationTracker || !GetCPUArea().ThreadState()) { return; } std::scoped_lock Lock(ThreadCreationMutex); InvalidationTracker->HandleMemoryProtectionNotification(reinterpret_cast(Address), static_cast(Size), Prot); } void NotifyMemoryFree(void* Address, SIZE_T Size, ULONG FreeType) { if (!InvalidationTracker || !GetCPUArea().ThreadState()) { return; } std::scoped_lock Lock(ThreadCreationMutex); if (!Size) { InvalidationTracker->InvalidateContainingSection(reinterpret_cast(Address), true); } else if (FreeType & MEM_DECOMMIT) { InvalidationTracker->InvalidateAlignedInterval(reinterpret_cast(Address), static_cast(Size), true); } } void NotifyMemoryProtect(void* Address, SIZE_T Size, ULONG NewProt) { if (!InvalidationTracker || !GetCPUArea().ThreadState()) { return; } std::scoped_lock Lock(ThreadCreationMutex); InvalidationTracker->HandleMemoryProtectionNotification(reinterpret_cast(Address), static_cast(Size), NewProt); } void NotifyUnmapViewOfSection(void* Address) { if (!InvalidationTracker || !GetCPUArea().ThreadState()) { return; } std::scoped_lock Lock(ThreadCreationMutex); InvalidationTracker->InvalidateContainingSection(reinterpret_cast(Address), true); } void BTCpu64FlushInstructionCache(const void* Address, SIZE_T Size) { if (!InvalidationTracker || !GetCPUArea().ThreadState()) { return; } std::scoped_lock Lock(ThreadCreationMutex); InvalidationTracker->InvalidateAlignedInterval(reinterpret_cast(Address), static_cast(Size), false); } NTSTATUS ThreadInit() { const auto CPUArea = GetCPUArea(); auto* Thread = CTX->CreateThread(0, 0); Thread->CurrentFrame->Pointers.Common.ExitFunctionEC = reinterpret_cast(&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; { std::scoped_lock Lock(ThreadCreationMutex); Threads.emplace(GetCurrentThreadId(), Thread); } CPUArea.ThreadState() = Thread; return STATUS_SUCCESS; } NTSTATUS ThreadTerm(HANDLE Thread) { 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(Info.ClientId.UniqueThread); std::scoped_lock Lock(ThreadCreationMutex); Threads.erase(ThreadTID); } CTX->DestroyThread(OldThreadState); return STATUS_SUCCESS; } BOOLEAN BTCpu64IsProcessorFeaturePresent(UINT Feature) { return CPUFeatures->IsFeaturePresent(Feature) ? TRUE : FALSE; } void UpdateProcessorInformation(SYSTEM_CPU_INFORMATION* Info) { CPUFeatures->UpdateInformation(Info); }