Running Mac binaries on Linux (and realizing an OS is just a syscall dictionary!)
Because Mac and Linux share ARM64 hardware, tools run macOS binaries natively by rewriting system calls, proving an OS is just a syscall dictionary.
By Poppy Lin
Sparked by Show HN: Kakehashi – Experimental userspace to run macOS binaries on Linux ARM · discussion

I was scrolling yesterday and saw a really fascinating Hacker News discussion about a wildly cool project called Kakehashi. It’s a tool that runs macOS ARM64 binaries on Linux natively.
Someone in the comments asked a question that I thought was super interesting: how is this actually possible without a heavy emulator? Aren't Mac and Linux completely different operating systems?
I’ve actually been a little confused by this myself! When you try to run software designed for one system on another (like running classic x86 Windows games on a modern ARM Mac), you usually need a massive translation layer like Rosetta or QEMU. If the CPU architectures are fundamentally different, an emulator has to catch every single mathematical operation, decode it, and translate it into something the host hardware understands. It’s a slow, resource-heavy process because you are basically faking an entire physical motherboard in software.
But for macOS ARM64 to Linux ARM64, the CPU architecture is exactly the same!
An M1 Mac and an ARM Linux server speak the exact same physical machine code. Because the underlying hardware language is identical, the guest code runs entirely natively. Standard instructions—like adding two numbers, multiplying, or moving data around in memory—fly cleanly through to the CPU with zero interference. They execute at full native speed, with absolutely no translation overhead.
The instructions only hit a brick wall when they try to talk to the operating system. If the program wants to read a file, open a network connection, or print text to a screen, it has to make a system call (syscall).
To understand how projects like Kakehashi bridge this gap, I like looking at how standard Linux tools handle system calls. If you run a normal Linux command under a diagnostic tool like strace, you can see these OS requests happening in real time:
$ strace echo "Hello"
execve("/usr/bin/echo", ["echo", "Hello"], 0x7ffd5...) = 0
...
write(1, "Hello\n", 6) = 6
How does strace catch the program right as it hits that OS boundary? The secret is a Linux kernel feature called ptrace (specifically the PTRACE_SYSCALL option). ptrace lets a tracer process pause a program the exact millisecond it asks the operating system to do something. It acts almost like a border guard inspecting a passport.
Tools like strace or debuggers like gdb just inspect the passport and log it to your terminal. But what if you used ptrace to change the passport while the process was paused?
This is exactly how Kakehashi conceptually works. To picture the mechanics behind this process, we can imagine constructing a tiny conceptual C script to experiment with it. Suppose we sketched out a mini tracer that intercepts syscalls, paired with a dummy Mac binary that stubbornly tries to call the Apple-specific write syscall.
Here is what the stripped-down C code for that hypothetical tracer loop might look like:
while (1) {
// Wait for the guest program to make a syscall
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
waitpid(pid, &status, 0);
// Grab the CPU registers from the paused process!
struct user_regs_struct regs;
ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, ®s);
// Apple's macOS 'write' syscall ID is 0x2000004
if (regs.regs[8] == 0x2000004) {
printf("[Tracer] Paused process at syscall: 0x%llx\n", regs.regs[8]);
printf("[Tracer] Rewriting register x8 to 64...\n");
// Swap it to the Linux 'write' ID (64)
regs.regs[8] = 64;
// Write the modified registers back into the CPU
ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, ®s);
}
}
And if we were to compile and run this conceptual tracer against that dummy Mac binary, the terminal output would show something like this:
$ gcc mini-tracer.c -o mini-tracer
$ ./mini-tracer ./fake-mac-hello
[Tracer] Paused process at syscall: 0x2000004
[Tracer] Rewriting register x8 to 64...
Hello from a fake Mac binary!
[Tracer] Process exited cleanly.
Let's break down exactly what happened in that output:
- The Mac binary makes a request: When the binary attempts to print text to the console, it places its native syscall number into a specific CPU register. On ARM64 architectures, the register used for syscall numbers is
x8. It loads0x2000004. (I honestly didn't realize until reading about this that different OSes just use totally different arbitrary numbers for the exact same concepts! Apple uses the0x2000000prefix to signify a BSD-style system call). - The tracer pauses it: Because of the
ptraceloop, the Linux kernel halts the program. The tracer reads thex8register, notices the Mac identifier, and prints ourPaused process at syscall: 0x2000004log. - The tracer rewrites history: It physically overwrites that register in the paused CPU with the number
64. That number happens to be the standard Linux kernel's identifier forwrite. - Linux takes over: Finally, the tracer tells the kernel to resume execution. The Linux kernel wakes up, looks at the
x8register, sees its own familiar64, and successfully printsHello from a fake Mac binary!to your terminal window. The Mac program has NO IDEA it just talked to Linux.
There is a LOT I still don't know about this—especially memory management! Apple Silicon binaries strictly mandate 16 KiB memory pages, and I have absolutely no idea how that mapping actually works under the hood (you apparently need a kernel configured for 16K pages, like Asahi Linux, to even try this). But realizing that an OS is basically just a giant lookup table for syscalls makes it feel SO much less intimidating :)