How am I running x86_64 programs without arch on Apple Silicon?

Background

After getting an M1 MacBook Air, I wanted to install some command-line packages. Following this blog post, I duplicated Terminal.app and set it to run with Rosetta. Using this x86_64 terminal, I installed Nix and used it to install htop, nnn, etc. These run fine in the x86_64 terminal.

However, they also work in the native terminal, without arch. This makes me confused, because from reading this post, I thought I needed to add arch -x86_64 before the command to run it with Rosetta.

Screenshot of command output

The file command tells me that the binary does not have a version for Apple Silicon.

Question

How did htop run successfully?


  • If Rosetta is installed, the system will run x64 binaries automatically, you don’t need arch
  • arch is only required if you want to force the use of a specific version in an universal binary (containing both x64 and arm code), e.g. to run the x64 version of a binary if it relies on plugins only available in x64.

TL;DR: The binary itself embeds info about which architecture it is, and the OS looks at that to decide how to run it. arch is only reqired to force usage of a specific architectural variant of a universal binary.


All modern executable file formats, including the Mach-O format used by macOS, the ELF format used by most other UNIX-like systems, and the PE format used by Windows and UEFI, embed information in the file header about what CPU architecture the code in the file is for and what environment (or OS) it’s supposed to be run in.

When you invoke an executable, the first thing that happens is that a part of the OS typically known as the ‘executable loader’ takes a look at the file and decides how to load it into memory and how to actually run it. The executable loader is generally reasonably smart, and can typically determine that it needs to use an interpreter or translation layer before it even loads the code into memory. A trivial example of this is the handling of scripts starting with a #!, the executable loader in the OS sees that #! line and knows it means to run the command after that and pass the path to the script as a positional argument.

The same logic applies for this info about what CPU architecture the executable is for. In macOS for Apple silicon, there’s a special case in the executable loader code that will try to run binaries for x86_64 CPUs using Rosetta 2 if it’s installed. If Rosetta 2 isn’t installed, you’ll just get an error message that the executable couldn’t be run, but if it is, the executable will just run seamlessly without bothering you.

The arch command primarily exists for the special case of running universal binaries. With the Mach-O format and the standard loader code in macOS, the OS will run the first valid version of a program in a universal binary that matches the host CPU. By using arch though, you can explicitly force the system to run the x86_64 version on a system with Rosetta 2 installed. The same could be done historically to explicitly run the 32-bit version of a binary on a 64-bit system. This sounds kind of pointless, but it’s useful for developers to verify that their universal binaries are working correctly when they only have access to one type of hardware.


On a side note, this same type of runtime flexibility can be extended even further on some platforms. Linux, for example, has a feature called binfmt_misc that lets you associate arbitrary interpreters with arbitrary file types. Historically, this originated to support things like treating JAR files as regular executables, but it’s actively used today together with QEMU userspace emulation to do the same type of thing that macOS is doing with Rosetta 2 for even more CPU architectures.