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

Library Paths and Linking

macOS handles dynamic libraries differently from Linux. Understanding these differences is essential for compiling software, debugging linking issues, and distributing applications.

Dynamic Libraries: dylib vs so

PlatformExtensionFormat
macOS.dylibMach-O
Linux.soELF
Windows.dllPE

Library Naming Conventions

# macOS naming
libfoo.dylib                    # Current version symlink
libfoo.1.dylib                  # Major version
libfoo.1.2.3.dylib              # Full version

# Linux naming (for comparison)
libfoo.so                       # Symlink
libfoo.so.1                     # Major version
libfoo.so.1.2.3                 # Full version

# macOS also supports .so for compatibility
# Some ports create .so files, but native libraries use .dylib

Static Libraries

Static libraries use the same format on both platforms:

# Archive format
libfoo.a

# Create static library
$ ar rcs libfoo.a foo.o bar.o

# View contents
$ ar -t libfoo.a
foo.o
bar.o

Library Search Paths

Default Search Order

The dynamic linker (dyld) searches in this order:

  1. DYLD_LIBRARY_PATH (if set and SIP disabled)
  2. LD_LIBRARY_PATH (fallback, if set)
  3. Paths embedded in the binary (rpath, @executable_path, etc.)
  4. /usr/lib
  5. /usr/local/lib

Viewing Search Paths

# Show dyld search paths
$ dyld_info -search_paths /usr/bin/some_binary

# Or check man page for full algorithm
$ man dyld

DYLD_LIBRARY_PATH

Similar to Linux’s LD_LIBRARY_PATH but with restrictions:

# Set library path
$ export DYLD_LIBRARY_PATH=/opt/homebrew/lib:/usr/local/lib

# Run program with modified path
$ DYLD_LIBRARY_PATH=/custom/lib ./myprogram

Important: System Integrity Protection (SIP) clears DYLD_* variables for system binaries and when calling protected executables.

# This won't work for system binaries
$ DYLD_LIBRARY_PATH=/custom sudo somecommand  # Variable cleared

DYLD_FALLBACK_LIBRARY_PATH

Used when library isn’t found in standard locations:

$ export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib:$HOME/lib:/usr/local/lib:/usr/lib

The otool Command

otool is the macOS equivalent of ldd and readelf:

View Linked Libraries

$ otool -L /usr/bin/python3
/usr/bin/python3:
        /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
        /usr/lib/libncurses.5.4.dylib
        /usr/lib/libSystem.B.dylib

# For a Homebrew binary
$ otool -L /opt/homebrew/bin/wget
/opt/homebrew/bin/wget:
        /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib
        /opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib
        /opt/homebrew/opt/libidn2/lib/libidn2.0.dylib
        /usr/lib/libz.1.dylib
        /usr/lib/libiconv.2.dylib
        /usr/lib/libSystem.B.dylib

View Load Commands

# All load commands
$ otool -l /opt/homebrew/bin/wget | head -50

# Just the library paths
$ otool -l /opt/homebrew/bin/wget | grep -A2 LC_LOAD_DYLIB
          cmd LC_LOAD_DYLIB
      cmdsize 64
         name /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib

View Symbols

# External symbols
$ nm /opt/homebrew/lib/libssl.dylib | head
0000000000001234 T _SSL_connect
0000000000001456 T _SSL_read
...

# Undefined symbols (needed from other libraries)
$ nm -u /opt/homebrew/bin/wget
                 U _SSL_connect
                 U _SSL_read

Install Names

Every macOS dynamic library has an “install name”—the path recorded inside the library itself:

# View install name
$ otool -D /opt/homebrew/lib/libssl.dylib
/opt/homebrew/lib/libssl.dylib:
/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib

# The install name is embedded in the library
# When you link against it, this path is recorded in your binary

How Install Names Work

  1. Library is built with an install name
  2. When you link against the library, the install name is copied to your binary
  3. At runtime, dyld uses this path to find the library
# Example: Building and linking

# Library specifies its install name during build
$ clang -dynamiclib -install_name /usr/local/lib/libfoo.dylib \
    -o libfoo.dylib foo.c

# Program links against library
$ clang -o myprogram main.c -L. -lfoo

# The install name is recorded
$ otool -L myprogram
myprogram:
        /usr/local/lib/libfoo.dylib  # Install name, not build path!
        /usr/lib/libSystem.B.dylib

install_name_tool

Modify install names in binaries and libraries:

Change Install Name

# Change a library's own install name
$ install_name_tool -id /new/path/libfoo.dylib libfoo.dylib

# Verify
$ otool -D libfoo.dylib
libfoo.dylib:
/new/path/libfoo.dylib

Change Dependency Path

# Change where a binary looks for a library
$ install_name_tool -change \
    /old/path/libfoo.dylib \
    /new/path/libfoo.dylib \
    myprogram

# Verify
$ otool -L myprogram

Add rpath

# Add runtime search path
$ install_name_tool -add_rpath /opt/homebrew/lib myprogram

# Delete rpath
$ install_name_tool -delete_rpath /old/path myprogram

# View rpaths
$ otool -l myprogram | grep -A2 LC_RPATH
          cmd LC_RPATH
      cmdsize 32
         path /opt/homebrew/lib

@executable_path, @loader_path, @rpath

Special path prefixes for relocatable binaries:

@executable_path

Relative to the main executable:

# Install name
@executable_path/../lib/libfoo.dylib

# If executable is /Applications/MyApp.app/Contents/MacOS/MyApp
# Library resolves to /Applications/MyApp.app/Contents/lib/libfoo.dylib

@loader_path

Relative to the binary containing the reference (useful for plugin systems):

# Install name
@loader_path/../Frameworks/libfoo.dylib

# Resolves relative to whatever binary loads this library
# Different from @executable_path for plugins/bundles

@rpath

Resolved using the binary’s rpath list:

# Install name
@rpath/libfoo.dylib

# dyld searches each rpath entry:
# If rpath contains /opt/homebrew/lib, tries /opt/homebrew/lib/libfoo.dylib

Using @rpath

# Build library with @rpath install name
$ clang -dynamiclib -install_name @rpath/libfoo.dylib \
    -o libfoo.dylib foo.c

# Build executable with rpath
$ clang -o myprogram main.c -L. -lfoo \
    -Wl,-rpath,/usr/local/lib \
    -Wl,-rpath,/opt/homebrew/lib

# Multiple rpaths are searched in order
$ otool -l myprogram | grep -A2 LC_RPATH
          cmd LC_RPATH
      cmdsize 32
         path /usr/local/lib
--
          cmd LC_RPATH
      cmdsize 40
         path /opt/homebrew/lib

dyld Debugging

DYLD_PRINT_LIBRARIES

See which libraries are loaded:

$ DYLD_PRINT_LIBRARIES=1 ./myprogram
dyld[12345]: <1A23B456-...> /usr/lib/libSystem.B.dylib
dyld[12345]: <2B34C567-...> /opt/homebrew/lib/libssl.3.dylib
...

DYLD_PRINT_SEARCHING

See the search process:

$ DYLD_PRINT_SEARCHING=1 ./myprogram
dyld[12345]: find path "/opt/homebrew/lib/libfoo.dylib"
dyld[12345]:   found: "/opt/homebrew/lib/libfoo.dylib"

Note: These only work with SIP disabled or for non-system binaries.

dyld_info

Modern tool for analyzing binaries:

# Show all dyld info
$ dyld_info -all /opt/homebrew/bin/wget

# Just dependencies
$ dyld_info -dependents /opt/homebrew/bin/wget

# Exports
$ dyld_info -exports /opt/homebrew/lib/libssl.dylib

Common Linking Problems

Library Not Found

$ ./myprogram
dyld[12345]: Library not loaded: /usr/local/lib/libfoo.dylib
  Referenced from: <UUID> /path/to/myprogram
  Reason: tried: '/usr/local/lib/libfoo.dylib' (no such file)

Solutions:

# 1. Install the library
$ brew install foo

# 2. Create symlink
$ sudo ln -s /opt/homebrew/lib/libfoo.dylib /usr/local/lib/

# 3. Fix the binary's path
$ install_name_tool -change \
    /usr/local/lib/libfoo.dylib \
    /opt/homebrew/lib/libfoo.dylib \
    myprogram

# 4. Add rpath
$ install_name_tool -add_rpath /opt/homebrew/lib myprogram

Wrong Architecture

$ ./myprogram
dyld[12345]: Library not loaded: libfoo.dylib
  Reason: tried: 'libfoo.dylib' (mach-o file, but is an incompatible architecture)

Check architectures:

$ file myprogram
myprogram: Mach-O 64-bit executable arm64

$ file /path/to/libfoo.dylib
libfoo.dylib: Mach-O 64-bit dynamically linked shared library x86_64

# Need to rebuild for matching architecture

Symbol Not Found

$ ./myprogram
dyld[12345]: Symbol not found: _some_function
  Referenced from: <UUID> /path/to/myprogram
  Expected in: <UUID> /path/to/libfoo.dylib

Library version mismatch—library doesn’t have expected symbol:

# Check what symbols library exports
$ nm /path/to/libfoo.dylib | grep some_function

# May need newer library version
$ brew upgrade foo

Creating Dynamic Libraries

Basic Dynamic Library

# Compile to object file
$ clang -c -fPIC foo.c -o foo.o

# Create dynamic library
$ clang -dynamiclib \
    -install_name @rpath/libfoo.dylib \
    -o libfoo.dylib \
    foo.o

# With version info
$ clang -dynamiclib \
    -install_name @rpath/libfoo.1.dylib \
    -current_version 1.2.3 \
    -compatibility_version 1.0.0 \
    -o libfoo.1.2.3.dylib \
    foo.o

# Create symlinks
$ ln -s libfoo.1.2.3.dylib libfoo.1.dylib
$ ln -s libfoo.1.dylib libfoo.dylib

Version Numbers

# View version info
$ otool -L libfoo.dylib
libfoo.dylib:
        @rpath/libfoo.1.dylib (compatibility version 1.0.0, current version 1.2.3)
  • Current version: Actual version of the library
  • Compatibility version: Minimum version needed by binaries linked against it

TBD Files (Text-Based Stubs)

Modern macOS uses text-based stub files instead of actual library binaries in SDKs:

$ cat /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib/libSystem.tbd
--- !tapi-tbd
tbd-version:     4
targets:         [ x86_64-macos, arm64-macos ]
install-name:    '/usr/lib/libSystem.B.dylib'
exports:
  - targets:     [ x86_64-macos, arm64-macos ]
    symbols:     [ __exit, _abort, ... ]

TBD files:

  • Contain metadata about library (exports, version)
  • Much smaller than full dylib
  • Used at link time, not runtime
  • Runtime uses actual dylib

Linking at Build Time

Compiler Flags

# Link against library
$ clang -o myprogram main.c -lfoo

# Specify library path
$ clang -o myprogram main.c -L/opt/homebrew/lib -lfoo

# Link against framework
$ clang -o myprogram main.c -framework CoreFoundation

# Specify framework path
$ clang -o myprogram main.c -F/path/to/frameworks -framework MyFramework

# Add rpath at link time
$ clang -o myprogram main.c -lfoo -Wl,-rpath,/opt/homebrew/lib

Static vs Dynamic

# Force static linking of specific library
$ clang -o myprogram main.c /path/to/libfoo.a

# Let linker choose (prefers dynamic)
$ clang -o myprogram main.c -L/path/to/lib -lfoo

# Force all libraries static (not fully supported on macOS)
# macOS always dynamically links system libraries

Summary

ToolPurpose
otool -LView linked libraries
otool -DView install name
otool -lView load commands
nmView symbols
install_name_toolModify paths
dyld_infoModern binary analysis
fileCheck architecture
Path PrefixMeaning
@executable_pathRelative to main executable
@loader_pathRelative to loading binary
@rpathSearch rpath list

Key differences from Linux:

AspectLinuxmacOS
Extension.so.dylib
Path variableLD_LIBRARY_PATHDYLD_LIBRARY_PATH
View dependencieslddotool -L
Modify pathspatchelfinstall_name_tool
Embedded pathRPATH/RUNPATHInstall name

Understanding install names and rpaths is crucial for creating portable macOS applications and debugging library loading issues.