Mach-O (Mach Object) is a binary file format used by macOS and iOS for executables, libraries, and object code. A Mach-O file consists of a header, followed by a series of load commands, and then a series of segments.
The 4GB Limitations
The original 32-bit format used 32-bit offsets, which can address a maximum of 2^32 bytes, or 4GB of memory. The 64-bit variant uses 64-bit offsets, vastly increasing the addressable memory space. However, somewhat surprisingly, the on-disk size of a Mach-O file is still limited to 4GB due to the use of 32-bit file offsets.
Section Commands
Directly following the segment load command is an array of section data structures. Notably, the 64-bit variant of this structure still uses 32 bits to encode its file offset.
From mach-o/loader.h:
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
The Symbol Table
The symbol table load command (LC_SYMTAB) contains the offsets and sizes of
the link-edit stab-style symbol table. Both the symbol table and string table
offsets are encoded as 32-bit values.
From mach-o/loader.h:
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};
Fat Binaries
Multiple Mach-O files can be combined into a multi-architecture binary,
commonly referred to as a “universal binary” or “fat binary.” A fat binary
consists of a fat_header structure followed by multiple fat_arch structures.
Notably, these structures still use 32-bit file offsets.
From mach-o/fat.h:
struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint32_t offset; /* file offset to this object file */
uint32_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
};
The individual architecture-specific components of a fat binary are called slices. Due to the use of 32-bit offsets, Universal Mach-O files cannot contain a slice that starts beyond the 4GB boundary. For example, if you have three binaries, each 3GB in size, you can create a fat binary with two slices—one starting near offset 0 and the second near 3GB. However, a third slice at approximately 6GB would exceed the 4GB limit and cause an overflow.
Debug Info
In practice, binaries rarely exceed the 4GB limit. The main exception is dSYMs.
A dSYM is a bundle that contains a “Mach-O companion file” — a stripped-down
binary that retains only the debug info sections. The .debug_info section, in
particular, can be very large. As mentioned earlier, this issue is further
compounded when building for multiple architectures.
To address this, dsymutil, the DWARF linker on macOS, uses a 64-bit variant
of the fat header:
struct fat_arch_64 {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint64_t offset; /* file offset to this object file */
uint64_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
uint32_t reserved; /* reserved */
};
Older versions of dsymutil required passing the -fat64 flag to enable this
format. In newer versions, a warning is issued when a binary exceeds 4GB
(unless -fat64 is explicitly provided), and the tool automatically switches
to the 64-bit fat header.
Starting with macOS Sequoia and Xcode 16, both LLDB (the debugger) and CoreSymbolication fully support dSYMs with a 64-bit fat header.