Mach-O Binary Inspector
Reads a Mach-O binary the way otool and lipo do, straight out of the bytes and entirely in your browser. A macOS or iOS file is either thin — one architecture — or universal, a big-endian table of architectures each pointing at a complete image; both are handled, and for a universal file you get the architecture table with every slice’s offset, size and alignment before you choose a slice and read the rest. Inside a slice the header gives the CPU type and subtype (arm64, arm64e, x86_64, i386, ppc), the file type — executable, dynamic library, bundle, relocatable object, dSYM — and the flags worth knowing: MH_PIE, MH_TWOLEVEL, MH_DYLDLINK, MH_ALLOW_STACK_EXECUTION and MH_NO_HEAP_EXECUTION. The load commands supply the rest: every segment with its VM address, VM size, file offset and file size, the sections inside each one (__TEXT,__text or __DATA,__const), each LC_LOAD_DYLIB and LC_LOAD_WEAK_DYLIB with its current and compatibility versions, the LC_RPATH entries the loader substitutes for @rpath, the LC_ID_DYLIB install name a library publishes itself under, the UUID that pairs a binary with its dSYM, and the platform, minimum OS and SDK from LC_BUILD_VERSION or the older LC_VERSION_MIN_MACOSX. The segment sizes are the answer to “why is this binary 40 MB” — read them carefully, because a segment’s size in memory can be far larger than its size on disk: zero-fill sections and __PAGEZERO’s four gigabytes occupy no file at all. Two things this will not do. It does not disassemble. And it does not verify the code signature: an LC_CODE_SIGNATURE command means the file carries a signature blob, while whether that signature is valid, whose certificate signed it and whether the binary was notarized are questions only codesign and spctl on a Mac can answer. Byte order and word size both come out of the magic rather than being assumed, so a big-endian 32-bit PowerPC binary from twenty years ago opens as readily as an arm64e one, and a file whose load-command table claims more bytes than it holds is refused by name instead of being half-read.
How to use
- Drop the binary onto the box. A command-line tool with no extension, a .dylib, a .bundle, a .o, a dSYM’s DWARF file or the executable from inside an .app all work.
- If the file is universal, read the architecture table first — that is the “does this ship arm64?” answer — then click an architecture to see its header, segments and libraries.
- Check the hardening chips: PIE should be Yes on anything modern, and an executable stack is worth an explanation before you ship the file.
- Look at Linked libraries for what the loader will need at run time, at Runtime search paths for the directories @rpath expands to, and at Install name for the path a library claims for itself.
- Open the section list when you are hunting for size: __TEXT is code, __DATA is writable data, __LINKEDIT holds the symbol tables and the signature, and a dSYM’s __DWARF sections are usually the largest thing on disk.
Frequently asked questions
- How do I tell whether an app ships an arm64 (Apple silicon) slice?
- Open the binary inside the .app — Contents/MacOS/<name> — and look at the architecture table. A universal file starts with a big-endian fat header listing each architecture, its offset into the file and the alignment its slice was placed on; anything not listed there is not in the file, so an app with only x86_64 will run under Rosetta rather than natively. Each slice is a complete Mach-O image with its own header and load commands, which is why a universal binary is roughly the sum of its parts, and why lipo can thin one down by copying a single slice out.
- Does a code signature mean the binary is trustworthy?
- No, and this tool is careful about the difference. LC_CODE_SIGNATURE only says that a signature blob sits at some offset inside __LINKEDIT; it does not say the hashes still match the pages they cover, that the certificate chains up to Apple, that the certificate has not been revoked, or that the binary was notarized. Verifying any of that needs the certificate chain and, for notarization, Apple’s servers — that is codesign --verify --deep --strict and spctl --assess on a Mac. The hardened runtime is not in the header either: it lives in the signature’s entitlements and flags, so a header alone cannot tell you whether it is on.
- Why is the binary so much bigger than the code in it?
- Read the segment table with the two size columns side by side. __TEXT holds the code and read-only data and is usually mapped read-execute; __DATA and __DATA_CONST hold writable data; __LINKEDIT holds the symbol table, the function starts, the dyld fixups and the code signature, and is often the largest thing in a shipped binary. Size in memory and size in the file are different numbers on purpose: zero-fill sections such as __bss occupy address space but no bytes on disk, and __PAGEZERO reserves four gigabytes of address space that will never be mapped from the file. If the file is a universal one, remember that you are also carrying every architecture at once.
- What is an install name, and why do I keep seeing @rpath?
- A dynamic library records the path it expects to be found at in LC_ID_DYLIB, and every program that links against it copies that string into its own LC_LOAD_DYLIB. If the string is an absolute path, the library has to be at exactly that path on every machine. Modern frameworks instead use @rpath/Something.framework/Something and leave the resolution to the program: the LC_RPATH entries listed here are the directories dyld substitutes for @rpath, in order, and they are usually written relative to the binary with @executable_path or @loader_path so an app bundle can be moved anywhere. A missing rpath is the usual cause of “image not found” at launch.
- What does the Encrypted chip mean on an iOS binary?
- App Store binaries for iOS carry an LC_ENCRYPTION_INFO (or the 64-bit form) whose cryptid is 1, which marks a byte range — normally most of __TEXT — as encrypted with a per-device key applied by the App Store. The kernel decrypts that range while loading, so the file on disk is unreadable in that region: strings finds nothing useful and a disassembler sees noise. A binary you built yourself, or one pulled from an IPA before the store re-signed it, has cryptid 0 and is plain. The command is present but zero on plenty of files, which is why this tool reports the cryptid rather than just the command.
- Is the binary uploaded anywhere?
- No. The file is read with the browser’s File API and parsed by JavaScript inside the page. Nothing is sent to a server and there is no server-side component to send it to, which matters more here than for most tools: an unreleased build or a customer’s app is not something you want to hand to a website. You can disconnect from the network after the page has loaded and everything still works.
Related tools
ELF Binary Inspector
Open a Linux executable, .so or .o in your browser: architecture, shared libraries it needs, build ID, and whether it is PIE, NX and RELRO hardened.
PE Inspector: Windows EXE and DLL Viewer
Open a Windows .exe, .dll, .sys or .efi in your browser: architecture, subsystem, imports, exports, sections, and whether ASLR, DEP and CFG are on.
Java Class File Inspector
Read a compiled .class file in the browser: class file version and JDK release, access flags, constant pool, fields, methods and referenced classes.
Font File Inspector
Open a .ttf, .otf or .woff and read its real family name, glyph and character counts, version, licence text and embedding permissions.
gzip (.gz) Inspector
Read a .gz header field by field, walk every member, and verify the trailer by recomputing CRC32 and the real size in your browser.
xz (.xz) Stream Inspector
Read an .xz container: stream header, every block header and filter chain, the index and the footer — with the true uncompressed size, unpacking nothing.