diff --git a/Source/Windows/ARM64EC/Module.S b/Source/Windows/ARM64EC/Module.S index 4a59cf650..65b2cd985 100644 --- a/Source/Windows/ARM64EC/Module.S +++ b/Source/Windows/ARM64EC/Module.S @@ -101,9 +101,26 @@ ExitFunctionSuspendPoint: brk #0xCAFE // Resume will jump back to `ExitFunctionSuspendResumePoint` +#ifdef ARCHITECTURE_arm64ec + // This table will contain all of the possible SVC #x instructions that we might need. + // We index into this table in DIRECT_SYSCALL_WRAPPER based on the syscall number we find + // by parsing the table __arm64ec_syscall_ffs from NTDLL. +#define SYSCALL_TABLE_ENTRIES 512 +.global SyscallTable +SyscallTable: + .set SyscallId, 0 + .rept SYSCALL_TABLE_ENTRIES + svc #SyscallId + ret + .set SyscallId, SyscallId + 1 + .endr +.global SyscallTableEnd +SyscallTableEnd: +#endif + // Makes a wrapper for calling a system call directly, skipping the usual ntdll thunks #define HASH # -#define DIRECT_SYSCALL_WRAPPER(Name, WineIdName, WindowsId) \ +#define DIRECT_SYSCALL_WRAPPER(Name, WineIdName) \ .global Name; \ Name:; \ adrp x16, WineSyscallDispatcher; \ @@ -115,19 +132,23 @@ ExitFunctionSuspendPoint: blr x16; \ ret; \ 1:; \ - svc HASH WindowsId; \ - ret + adrp x16, SyscallTable; \ + add x16, x16, HASH:lo12:SyscallTable; \ + adrp x17, WineIdName; \ + ldr x17, [x17, HASH:lo12:WineIdName]; \ + add x16, x16, x17, lsl HASH 3; \ + br x16 // Allows for continuing from a full native context, as the NTDLL NtContinue export takes in an x64 context with EC and // the conversion to that loses the ARM64EC ABI-disallowed registers that FEX uses. -DIRECT_SYSCALL_WRAPPER("#NtContinueNative", WineNtContinueSyscallId, 0x43) -DIRECT_SYSCALL_WRAPPER("#NtRaiseExceptionNative", WineNtRaiseExceptionSyscallId, 0x174) +DIRECT_SYSCALL_WRAPPER("#NtContinueNative", WineNtContinueSyscallId) +DIRECT_SYSCALL_WRAPPER("#NtRaiseExceptionNative", WineNtRaiseExceptionSyscallId) // Both of these are wrapped as FEX needs them to setup its call checker at startup time and their NTDLL thunks could // already be patched by then (and because the call checker isn't installed, their patched x86 versions would be invoked // when called by FEX). -DIRECT_SYSCALL_WRAPPER("#NtAllocateVirtualMemoryNative", WineNtAllocateVirtualMemorySyscallId, 0x18) -DIRECT_SYSCALL_WRAPPER("#NtProtectVirtualMemoryNative", WineNtProtectVirtualMemorySyscallId, 0x50) +DIRECT_SYSCALL_WRAPPER("#NtAllocateVirtualMemoryNative", WineNtAllocateVirtualMemorySyscallId) +DIRECT_SYSCALL_WRAPPER("#NtProtectVirtualMemoryNative", WineNtProtectVirtualMemorySyscallId) // A replacement for the standard ARM64EC call checker that ignores any FFS patches and always redirects to a function's // native implementation. As the only library FEX calls into is NTDLL, this is done using a LUT generated at init time. diff --git a/Source/Windows/ARM64EC/Module.cpp b/Source/Windows/ARM64EC/Module.cpp index 93a3ab098..2d6edcfe4 100644 --- a/Source/Windows/ARM64EC/Module.cpp +++ b/Source/Windows/ARM64EC/Module.cpp @@ -74,6 +74,10 @@ extern void* ExitFunctionEC; extern void* CheckCall; extern void* ExitFunctionSuspendPoint; extern void* ExitFunctionSuspendResumePoint; +#ifdef ARCHITECTURE_arm64ec +extern uint64_t SyscallTable[]; +extern uint64_t SyscallTableEnd[]; +#endif void* X64ReturnInstr; // See Module.S uintptr_t NtDllBase; @@ -82,7 +86,7 @@ uintptr_t NtDllBase; 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 +// Wine doesn't support issuing direct system calls with SVC, and like Windows it also doesn't have 'stable' syscall numbers void* WineSyscallDispatcher; uint64_t WineNtContinueSyscallId; uint64_t WineNtAllocateVirtualMemorySyscallId; @@ -268,11 +272,53 @@ void ParseWineSyscallNumbers(HMODULE NtDll) { } } +#ifdef ARCHITECTURE_arm64ec +uint64_t GetNTSyscallNo(HMODULE NtDll, const char* ExportName) { + ULONG LoadConfigSize; + const _IMAGE_LOAD_CONFIG_DIRECTORY64* NtDllLoadConfig = reinterpret_cast<_IMAGE_LOAD_CONFIG_DIRECTORY64*>( + RtlImageDirectoryEntryToData(NtDll, true, IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG, &LoadConfigSize)); + + const IMAGE_ARM64EC_METADATA* NtDllARM64ECMetadata = reinterpret_cast(NtDllLoadConfig->CHPEMetadataPointer); + + // This redirection table stores a mapping from x64 entry points for fast forward sequences, + // to the real ARM64EC implementation of the function. + const IMAGE_ARM64EC_REDIRECTION_ENTRY* RedirectionTableBegin = + reinterpret_cast(NtDllBase + NtDllARM64ECMetadata->RedirectionMetadata); + const IMAGE_ARM64EC_REDIRECTION_ENTRY* RedirectionTableEnd = RedirectionTableBegin + NtDllARM64ECMetadata->RedirectionMetadataCount; + + // Walk through the table until we find an entry matching the x64 FFS. + const uintptr_t x64FFS = reinterpret_cast(GetProcAddress(NtDll, ExportName)) - NtDllBase; + uintptr_t ARM64ECImplementation = 0; + for (const IMAGE_ARM64EC_REDIRECTION_ENTRY* RedirectionEntry = RedirectionTableBegin; RedirectionEntry != RedirectionTableEnd; + RedirectionEntry++) { + if (RedirectionEntry->Source == x64FFS) { + ARM64ECImplementation = NtDllBase + RedirectionEntry->Destination; + break; + } + } + + // Now walk through the syscall FFS table and find which entry points to the ARM64EC implementation + // we found in the previous table. The index of this entry in that table will be the syscall number. + const uintptr_t* SyscallImplementationTable = reinterpret_cast(GetProcAddress(NtDll, "__arm64ec_syscall_ffs")); + const uint32_t* SyscallImplementationCount = reinterpret_cast(GetProcAddress(NtDll, "__arm64ec_syscall_ffs_size")); + if (!ARM64ECImplementation || !SyscallImplementationTable || !SyscallImplementationCount) { + return -1; + } + + for (uint64_t SyscallNo = 0; SyscallNo < *SyscallImplementationCount; SyscallNo++) { + if (SyscallImplementationTable[SyscallNo] == ARM64ECImplementation) { + return SyscallNo; + } + } + return -1; +} +#endif + // Syscall thunks may have been patched before FEX has loaded, the default call checker installed by ntdll into FEX will // try to invoke the JIT when calling such patched syscalls but this obviously doesn't work before FEX is initalised. // This function parses ntdll and sets up a custom call checker to prevent this, as such it must avoid using any syscall // thunks itself. -void InitSyscalls() { +bool InitSyscalls() { // The ntdll exports called by GetModuleHandle/GetProcAddress aren't known to be patched before JIT init by any current // software so are safe to call, but if that changes the loader structures in the PEB could be parsed manually. const auto NtDll = GetModuleHandleW(L"ntdll.dll"); @@ -282,10 +328,29 @@ void InitSyscalls() { if (WineSyscallDispatcherPtr) { WineSyscallDispatcher = *WineSyscallDispatcherPtr; ParseWineSyscallNumbers(NtDll); + } else { +#ifdef ARCHITECTURE_arm64ec + // NT syscall numbers may change between versions, so we need to find the numbers + // for the syscalls we need ahead of time. + WineNtContinueSyscallId = GetNTSyscallNo(NtDll, "NtContinue"); + WineNtAllocateVirtualMemorySyscallId = GetNTSyscallNo(NtDll, "NtAllocateVirtualMemory"); + WineNtProtectVirtualMemorySyscallId = GetNTSyscallNo(NtDll, "NtProtectVirtualMemory"); + WineNtRaiseExceptionSyscallId = GetNTSyscallNo(NtDll, "NtRaiseException"); + + // Fail if the syscall number we found is beyond the bounds + // of our static table. This is almost certainly not going + // to happen, but failing here will make debugging easier later on. + const uint64_t SyscallTableSize = SyscallTableEnd - SyscallTable; + if (std::max({WineNtContinueSyscallId, WineNtAllocateVirtualMemorySyscallId, WineNtProtectVirtualMemorySyscallId, + WineNtRaiseExceptionSyscallId}) >= SyscallTableSize) { + return false; + } +#endif } FillNtDllLUTs(NtDll); PatchCallChecker(); + return true; } void HandleImageMap(uint64_t Address, bool MainImage = false) { @@ -580,7 +645,9 @@ extern "C" void SyncThreadContext(CONTEXT* Context) { } NTSTATUS ProcessInit() { - InitSyscalls(); + if (!InitSyscalls()) { + return STATUS_NOT_SUPPORTED; + } FEX::Windows::InitCRTProcess(); FEX::Windows::SetupThreadHandlers();