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.
The route
Jump straight to the step you need, or tick off Done means at the end.
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
- Version confirmed.
llvm-otool-18 --versionidentified the installed LLVM build. - Type established.
-hor-festablished the Mach-O type and, where relevant, available slices. - Load commands read.
-land-Lgave you load-command and dependency evidence. - Sections checked first. Section or disassembly output was requested only after checking the file's architecture and section names.
- Binary untouched. The original binary was treated as read-only and no elevated privileges were needed.