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.