[KongchangAI]
· 2 min read· 1,323 words

The Plight of x86 Emulation: Why Running x86 Programs on ARM Is So Hard

The Plight of x86 Emulation: Why Running x86 Programs on ARM Is So Hard

FEX-Emu's deep dive reveals the core technical obstacles behind running x86 software on ARM hardware.

The FEX-Emu team's article *The Scourge of x86 Emulation* systematically exposes the deep challenges of emulating x86 on ARM64. Three core problems stand out: the fundamental mismatch between x86's strong TSO memory model and ARM's weak memory model forces emulators to insert costly memory barriers; x86's implicit flag-setting mechanism has no direct ARM equivalent, requiring lazy evaluation strategies; and x87's 80-bit extended precision is unsupported natively on ARM, making accurate emulation expensive. As Apple Silicon and Windows on ARM mature, open-source projects like FEX-Emu transparently document these engineering tradeoffs for the broader developer community.

As ARM architecture becomes increasingly prevalent — from Apple Silicon Macs and Snapdragon-powered Windows laptops to a wide range of embedded devices — a common challenge persists: how to run the vast library of x86 software smoothly on ARM hardware. The FEX-Emu project team recently published a technical article titled The Scourge of x86 Emulation, which dives deep into the technical difficulties of x86 emulation. The post garnered 246 upvotes and 67 comments on Hacker News, sparking widespread discussion in the developer community.

Why x86 Emulation Is Called a "Scourge"

FEX-Emu is an open-source project focused on emulating x86 and x86-64 instruction sets on ARM64 (AArch64) platforms. The word "scourge" in the article title vividly captures the deep-seated difficulties of emulating x86.

x86 is not a "clean" architecture. Decades of evolution have left it burdened with historical baggage: complex instruction encoding, variable-length instructions, rich addressing modes, and an entire set of memory model and flag-handling mechanisms that differ fundamentally from ARM. When developers attempt to faithfully reproduce these behaviors on ARM, even the smallest deviation in any detail can cause the emulated program to crash or behave incorrectly.

For emulators, correctness and performance are often a difficult tradeoff. Achieving behavior fully consistent with real x86 hardware requires enormous performance costs, while optimizations made in pursuit of speed can introduce subtle bugs in edge cases.

Fundamental Differences in Memory Models

The difference in memory consistency models between x86 and ARM is one of the most challenging problems in emulation work. x86 uses a relatively strong memory ordering model — TSO (Total Store Order) — while ARM uses a more relaxed weak memory model.

This means that memory access ordering guaranteed by x86 hardware does not automatically hold on ARM. A multithreaded program that runs correctly on x86 hardware, when directly translated to ARM, may exhibit data races and hard-to-reproduce bugs due to memory reordering. To ensure correctness, the emulator is forced to insert a large number of memory barrier instructions during translation — and these extra barriers severely drag down execution performance.

This tension is the concentrated essence of x86 emulation's "scourge": every emulator designer must face the choice between sacrificing performance for correctness or risking correctness for performance.


The core characteristic of the TSO model is that each processor core behaves as if it has a FIFO write buffer, making writes appear "immediately globally visible" and ordered to other cores. Specifically, x86 guarantees that all cores observe write operations in program order, that reads cannot be reordered past subsequent writes, and that they cannot be reordered past writes from other cores. This allows x86 programmers to write multithreaded code without explicitly inserting memory barriers to ensure basic sequential consistency.

ARM's weakly-ordered memory model is entirely different: the processor is free to reorder read and write operations as long as single-threaded semantics are preserved. This greatly increases the potential for out-of-order execution and pipeline optimization, but it also means multithreaded programs must use explicit synchronization primitives (such as dmb and dsb barrier instructions, or atomic operations with acquire/release semantics) to guarantee cross-thread visibility. If an emulator wants x86 multithreaded programs to be "unaware" of the underlying architecture change, it must determine at each memory access translation whether a barrier needs to be added — and this determination itself consumes additional runtime resources.

Precise Reproduction of Flags and Floating-Point Behavior

Beyond the memory model, x86's flag handling is another major challenge. Many x86 instructions implicitly set status flags (such as carry, overflow, and zero flags), and subsequent instructions may depend on those flags. ARM's flag semantics do not map directly to x86's.

If the emulator precisely calculates and updates all flags for every instruction, the overhead becomes enormous. Projects like FEX therefore often employ a "lazy flag evaluation" strategy — only computing flags when they actually need to be read. While this optimization can significantly improve performance, it also greatly increases implementation complexity.

Floating-point arithmetic presents its own pitfalls. The x87 floating-point unit uses 80-bit extended precision, while modern ARM floating-point units typically only offer 64-bit double precision. This precision gap can lead to subtle differences in numerical computation results, which may cause visible problems in scientific computing or game physics engines that rely on precise floating-point behavior.


The x87 floating-point unit was introduced by Intel in 1980 with the 8087 coprocessor. Its 80-bit extended precision format (64-bit mantissa + 15-bit exponent + 1 sign bit) was designed to reduce accumulated errors in intermediate calculations. This format is still used by the x86 ABI in certain scenarios for default floating-point computation, particularly for the long double type in C. However, the SSE2 instruction set (introduced with the Pentium 4 in 2001) brought IEEE 754-compliant 32/64-bit floating-point operations, and modern x86 compilers have long since switched to SSE2 by default, making x87 instructions rare in new code. Nonetheless, a large number of legacy binaries still contain x87 instructions, forcing emulators to maintain a complete x87 emulation path. ARM's NEON/FP unit strictly adheres to IEEE 754 double-precision specifications and does not support 80-bit extended precision. So when an emulator encounters x87 instructions, it must either simulate 80-bit precision in software (at an extremely high performance cost) or approximate with 64-bit precision (risking numerical deviation).

As Apple completes its transition to its own silicon and the Windows on ARM ecosystem gradually matures, binary translation technology from x86 to ARM is becoming more important than ever. Apple's Rosetta 2, Microsoft's Prism, and open-source projects like FEX-Emu and Box64 are all advancing this technology in their respective domains.

As an open-source solution, FEX-Emu's value lies not only in enabling Linux users to run x86 games and applications on ARM devices, but also in its transparent, public demonstration of these underlying technical challenges. Unlike the black-box implementations of commercial solutions, open-source projects allow the entire community to understand, scrutinize, and improve emulation technology.

The lively discussion on Hacker News also reflects that, as ARM's share in desktop and server markets continues to grow, how to gracefully handle the massive legacy of x86 software will be an unavoidable topic in computing for years to come. x86 emulation may be a "scourge," but it is also a critical bridge connecting past and future computing architectures.


Binary translation technology falls into two main categories: static translation (Ahead-of-Time, AOT) and dynamic translation (Just-in-Time, JIT). Static translation converts x86 instructions to ARM instructions and stores them before the program runs. Its advantage is that translation overhead doesn't consume runtime resources; its disadvantage is that it cannot handle self-modifying code or code dynamically generated at runtime, and it cannot leverage runtime profiling data for optimization. Dynamic translation translates in real time during program execution. It can handle dynamically generated code and can perform deep optimization on frequently executed code blocks through "hot path detection" (such as register allocation optimization and multi-instruction fusion), but the translation itself consumes CPU and memory resources. FEX-Emu uses a JIT approach, while Rosetta 2 employs both AOT and JIT modes — triggering AOT translation and caching on first run, then using the cached results on subsequent executions, balancing both startup speed and runtime performance. Box64 is also an open-source JIT translator on the Linux platform, focusing on user-space x86-64 programs. It overlaps in design goals with FEX-Emu, and together the two form the foundation of open-source x86 compatibility layers in the Linux ARM ecosystem.

Conclusion

The FEX-Emu team's article, with candid technical perspective, reveals the many difficulties behind x86 emulation: from the fundamental divergence in memory consistency models to the fine-grained battles over flags and floating-point precision. These challenges have no perfect solutions — only the engineering art of constantly balancing correctness against performance. For developers focused on low-level systems, architecture migration, and open-source tooling, understanding these difficulties helps foster a more realistic view of the current capability boundaries and development potential of the ARM ecosystem.

#x86 emulation#ARM architecture#FEX-Emu#binary translation#Rosetta 2#memory model#JIT compilation#Windows on ARM#x86 to ARM#floating-point precision
Share:

Related articles