Hacktakes · Edition 17
Hacktakes · Edition 17 · August 3, 2026

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

As long as you hand him the exact files he asks for, he has absolutely no idea who he's talking to.
As long as you hand him the exact files he asks for, he has absolutely no idea who he's talking to.

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, &regs);

    // 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, &regs);
    }
}

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 loads 0x2000004. (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 the 0x2000000 prefix to signify a BSD-style system call).
  • The tracer pauses it: Because of the ptrace loop, the Linux kernel halts the program. The tracer reads the x8 register, notices the Mac identifier, and prints our Paused process at syscall: 0x2000004 log.
  • 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 for write.
  • Linux takes over: Finally, the tracer tells the kernel to resume execution. The Linux kernel wakes up, looks at the x8 register, sees its own familiar 64, and successfully prints Hello 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 :)

← Back to Edition 17