Inspect Mach-O Files on Linux with llvm-otool-20

A stray Mach-O file from a Mac build turns up on your Linux box, and llvm-otool-20 is the tool that reads it without running it. The examples use llvm-otool-20 from Ubuntu's llvm-20 package, version 1:20.1.8~++20250804090239+87f0227cb601-1~exp1~20250804210352.139, covering headers, load commands, linked libraries and section bytes.

Allow about fifteen minutes. You need a shell, the installed LLVM package and a readable Mach-O file. This is a read-only workflow: it does not rewrite, sign, execute or load the file. No command here needs sudo.

1. Confirm the installed tool

Check the binary and version before copying an example into a script. This matters because the command name is versioned on Debian and Ubuntu systems:

$ command -v llvm-otool-20
/usr/bin/llvm-otool-20
$ llvm-otool-20 --version
Ubuntu LLVM version 20.1.8
  Optimized build.
$ dpkg-query -W -f='${Package} ${Version}\n' llvm-20
llvm-20 1:20.1.8~++20250804090239+87f0227cb601-1~exp1~20250804210352.139

The upstream documentation calls the program llvm-otool; this installation exposes the versioned command. Use the name returned by command -v, or set a shell variable so later examples are easy to adapt:

OTOOL=llvm-otool-20

Checkpoint: $OTOOL --help should show the Mach-O options, including -h, -l, -L, -s and -arch.

2. Establish that the input is Mach-O

Start with file. Replace the example path with the file you need to examine:

$ SAMPLE=/usr/share/go-1.22/src/debug/dwarf/testdata/typedef.macho
$ file "$SAMPLE"
/usr/share/go-1.22/src/debug/dwarf/testdata/typedef.macho: Mach-O 64-bit x86_64 object, flags:<|SUBSECTIONS_VIA_SYMBOLS>

The fixture is a 64-bit x86_64 Mach-O object, rather than a complete executable. That is still enough to demonstrate headers, load commands and section contents. Keep the path quoted: it prevents whitespace in a file name from becoming another argument.

Do not assume that a macOS-looking suffix makes a file Mach-O. An ELF binary such as /bin/true is a different format. If the input is not Mach-O, stop and identify the correct file with file before trying more display options.

3. Read the Mach header

Use -h for the compact file header:

$ $OTOOL -h "$SAMPLE"
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedfacf 16777223          3  0x00           1     3       1376 0x00002000

This gives you the magic value, CPU type, file type, number of load commands and their combined size. Treat numeric fields as facts to record, not as a complete interpretation of the binary. The output above describes an object file with three load commands.

Checkpoint: save the header output alongside an investigation note if you need to compare two files later. llvm-otool-20 never changes the input for this operation.

4. Inspect load commands

Use -l when you need segment names, virtual addresses, file offsets, sections or other load-command data:

$ $OTOOL -l "$SAMPLE" | sed -n '1,42p'
/usr/share/go-1.22/src/debug/dwarf/testdata/typedef.macho:
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedfacf 16777223          3  0x00           1     3       1376 0x00002000
Load command 0
      cmd LC_SEGMENT_64
  cmdsize 1272
  segname
   vmaddr 0x0000000000000000
   vmsize 0x0000000000000b62

The first command is a 64-bit segment command. The complete output is deliberately longer than this excerpt and includes section records such as __TEXT,__text and DWARF sections. The sed limit is only for a first look; remove it when collecting a full report.

Use -L for the shorter question, "which shared libraries does this file use?":

$ $OTOOL -L "$SAMPLE"
/usr/share/go-1.22/src/debug/dwarf/testdata/typedef.macho:

No library entries are printed for this object. That is a property of this input, not a promise that every Mach-O file has no dependencies.

5. Dump one section without dumping everything

Use -s followed by the segment name and section name. For the fixture's code section:

$ $OTOOL -s __TEXT __text "$SAMPLE"
/usr/share/go-1.22/src/debug/dwarf/testdata/typedef.macho:
Contents of (__TEXT,__text) section
0000000000000000\t55 48 89 e5 c7 45 f8 00 00 00 00 8b 45 f8 89 45
0000000000000010\tfc 8b 45 fc 5d c3

Section names are not universal. First find the exact segname and sectname values with -l, then pass those values to -s. Do not guess __TEXT __text for a file whose load commands show different sections.

-x is the broader form: it prints all text sections. Use it when you genuinely need every text section, because its output can be much larger. -t selects the text section, while -v asks for verbose output or disassembly when text sections are printed. These are display choices; they do not turn the program into an emulator.

6. Handle universal files and output safely

A universal, or fat, Mach-O contains slices for more than one architecture. Use -f to print its universal headers, then -arch ARCH to select a slice for another inspection:

$ $OTOOL -f /path/to/universal-file
$ $OTOOL -arch x86_64 -h /path/to/universal-file

The architecture value must match a slice that actually exists. If it does not, the command rejects the selection. Run -f first instead of guessing from the file name.

Redirect output only after choosing a destination that will not overwrite evidence. This is safe for a new report:

$ $OTOOL -l "$SAMPLE" > mach-o-load-commands.txt
$ test -s mach-o-load-commands.txt && echo 'report written'
report written

Shell redirection truncates an existing file before llvm-otool-20 starts. If the report matters, use a new name or copy the old report first. The tool itself is read-only, but careless redirection can destroy your notes.

7. Diagnose the common traps

A missing path is a normal file-access error:

$ $OTOOL -h /path/to/missing-file
llvm-otool-20: error: '/path/to/missing-file': No such file or directory

Check the path and readability without escalating privileges:

$ ls -l /path/to/input
$ test -r /path/to/input && echo readable

An ELF file is not a substitute Mach-O input. This installed build reports that mismatch as an object-format error. Check the diagnostic itself and use file; do not infer success merely because a wrapper continued.

For the complete option list, run $OTOOL --help. Options such as -D for the shared-library ID, -r for relocations, -I for the indirect symbol table, -G for data-in-code information and -dyld_info for bind and rebase information are useful when the file contains the corresponding structures. Empty output can be a property of the file, not an error.

Done means