Universal Binaries and Apple Silicon
With Apple’s transition from Intel to Apple Silicon, macOS developers need to understand universal binaries—single executable files containing code for multiple architectures. This chapter covers building, inspecting, and managing universal binaries.
Architecture Background
The Transition
| Period | Mac Architecture | Identifier |
|---|---|---|
| 2006-2020 | Intel 64-bit | x86_64 |
| 2020-present | Apple Silicon | arm64 |
| Transition | Both | Universal |
Architecture Identifiers
# Check current machine architecture
$ uname -m
arm64 # Apple Silicon
# or
x86_64 # Intel
# Check process architecture
$ arch
arm64
# Full system info
$ uname -a
Darwin hostname 23.4.0 Darwin Kernel Version 23.4.0: ... RELEASE_ARM64_T6000 arm64
What Is a Universal Binary?
A universal binary (or “fat binary”) contains:
- Multiple architecture-specific code sections
- Metadata describing each architecture
- Single file, multiple executables inside
Universal Binary
┌─────────────────────────────────────┐
│ Fat Header │
│ - Magic number │
│ - Number of architectures: 2 │
│ - Architecture entries │
├─────────────────────────────────────┤
│ arm64 Mach-O executable │
│ - Code compiled for Apple Silicon │
├─────────────────────────────────────┤
│ x86_64 Mach-O executable │
│ - Code compiled for Intel │
└─────────────────────────────────────┘
Inspecting Binaries
Using file
# Single architecture (Intel)
$ file /usr/bin/some_intel_app
/usr/bin/some_intel_app: Mach-O 64-bit executable x86_64
# Single architecture (Apple Silicon)
$ file /usr/bin/some_arm_app
/usr/bin/some_arm_app: Mach-O 64-bit executable arm64
# Universal binary
$ file /bin/ls
/bin/ls: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64e:Mach-O 64-bit executable arm64e]
# Homebrew binary (usually single arch)
$ file /opt/homebrew/bin/wget
/opt/homebrew/bin/wget: Mach-O 64-bit executable arm64
Using lipo
lipo is the main tool for working with universal binaries:
# List architectures
$ lipo -info /bin/ls
Architectures in the fat file: /bin/ls are: x86_64 arm64e
# Detailed info
$ lipo -detailed_info /bin/ls
Fat header in: /bin/ls
fat_magic 0xcafebabe
nfat_arch 2
architecture x86_64
cputype CPU_TYPE_X86_64
cpusubtype CPU_SUBTYPE_X86_64_ALL
capabilities 0x0
offset 16384
size 72672
align 2^14 (16384)
architecture arm64e
cputype CPU_TYPE_ARM64
cpusubtype CPU_SUBTYPE_ARM64E
capabilities PTR_AUTH_VERSION USERSPACE 0
offset 98304
size 72832
align 2^14 (16384)
arm64 vs arm64e
# arm64: Standard Apple Silicon
# arm64e: Enhanced with Pointer Authentication (PAC)
# System binaries often use arm64e
$ file /bin/ls
/bin/ls: ... arm64e
# Third-party typically use arm64
$ file /opt/homebrew/bin/node
/opt/homebrew/bin/node: Mach-O 64-bit executable arm64
Building Universal Binaries
Compiler Flags
# Build for single architecture
$ clang -arch arm64 program.c -o program_arm64
$ clang -arch x86_64 program.c -o program_x86_64
# Build universal binary directly
$ clang -arch arm64 -arch x86_64 program.c -o program_universal
# Verify
$ file program_universal
program_universal: Mach-O universal binary with 2 architectures: [x86_64:...] [arm64:...]
Using lipo to Combine
# Build separate binaries
$ clang -arch arm64 program.c -o program_arm64
$ clang -arch x86_64 program.c -o program_x86_64
# Combine into universal
$ lipo -create program_arm64 program_x86_64 -output program_universal
# Verify
$ lipo -info program_universal
Architectures in the fat file: program_universal are: x86_64 arm64
CMake
# CMakeLists.txt
cmake_minimum_required(VERSION 3.19)
project(MyProject)
# Build universal binary
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")
add_executable(myprogram main.c)
Build:
$ mkdir build && cd build
$ cmake ..
$ make
$ file myprogram
myprogram: Mach-O universal binary with 2 architectures: [x86_64:...] [arm64:...]
Autotools/Configure
# Set compiler flags
$ CFLAGS="-arch arm64 -arch x86_64" \
LDFLAGS="-arch arm64 -arch x86_64" \
./configure --prefix=/usr/local
$ make
Xcode
In Xcode Build Settings:
- Architectures:
Standard Architectures (arm64, x86_64) - Or set
ARCHS = arm64 x86_64
Extracting Architectures
Extract Single Architecture
# Extract arm64 version
$ lipo -extract arm64 program_universal -output program_arm64_only
# Extract x86_64 version
$ lipo -thin x86_64 program_universal -output program_x86_64_only
# Verify
$ file program_arm64_only
program_arm64_only: Mach-O 64-bit executable arm64
Remove Architecture
# Remove x86_64, keep arm64
$ lipo -remove x86_64 program_universal -output program_arm64_only
Rosetta 2
Rosetta 2 translates x86_64 code to run on Apple Silicon:
Check if Running Under Rosetta
# From inside a process
$ sysctl -n sysctl.proc_translated
1 # Running under Rosetta
0 # Native arm64
# Shell check
if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ]; then
echo "Running under Rosetta 2"
else
echo "Running natively"
fi
Force x86_64 Execution
# Run Intel binary via Rosetta
$ arch -x86_64 ./intel_program
# Start shell in x86_64 mode
$ arch -x86_64 /bin/zsh
# Then all commands run as x86_64
$ uname -m
x86_64
Installing Rosetta
# Rosetta installs automatically when needed
# Or install manually:
$ softwareupdate --install-rosetta
# Non-interactive
$ softwareupdate --install-rosetta --agree-to-license
Rosetta Limitations
- Performance overhead (typically 70-90% of native)
- Some instructions not supported
- Kernel extensions don’t translate
- Virtual Machine hypervisor features differ
Homebrew and Architecture
Separate Installations
Homebrew uses different prefixes by architecture:
| Architecture | Prefix |
|---|---|
| Apple Silicon | /opt/homebrew |
| Intel | /usr/local |
# Native Apple Silicon Homebrew
$ /opt/homebrew/bin/brew --prefix
/opt/homebrew
# Intel Homebrew (under Rosetta)
$ arch -x86_64 /usr/local/bin/brew --prefix
/usr/local
Installing Both
# Native ARM Homebrew (on Apple Silicon)
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Intel Homebrew (optional, for x86_64 packages)
$ arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Checking Package Architecture
# Check what arch a Homebrew package is
$ file /opt/homebrew/bin/python3
/opt/homebrew/bin/python3: Mach-O 64-bit executable arm64
$ file /usr/local/bin/python3
/usr/local/bin/python3: Mach-O 64-bit executable x86_64
Development Considerations
Conditional Compilation
#if defined(__arm64__) || defined(__aarch64__)
// Apple Silicon specific code
printf("Running on Apple Silicon\n");
#elif defined(__x86_64__)
// Intel specific code
printf("Running on Intel\n");
#endif
Runtime Detection
#include <sys/sysctl.h>
int is_apple_silicon() {
int ret = 0;
size_t size = sizeof(ret);
// Check if running natively on ARM
if (sysctlbyname("hw.optional.arm64", &ret, &size, NULL, 0) == 0) {
return ret;
}
return 0;
}
int is_translated() {
int ret = 0;
size_t size = sizeof(ret);
// Check if running under Rosetta
if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == 0) {
return ret;
}
return 0;
}
Libraries and Linking
When building universal binaries, all linked libraries must support both architectures:
# Check library architecture
$ file /opt/homebrew/lib/libssl.dylib
/opt/homebrew/lib/libssl.dylib: Mach-O 64-bit dynamically linked shared library arm64
# Error when mixing architectures
$ clang -arch x86_64 program.c -L/opt/homebrew/lib -lssl
ld: warning: ignoring file /opt/homebrew/lib/libssl.dylib: building for macOS-x86_64 but attempting to link with file built for macOS-arm64
Solution: Use universal libraries or build architecture-specific binaries.
Debugging Universal Binaries
LLDB Architecture Selection
# Debug specific architecture
$ lldb --arch x86_64 ./universal_program
$ lldb --arch arm64 ./universal_program
Crash Reports
Check architecture in crash reports:
Process: myprogram [12345]
...
Code Type: ARM-64 (Native)
# or
Code Type: X86-64 (Translated)
Distribution
App Store
- Universal binaries recommended
- Apple Silicon required for new apps
- Rosetta support continues for existing Intel apps
Direct Distribution
# Check your binary before distribution
$ file MyApp.app/Contents/MacOS/MyApp
MyApp.app/Contents/MacOS/MyApp: Mach-O universal binary with 2 architectures...
# Notarize (works with universal binaries)
$ xcrun notarytool submit MyApp.zip --apple-id ... --team-id ...
Reducing Binary Size
Universal binaries are larger. To distribute architecture-specific:
# Create arm64-only version for Apple Silicon
$ lipo -thin arm64 MyApp -output MyApp_arm64
# Compare sizes
$ ls -lh MyApp MyApp_arm64
-rwxr-xr-x 1 user staff 2.0M MyApp
-rwxr-xr-x 1 user staff 1.0M MyApp_arm64
Common Issues
“Bad CPU Type” Error
$ ./program
Bad CPU type in executable
Causes:
- Running arm64 binary on Intel Mac
- Running x86_64 binary on Apple Silicon without Rosetta
Solutions:
# Install Rosetta (Apple Silicon)
$ softwareupdate --install-rosetta
# Or rebuild for correct architecture
$ clang -arch arm64 program.c -o program
Architecture Mismatch in Libraries
ld: building for macOS-arm64 but attempting to link with file built for macOS-x86_64
Solution: Ensure all libraries match target architecture, or build truly universal libraries.
Performance Profiling
When profiling universal binaries, ensure you’re testing the native architecture:
# On Apple Silicon
$ arch
arm64
# Force native execution (already default)
$ arch -arm64 ./program
# Profile
$ instruments -t "Time Profiler" ./program
Summary
| Task | Command |
|---|---|
| Check architecture | file binary or lipo -info binary |
| Build universal | clang -arch arm64 -arch x86_64 ... |
| Combine binaries | lipo -create arm64_bin x86_64_bin -output universal |
| Extract architecture | lipo -thin arm64 universal -output arm64_only |
| Run as Intel | arch -x86_64 ./program |
| Check if translated | sysctl -n sysctl.proc_translated |
Understanding universal binaries is essential for:
- Supporting both Intel and Apple Silicon Macs
- Proper library linking
- Performance optimization
- Application distribution
As the Mac ecosystem transitions fully to Apple Silicon, the need for x86_64 support will diminish, but understanding these concepts remains important for maintaining existing software and working with cross-platform codebases.