Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

PeriodMac ArchitectureIdentifier
2006-2020Intel 64-bitx86_64
2020-presentApple Siliconarm64
TransitionBothUniversal

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:

ArchitecturePrefix
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

TaskCommand
Check architecturefile binary or lipo -info binary
Build universalclang -arch arm64 -arch x86_64 ...
Combine binarieslipo -create arm64_bin x86_64_bin -output universal
Extract architecturelipo -thin arm64 universal -output arm64_only
Run as Intelarch -x86_64 ./program
Check if translatedsysctl -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.