AArchX is one process. The guest program is mapped into it, its instructions are translated to arm64 and run on real threads, and everything the guest thinks is the operating system is answered by AArchX.
| Component | Source | Responsibility |
|---|---|---|
| Loader | src/loader.c |
Mach-O parsing, mappings, the initial stack |
| Decoder | src/decode.c |
x86-64 and i386 into a 548-operation internal representation |
| Interpreter | src/interp*.c, src/flags.c |
reference execution and x86 flag semantics |
| JIT | src/jit.c, src/a64emit.c |
arm64 code generation, block chaining, superblocks |
| Mini-dyld | src/dyld.c, src/cache.c, src/dyldapi.c |
the shared cache, symbols, fixups, Objective-C, dlopen and dladdr |
| Virtual dylibs | src/vdylib.c |
native mode’s synthesized x86_64 system images and their stubs |
| API database | src/apidb.c, runtime/apis/, tools/sdkgen/ |
native mode’s export lists and signatures, generated from the SDK |
| Bridge | src/bridge.c |
native mode’s crossings, and which call a thread is in when it faults |
| System bridge | src/sysbridge.c |
the libSystem calls a generic crossing cannot make |
| Objective-C bridge | src/objcbridge.c, src/objcclass.c |
message sends both ways, and the guest’s own classes |
| ABI engine | src/abi.c, src/abicall.s |
System V x86-64 to Apple arm64 in both directions |
| Blocks | src/blocks.c |
native mode’s blocks in both directions |
| In-place routines | src/leaf.s |
arm64 strlen, memcpy and nine others that translated code calls with the guest’s registers where they are |
| Translation cache | src/tcache.c |
translated blocks kept on disk and shared between processes |
| Syscalls | src/syscall.c |
BSD, Mach, signals, threads, and Wine’s WoW64 calls |
The guest gets a large reserved region of the host address space, and a guest
address is turned into a host address by adding a base. Which base depends on
how the program is loaded. A statically linked program gets an arena at a fixed
guest address with a non-zero offset. A dynamically linked one gets an identity
mapping, where guest addresses are host addresses and the translation drops
the add entirely, which is the fast case and the usual one. Wine, whose main
image insists on living at a low address the host will not hand out, gets a
third mapping with a separate base for the low region, and reserves its arena
at a fixed address so that ntdll.so lands at the same place in every process
of a Wine session.
Guest pages are 4 KB and host pages are 16 KB, so AArchX tracks protection per 4 KB slot inside each host page and takes the union when it has to ask the kernel. That mismatch shapes most of the memory code, and it is why a guest’s protection changes are resolved on 16 KB boundaries.
The decoder turns instruction bytes into an internal operation with its operands. The interpreter can execute that directly, and is the reference: every guest test in the suite is run both interpreted and translated, and the two must agree byte for byte.
The JIT translates a block at a time, from an entry point to the branch that ends it. Inside a block:
ret uses a host return
stack that mirrors the guest’s calls, so the processor’s own return predictor
stays aligned with the guest’s.The instruction set is x86-64-v3 as Rosetta runs it on macOS 15 and later: AVX2, FMA, BMI1 and BMI2, F16C, LZCNT, MOVBE and XSAVE, none of which CPUID advertises under either. SSE4.2 is implemented and advertised, because Steam checks for it. The common x87 forms are translated; the rest of x87, any x87 operation that rounds other than to nearest or meets a NaN, and a few AVX2 forms run only in the interpreter; see Compatibility.
A guest that writes over code it has already executed is handled by making the page fault on write and dropping the translations covering it.
Translations live in a 1 GB arena that is filled front to back. A program that keeps generating code, such as a .NET or Mono game, can use it up; when little room is left, AArchX waits for every thread to leave translated code, throws all translations away and starts the arena again from the front.
x86 guarantees more about the order of memory operations than arm64 does. A program that never creates a thread, forks, or maps shared memory cannot observe the difference, so AArchX translates it with plain loads and stores. The first time it does any of those, AArchX switches to acquire and release forms for scalar accesses and keeps them for the rest of the process’s life.
Vector loads and stores are left plain even then, which is what other
translators do as well, because ordering them costs several times more than it
is worth; OCERZ_TSO_VECTOR=1 orders them for anyone who wants it. That
includes the 8-byte movq and 4-byte movd, which compilers sometimes use to
move a pointer or a counter; OCERZ_TSO_NARROW=1 orders just those two.
x86 and arm64 disagree about NaNs: an invalid operation gives a negative default NaN on one and a positive one on the other, and when both operands are NaNs x86 returns the first where arm64 lets a signalling one win. Programs do notice.
AArchX handles this two ways. Where the processor reports FEAT_AFP, setting
one bit in the floating-point control register makes arm64 arithmetic follow
x86’s rules, and the instructions are emitted bare. Where it does not, results
are checked and a whole run of floating-point work is replayed exactly through
the interpreter if a NaN appears — once per run rather than once per
instruction, with the registers it overwrites checkpointed so the replay can
start from the beginning.
Guest system calls do not reach the kernel as they are. AArchX implements the BSD and Mach interfaces itself, because most of them need the arguments or the results rewritten: a structure that differs between the two architectures, a pointer into guest memory, a thread port that names a guest thread rather than a host one. Signals are delivered onto the guest’s stack in the frame layout x86 code expects, and a fault inside translated code is turned back into a fault at the guest instruction that caused it, with every guest register recovered from the arm64 registers holding them.
What the guest links against is the part that differs between the two modes, and it is the largest difference in the project: cache mode maps Apple’s x86-64 shared cache and translates it like everything else, while native mode synthesizes x86 images whose stubs cross into the Mac’s own arm64 frameworks.
Every source file opens with a prose block explaining what it does and why it
has the shape it has, including the mistakes that shaped it. src/jit.c and
src/dyld.c are the two longest and the two most worth reading.