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.
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.
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.
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.
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.
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.
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.
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.
file identified the input as Mach-O before inspection.-h for the header and -l to discover real sections.-L for dependencies and -s for a targeted section dump.