Inspect Mach-O Binaries on Linux with llvm-otool-18

A colleague hands you a Mach-O binary from someone else's Mac, and llvm-otool-18 shows what is inside without running it. This guide uses the installed llvm-otool-18 from the Debian llvm-18 package to identify the file, read its headers and load commands, list dependencies, and inspect selected sections. Allow about 10 minutes for a first pass. You need a readable Mach-O file and a normal shell account; none of the inspection commands require sudo.

1. Confirm the local tool

Start by checking which binary will run and which LLVM build it reports:

$ command -v llvm-otool-18
/usr/bin/llvm-otool-18
$ llvm-otool-18 --version
Ubuntu LLVM version 18.1.3
  Optimized build.

The exact version matters when comparing output from another machine. The installed manpage describes the command as a Mach-O dumping tool and documents a command-line and output compatibility goal with macOS otool. It is a reader: the options below print information and do not rewrite the input.

Checkpoint: If command -v finds a different path, stop and record it before comparing results. Do not solve a path problem by installing another LLVM package in the middle of an investigation.

2. Check that the input is really Mach-O

Set a shell variable to a file you intend to inspect, then ask for its Mach header:

$ MACHO='/path/to/sample'
$ llvm-otool-18 -h "$MACHO"

A valid input produces a Mach header with fields such as the CPU type, file type, number of load commands and flags. The command is read-only. If the file is an ordinary Linux ELF binary, the tool reports an error like object is not a Mach-O file type. That is an input-format problem, not a reason to add more flags.

Checkpoint: Keep the original error and verify the path with file "$MACHO". A copied, truncated or compressed download must be corrected before interpretation. Do not feed untrusted files to other tools just to make them look like Mach-O.

3. Read universal-binary slices

A universal, or fat, Mach-O file contains more than one architecture. Print its slice headers first:

$ llvm-otool-18 -f "$MACHO"

Use the architecture name reported by that output to select one slice for later inspection:

$ llvm-otool-18 -arch arm64 -h "$MACHO"
$ llvm-otool-18 -arch x86_64 -l "$MACHO"

-arch selects a slice; it does not convert the file or change its default slice. If the requested architecture is absent, choose one that -f actually listed. This is a common distraction when a header command appears to work for one machine but not another.

4. Inspect load commands and linked libraries

Load commands describe how the Mach-O image is laid out and loaded. Print them without disassembling code:

$ llvm-otool-18 -l "$MACHO"

To focus on the libraries recorded by the file, use -L:

$ llvm-otool-18 -L "$MACHO"

Look for the install name and dependency paths, then compare those paths with the deployment environment. A dependency listed here is metadata, not proof that the library exists on your Linux host. The command also does not execute the Mach-O file, so it cannot prove that a macOS loader would successfully start it.

For a dylib, -D prints its shared-library identifier:

$ llvm-otool-18 -D "$MACHO"

5. Inspect sections and disassembly deliberately

Use -s when you know the segment and section names. This example prints the contents of the named section:

$ llvm-otool-18 -s __TEXT __cstring "$MACHO"

The names must match the file. Use -l first if you are unsure which sections exist. For text sections, -t prints the text section, while -x prints all text sections. Add -v for verbose output and disassembly-related detail:

$ llvm-otool-18 -arch x86_64 -t -v "$MACHO"
$ llvm-otool-18 -arch arm64 -x -v "$MACHO"

These outputs can be large. Redirect them to a new file if you need to search them, rather than overwriting the binary:

$ llvm-otool-18 -l "$MACHO" > load-commands.txt
$ rg 'LC_LOAD_DYLIB|LC_RPATH' load-commands.txt

That redirection creates or replaces load-commands.txt, not the Mach-O input. If the report is valuable, choose a new destination or make a backup first. Recovery is simply to remove the report or restore the previous copy of that report; the inspected binary is unchanged.

6. Handle errors without guessing

The tool exits non-zero when it encounters an error, and it may write diagnostics to standard error. Capture both streams when preserving a failed attempt:

$ llvm-otool-18 -l "$MACHO" > inspection.txt 2> inspection.err
$ printf 'status: %s\n' "$?"
$ sed -n '1,12p' inspection.err

Run the status command immediately. If the file is not Mach-O, check the path and file type. If a universal file rejects an architecture, return to -f. If a section name fails, inspect -l rather than inventing a spelling. Some options are specialised: -dyld_info prints bind and rebase information, -r prints relocations, and -p FUNCTION starts disassembly at a named function. Use them only when the file and question justify them.

Done means