Exploring iOS-es Mach-O executable structure

Cyril Cermak
6 min readJun 1, 2020

--

Let’s have a look under the hood of iOS executable format, Mach-O.

In this article, we are going to explore the Mach-O (Mach-Object) executable, which is a native format for executables on macOS, iOS and other systems based on Mach-kernel.

First things first, let’s create an IPA file so we can explore it.

iOS App Store Package (IPA)

To create an IPA in Xcode, archive an App first, click distribute in the organizer and then select export for development teams.

When the exporting is finished and your IPA is created, simply exchange the .ipa extension for .zip and unzip it. In the unzipped folder will be a directory called Payload that will contain the App.

For the demonstration purposes, I created IPA that is targeting iOS 10 up to the latest iOS 13 from this repository.

If you don’t have an app to practice on you can download a compiled DVIA-v2 app here.

Now let’s have a look at the binary. Since the app targets iOS 10 which supports 32-bit architecture the produced binary by Xcode should be a fat binary. Wait, what is a fat binary?

Fat binary

The term fat binary is quite known but what does it really mean and how to recognize it? Also is the binary really fat? Why? Not enough cardio?

Fat binary simply means that the executable can run on more than one CPU architecture, for example in the iOS world it can beamrv7 and arm64. Where the armv7 is a 32bit architecture (e.g iPhone 3GS/4/4S…) and arm64 is a 64-bit architecture starting with iPhone 5S.

For a simple analyzation of Mach-O executables Apple ships otool with macOS. A tool that can explore the Mach-O binary. To know the supported architectures of the binary it is necessary to pass -fv flags. Where -f prints fat headers and -v translate the output into known symbols.

The output of the following command otool -fv ~/RE/DVIA-v2/Payload/DVIA-v2.app/DVIA-v2could look like this:

Fat headers
fat_magic FAT_MAGIC
nfat_arch 2
architecture armv7
cputype CPU_TYPE_ARM
cpusubtype CPU_SUBTYPE_ARM_V7
capabilities 0x0
offset 16384
size 2964496
align 2^14 (16384)
architecture arm64
cputype CPU_TYPE_ARM64
cpusubtype CPU_SUBTYPE_ARM64_ALL
capabilities 0x0
offset 2981888
size 3573088
align 2^14 (16384)

If the output is blank that simply means the executable is not a fat binary and only one architecture is targeted. To know what is the target architecture when the output is blank it is necessary to have a look at the Mach-O header, explained below.

We will explore more what the output means in the next section, which is focusing on the Mach-O format.

Mach-O format

As mentioned before the Mach-O is a format for native macOS and iOS executables. The format consists of many commands and data types which are represented by C structs. All structs can be easily explored via Xcode.

I will go through the defined structures in order as they are placed in the compiled binary.

Disassembled Mach-O part of a binary

There are many disassemblers that can be used for exploring Mach-O files. My two favourites are Hopper and Radare2. For this demonstration, I used Hopper to disassemble the binary and explore the Mach-O part in it. A full disassembled output can be found here.

A sneak-peek that contains the header and a segment commands can be seen on the snippet below.

Example of Mach-O structs used within the compiled binary

In the very beginning of the binary is Mach-O header so let’s start with it.

Mach-O header

Each Mach-O binary starts with a header. The otool program will help us here again. Passing to otool the parameter h will output the Mach-O header or headers if it is a fat binary. The command otool -hv ./DVIA-v2 will result in the following output:

Mach-O headers

Because I have a fat binary the output of otool produces two results one for ARM(32-bit) and one for ARM64(64-bit).

Where did the header come from?

The answer sits in /usr/include/mach-o/loader.h. A struct mach_header and mach_header_64 is declared there that contains all values that the otool output produced. The struct comment helps a lot in its understanding.

Declaration of Mach-O header

Let’s have a look at some of the parameters the header contains.

uint32_t magic is declared within the loader.h as well and posses those two declarations:

MH_MAGIC for Mach-O executable

cputype is declared in /usr/include/mach/machine.h and there sits the CPU_TYPE_ARM and CPUT_TYPE_ARM64 alongside with all other supported CPU architectures by Mach-O.

The rest of the parameters in the header struct I will leave for your own exploration. :)

After the header in the binary comes the Mach-O segment.

Mach-O segment

A segment represents a part of the executable that is loaded into the memory. It is the app itself alongside with all linked libraries and frameworks with a linker. Fortunately, segments struct comment speaks for itself.

Mach-O segment struct

Mach-O section

The previously mentioned segment command is made up of zero or more sections. When you are in a program runtime inlldb you can explore all sections of the app via image dump sections command. That will dump all segments and their sections. On the image below is output of all sections within DVIA-v2 segment which is the main executable.

Sections dump for DVIA-v2 segment, the main executable

To get a segment of a loaded 3rd party framework the command can be executed with the name of the included library. E.g image dump sections Realm would produce the following output:

Sections dump for Realm segment, a third-party framework

A section struct looks as follows.

Mach-O section struct

There is so much more to explore, I would highly recommend reading through the whole loader.h file, as it is full of interesting stuff so as machine.h and mach.h.

Next, I will list only names of structs/commands that are being used in their specific order within the binary.

__macho_dyld_info_command
__macho_symtab_command
__macho_dysymtab_command
__macho_dylinker_command
__macho_uuid_command
__macho_version_min_command
__macho_load_command
__macho_entry_point_command
__macho_load_command
__macho_dylib_command
__macho_rpath_command
__macho__linkedit_data_command

Let’s have a brief look at the highlighted commands. Thanks to their struct comments there is no need for much of a further explanation. Thanks, Apple!

__macho_uuid_command

This struct declares the unique app install identifier that the app developer can and sometimes must rely on.

Mach-O uuid-command struct

__macho_version_min_command

This is a command that identifies OS version the executable can run on.

Mach-O version_min_command struct

__macho_entry_point_command

A command that points to the main function. The main function is the programs entry point.

Mach-O entry_point_command struct

__macho_dylib_command

With the dylib_command, the dynamic frameworks and libraries are defined and then by a dynamic linker -dyld loaded into the app memory.

Mach-O dylib_command struct

In Conclusion

This article gives an overview of how Mach-O executable format looks like in a practical explorational way. I hope that it gave enough information to continue with the exploration on your own.

For more detail information about this topic, I would highly recommend studying these articles:

Mike Ash — Let’s build a Mach-O executable
OSX ABi Mach-O File Format reference
Apple — Mach-O topics

If you like what you read please share and give a clap or fifty. :)

If you liked this article and want to know more then please have a look at my book Modular Architecture on iOS and macOS — Building large scalable iOS and macOS apps and frameworks with Domain Driven Design where I keep the latest development practices I learnt along the way. Or simply get in touch on LinkedIn.

--

--

Cyril Cermak

iOS Engineering Lead @ Porsche AG. Setting goals and achieving them in AchieveMe.