What is required for a Mach-O executable to load?

Since 10.10.5 Yosemite, the executable file must be at least 4096 bytes long ( PAGE_SIZE ), or it will be killed immediately. The relevant code found by @Siguza in the XNU kernel exec_activate_image function https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/bsd/kern/kern_exec.c#L1456

Without dyld

Assuming you want a 64-bit macOS executable using only system calls, you need:

  • Mach-O 64-bit Header
  • LC_SEGMENT_64 __PAGEZERO (with nonzero size, name can be anything)
  • LC_SEGMENT_64 __TEXT (name can be anything; must be readable and executable; sections are optional)
  • LC_UNIXTHREAD

Here is my example for this case.

With dyld

You can't do much without dyld though, so if you want to use it the minimal set is:

  • Mach-O 64-bit Header
  • LC_SEGMENT_64 __PAGEZERO (with nonzero size)
  • LC_SEGMENT_64 __TEXT (name can be anything; must be readable and executable; sections are optional)
  • LC_SEGMENT_64 __LINKEDIT (must be writable because dyld requires a writable segment, in a ld linked binary the writable segment typically would be __DATA)
  • LC_DYLD_INFO_ONLY (specifies where the actual dyld load commands physically are in the executable, typically they will be found __LINKEDIT but there's no limitation on this) or interestingly LC_SYMTAB instead, which would make the actual dyld impossible to use without LC_DYLD_INFO_ONLY.
  • LC_DYSYMTAB (this can be empty)
  • LC_LOAD_DYLINKER
  • LC_MAIN or LC_UNIXTHREAD
  • LC_LOAD_DYLIB (at least one actual dylib to load that eventually depends on libSystem or libSystem itself for LC_MAIN to work)

Additionally since MacOS Monterey 12.3:

  • LC_SYMTAB (which is now required if LC_DYSYMTAB is used)

LC_UNIXTHREAD and LC_MAIN

In modern executables (since 10.7 Mountain Lion), LC_UNIXTHREAD is replaced by LC_MAIN, which requires dyld — but LC_UNIXTHREAD is still supported for any executable as of 10.12 Sierra (and it should be in future MacOS versions, because it's utilised by dyld executable itself to actually start).

For dyld to work the extra steps depend on type of binding:
bind at load is the least effort approach , where LC_DYLD_INFO_ONLY pointing to valid dyld load commands pointing to writable segment will do the trick.
lazy binding additionally requires extra platform specific code in __TEXT which utilises binded at load time dyld_stub_binder to lazy load address of a dyld loaded function.
There are other types of dyld binding which I don't cover here.

Further details can be found here: https://github.com/opensource-apple/dyld/blob/master/src/ImageLoaderMachO.cpp

Tags:

Mach O

Xnu