Давайте заглянем под капот исполняемого формата iOS, Mach-O.

В этой статье мы собираемся изучить исполняемый файл Mach-O (Mach-Object), который является собственным форматом для исполняемых файлов в macOS, iOS и других системах, основанных на ядре Mach.

Прежде всего, давайте создадим файл IPA, чтобы мы могли его изучить.

Пакет iOS App Store (IPA)

Чтобы создать IPA в Xcode, сначала заархивируйте приложение, щелкните распространить в органайзере, а затем выберите экспорт для групп разработчиков.

Когда экспорт будет завершен и ваш IPA будет создан, просто замените расширение .ipa на .zip и разархивируйте его. В распакованной папке будет каталог с именем Payload, который будет содержать приложение.

В демонстрационных целях я создал IPA, ориентированный на iOS 10 до последней версии iOS 13 из репозитория this.

Если у вас нет приложения для практики, вы можете скачать скомпилированное приложение DVIA-v2 здесь.

Теперь давайте посмотрим на двоичный файл. Поскольку приложение нацелено на iOS 10, которая поддерживает 32-разрядную архитектуру, создаваемый Xcode двоичный файл должен быть толстым двоичным файлом. Подождите, что такое толстый двоичный файл?

Жир двоичный

Термин «толстый двоичный файл» достаточно известен, но что он означает на самом деле и как его распознать? И действительно ли двоичный файл толстый? Почему? Недостаточно кардио?

Жирный двоичный файл просто означает, что исполняемый файл может работать на нескольких архитектурах ЦП, например, в мире iOS это может быть amrv7 и arm64. Где armv7 - это 32-битная архитектура (например, iPhone 3GS / 4 / 4S…), а arm64 - 64-битная архитектура, начиная с iPhone 5S.

Для простого анализа исполняемых файлов Mach-O Apple поставляет otool macOS. Инструмент, который может исследовать двоичный файл Mach-O. Чтобы узнать поддерживаемые архитектуры двоичного файла, необходимо передать -fv флагов. Где -f печатает толстые заголовки, а -v переводит вывод в известные символы.

Результат выполнения следующей команды otool -fv ~/RE/DVIA-v2/Payload/DVIA-v2.app/DVIA-v2 может выглядеть так:

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)

Если выходные данные пустые, это просто означает, что исполняемый файл не является толстым двоичным файлом и нацелена только на одну архитектуру. Чтобы узнать, какова целевая архитектура при пустом выводе, необходимо взглянуть на заголовок Mach-O, описанный ниже.

Мы подробнее рассмотрим, что означает вывод, в следующем разделе, посвященном формату Mach-O.

Формат Mach-O

Как упоминалось ранее, Mach-O - это формат для собственных исполняемых файлов macOS и iOS. Формат состоит из множества команд и типов данных, которые представлены структурами C. Все структуры можно легко изучить через Xcode.

Я пройдусь по определенным структурам в порядке их размещения в скомпилированном двоичном файле.

Дизассемблированная часть двоичного файла Mach-O

Существует множество дизассемблеров, которые можно использовать для изучения файлов Mach-O. Два моих фаворита - Hopper и Radare2. Для этой демонстрации я использовал Hopper, чтобы разобрать двоичный файл и исследовать в нем часть Mach-O. Полный разобранный вывод можно найти здесь.

Краткий обзор, содержащий команды заголовка и сегмента, можно увидеть во фрагменте ниже.

В самом начале двоичного файла находится заголовок Mach-O, поэтому начнем с него.

Заголовок Mach-O

Каждый двоичный файл Mach-O начинается с заголовка. И здесь нам поможет программа otool. При переходе к otool параметр h выводит заголовок Mach-O или заголовки, если это толстый двоичный файл. Команда otool -hv ./DVIA-v2 приведет к следующему результату:

Поскольку у меня толстый двоичный файл, вывод otool дает два результата: один для ARM (32-разрядный) и один для ARM64 (64-разрядный).

Откуда взялся заголовок?

Ответ находится в /usr/include/mach-o/loader.h. Здесь объявляется структура mach_header и mach_header_64, которая содержит все значения, полученные в результате otool. Структурный комментарий очень помогает в его понимании.

Давайте посмотрим на некоторые параметры, которые содержит заголовок.

uint32_t magic также объявлен в loader.h и содержит эти два объявления:

cputpe объявлен в /usr/include/mach/machine.h, и там находятся CPU_TYPE_ARM и CPUT_TYPE_ARM64 вместе со всеми другими архитектурами ЦП, поддерживаемыми Mach-O.

Остальные параметры в структуре заголовка я оставлю для вашего собственного исследования. :)

После заголовка в двоичном файле идет сегмент Mach-O.

Сегмент Mach-O

Сегмент представляет собой часть исполняемого файла, загруженного в память. Это само приложение вместе со всеми связанными библиотеками и фреймворками с компоновщиком. К счастью, комментарий структуры сегментов говорит сам за себя.

Раздел Mach-O

Ранее упомянутая команда сегмента состоит из нуля или более разделов. Когда вы находитесь в среде выполнения программы inlldb, вы можете исследовать все разделы приложения с помощью команды image dump sections. Это сбросит все сегменты и их разделы. На изображении ниже показаны все разделы в сегменте DVIA-v2, который является основным исполняемым файлом.

Чтобы получить сегмент загруженного стороннего фреймворка, можно выполнить команду с именем включенной библиотеки. Например, image dump sections Realm выдаст следующий результат:

Структура раздела выглядит следующим образом.

Есть еще много всего, что нужно исследовать, я настоятельно рекомендую прочитать весь файл loader.h, так как он полон интересных вещей, таких как machine.h и mach.h.

Далее я перечислю только имена структур / команд, которые используются в их определенном порядке в двоичном файле.

__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

Давайте кратко рассмотрим выделенные команды. Благодаря их структурным комментариям нет необходимости в дополнительных объяснениях. Спасибо, Apple!

__macho_uuid_command

Эта структура объявляет уникальный идентификатор установки приложения, на который разработчик приложения может, а иногда и должен полагаться.

__macho_version_min_command

Это команда, которая определяет версию ОС, в которой может работать исполняемый файл.

__macho_entry_point_command

Команда, указывающая на функцию main. Основная функция - точка входа в программы.

__macho_dylib_command

В dylib_command определяются динамические фреймворки и библиотеки, а затем динамический компоновщик -dyld загружается в память приложения.

В заключении

В этой статье дается обзор того, как исполняемый формат Mach-O выглядит с практической точки зрения. Я надеюсь, что он дал достаточно информации, чтобы продолжить исследование самостоятельно.

Для получения более подробной информации по этой теме я настоятельно рекомендую изучить эти статьи:

Майк Эш - ​​Давайте создадим исполняемый файл Mach-O
Справочник по формату файлов OSX ABi Mach-O
Apple - темы Mach-O

Если вам нравится то, что вы читаете, поделитесь и похлопайте пятьдесят. :)

Если вам понравилась эта статья и вы хотите узнать больше, пожалуйста, ознакомьтесь с моей книгой Модульная архитектура на iOS и macOS - Создание больших масштабируемых приложений и фреймворков iOS и macOS с помощью Domain Driven Design, где я храню последние практики разработки, которые я изучил вместе путь. Или просто свяжитесь с LinkedIn.