Windows: Don't hardcode NT syscall numbers for ARM64EC

This commit is contained in:
Justin Becker committed 2026-09-29 11:24:36 -07:00
1 parent 08f451d3bc
commit aad2c969b7
2 files changed
+98 -10

No files matched your search

+28 -7
View File
@@ -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.
+70 -3
View File
@@ -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<IMAGE_ARM64EC_METADATA*>(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<IMAGE_ARM64EC_REDIRECTION_ENTRY*>(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<uintptr_t>(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<uintptr_t*>(GetProcAddress(NtDll, "__arm64ec_syscall_ffs"));
const uint32_t* SyscallImplementationCount = reinterpret_cast<uint32_t*>(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();