Windows 11 ARM x86/x64 Emulation Layer Internals
Windows 11 ARM x86/x64 Emulation Layer Internals
How does Windows 11 on Arm run x64 programs? Microsoft uses dynamic binary translation (DBT) to JIT-compile x86/x64 instructions into ARM64 instructions in user mode. This article dives into the Prism emulator's components, JIT flow, caching, and limitations.
Emulator Components
Windows 11 ARM ships with multiple emulators (all running in user mode):
| Component | Location | Function |
|---|---|---|
xtajit64.dll | C:\Windows\System32\ | x64 -> ARM64 JIT compilation |
xtajit64se.dll | KnownDLLs | x64 secure variant |
wow64.dll | KnownDLLs | WoW64 core infrastructure |
wow64win.dll | KnownDLLs | Win32k syscall thunk |
Verified on an EC2 Graviton instance running Windows 11 ARM:
xtajit64.dllexists (1,451,096 bytes)SysWOW64\xtajit.dll(x86 emulator) does not existwowarmhw.dll(ARM32 emulator) does not exist
x86 emulation may be handled by xtajit64.dll uniformly, or this system version does not include full x86 emulation support.
JIT Compilation Flow
x64 process startup
|- Loader detects PE as x64
|- Create process, map x64 EXE and DLLs
|- Load xtajit64.dll into process address space
`- Initialize translation cache (XtaCache)
First execution of a code block
|- CPU reaches an x64 instruction address
|- Trigger emulator entry (via Entry Thunk)
|- xtajit64 reads the x64 instruction stream
|- Decode, optimize, generate ARM64 equivalent
|- Write to executable memory
`- Jump to translated ARM64 code
Subsequent execution
|- Check cache hit
|- Hit -> execute cached ARM64 code directly
`- Miss -> trigger new JIT compilationTranslation Blocks
The JIT translates at the basic-block level — a linear sequence of instructions ending with a jump, call, or return. Each basic block becomes a corresponding ARM64 code block written to cache.
High cache hit rate is key to performance. Hot paths almost always hit cache; JIT compilation is only triggered on first execution or cache eviction.
Syscall Thunks
When an x64 program calls a Win32 API, parameters and calling conventions must be converted:
- The x64 program makes a syscall via
wow64win.dll wow64win.dllconverts the x64 calling convention (register-based params) to ARM64- Calls the real ARM64 system service (ntdll.dll, etc.)
- Return values are converted back to x64 format
This is transparent to the app but adds a small overhead. APIs like GetNativeSystemInfo return virtualized information in emulated processes, making the app "think" it runs on x64.
Instruction Set Support
Prism supports the vast majority of x86/x64 instructions, including:
- Integer, logical, and bit operations
- SSE/SSE2/SSE3/SSE4 (emulated via NEON/SVE)
- AVX/AVX2 (partial support, with performance loss)
AVX-512-dependent apps are not supported.
Kernel Driver Limitations
The emulator only applies to user-mode code:
- The Windows kernel itself is compiled natively for ARM64; kernel-mode code runs directly on the ARM64 CPU
- Kernel code runs at high IRQL and cannot switch to the emulator context
- Kernel drivers must pass Microsoft ARM64 signature validation
Therefore x64 drivers (.sys) cannot load. The Sysmon x64 kernel driver SysmonDrv.sys failing to load is exactly this reason.
Performance Overhead Sources
| Source | Description |
|---|---|
| JIT compilation | Translation time on first execution |
| Cache lookup | Check cache before each execution |
| Thunk overhead | Parameter conversion on cross-architecture calls |
| Instruction semantics | Some x64 instructions need multiple ARM64 instructions |
| Memory model differences | Some atomic operations need extra handling |
Typical overhead:
- Compute-bound (crypto, compression): +20%~40%
- IO-bound (file ops, networking): +5%~15%
- Mixed: +10%~30%
Diagnostic Commands
Inspect process architecture distribution
$x64 = 0; $arm64 = 0; $x86 = 0
Get-Process | ForEach-Object {
try {
$m = $_.MainModule.FileName
if ($m) {
$pe = [System.IO.File]::ReadAllBytes($m)
$off = [BitConverter]::ToInt32($pe, 60)
$mach = [BitConverter]::ToUInt16($pe, $off + 4)
switch ($mach) {
0x8664 { $x64++ }
0xAA64 { $arm64++ }
0x14c { $x86++ }
}
}
} catch {}
}
Write-Host "ARM64: $arm64, x64: $x64, x86: $x86"Detect whether the current process is emulated
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class Wow64 {
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool IsWow64Process2(
IntPtr hProcess,
out ushort pProcessMachine,
out ushort pNativeMachine);
}
"@
$proc = $native = 0
[Wow64]::IsWow64Process2([IntPtr](-1), [ref]$proc, [ref]$native)
Write-Host "ProcessMachine: 0x$($proc.ToString('X4')), NativeMachine: 0x$($native.ToString('X4'))"
# 0x0000 = native, 0x8664 = x64 emulated, 0x14c = x86 emulated, 0xAA64 = ARM64 native systemVerify xtajit64.dll exists
Get-Item C:\Windows\System32\xtajit64.dll | Select-Object FullName, Length, LastWriteTime
# Length: 1451096