How do you inspect what a binary links, what load commands it has and how it is signed with command-line tools?

6.11.3 · L5 · Learning

Predict first

What do otool, file and codesign report for a freshly built command-line tool?

// === t.swift ===
import Foundation
print(Bundle.main.bundleIdentifier ?? "no bundle identifier")
// === run.sh ===
swiftc -swift-version 5 t.swift -o demo 2>&1 | grep error
echo "-- linked libraries"; otool -L demo | tail -n +2 | awk '{print $1}' | sed 's|^/usr/lib/swift/||' | head -3
echo "-- load commands present"; otool -l demo | grep -oE 'LC_(MAIN|LOAD_DYLIB|CODE_SIGNATURE|UUID|BUILD_VERSION)' | sort -u
echo "-- file type"; file demo | sed 's/^demo: //' | cut -c1-30
echo "-- Info.plist embedded in a command-line binary?"; otool -s __TEXT __info_plist demo 2>&1 | grep -c 'Contents of'
echo "-- an unsigned binary after stripping the signature"; codesign --remove-signature demo 2>/dev/null; codesign -d demo 2>&1 | tail -1 | sed 's/^demo: //'
echo "-- ad hoc signed again"; codesign -f -s - demo 2>/dev/null; codesign -dv demo 2>&1 | grep -E '^Signature='; ./demo
  • It links libSystem, Foundation and libobjc, has LC_MAIN, LC_UUID and LC_CODE_SIGNATURE, carries no Info.plist, can be unsigned and re-signed ad hoc, and prints no bundle identifier
  • It has an embedded Info.plist and a bundle identifier
  • It has no code signature at all

Mental model. otool and codesign are the X-ray and the passport check for a binary: one shows what it is built from and where it enters, the other who vouches for it.

Three tools answer most launch and packaging questions without Xcode. otool -L binary lists the dynamic libraries the binary will ask dyld to load (the check shows libSystem, Foundation and libobjc), which is the quickest way to count your own frameworks (each one is launch cost, Q220) and to see an accidental link against a library you thought was static. otool -l binary prints the load commands: LC_MAIN (the entry point), LC_LOAD_DYLIB (one per dependency), LC_UUID (the build identifier that crash logs and dSYMs are matched on), LC_BUILD_VERSION (platform and minimum OS) and LC_CODE_SIGNATURE (where the signature lives). codesign -dv shows the signing identity, team and flags; after --remove-signature it reports the binary as not signed, and signing again ad hoc restores Signature=adhoc.

Two further points keep this honest. A command-line tool has no Info.plist unless one is linked into the __TEXT,__info_plist section (the check found none, so Bundle.main.bundleIdentifier printed nothing meaningful): bundle APIs belong to .app bundles, which is another reason a test run in a bare process can differ from the app. And these are macOS tool runs; the same tools work on a binary extracted from an iOS app (.app/AppName), whose otool -L also lists your embedded frameworks as @rpath/Foo.framework/Foo, but device-only information (the embedded provisioning profile, FairPlay encryption on App Store downloads) was not exercised here. Related tools worth knowing: dyld_info -fixups and -exports (rebase and bind work), nm and swift-demangle for symbols, vmmap and footprint for a running macOS process, and DYLD_PRINT_LIBRARIES style environment variables for tracing loads (honoured for local builds only; they are stripped from hardened and restricted binaries).

Load commands and the signature are properties of the Mach-O file; the plist is a separate bundle file or an embedded section.

Common traps

  • Counting dependencies from the Xcode project instead of the binary. The binary is the truth; transitive dynamic libraries only show up there.
  • Trusting test results from a bare executable for bundle-dependent code. There is no bundle identifier, resource bundle or plist.

They might follow up with

  • How are crash logs matched to symbols? By the LC_UUID of the binary and the matching dSYM.
  • What is the cheapest way to cut launch work found here? Merge or statically link your own dynamic frameworks (Q220).

Related questions

  • 6.4.1 Walk through what happens from tapping the icon to the first frame.
  • 6.4.2 What does dyld do, and how do static and dynamic linking change launch?
  • 6.4.3 Your app launches slowly. How do you find out why and what do you cut?
  • 6.11.4 How do you validate, read and convert an Info.plist or entitlements file from the command line, and what are…

← In a scene-based app, which callbacks fire at launch and on…   How do you validate, read and convert an Info.plist or entitlements… →