Процесс загрузки iPhone. Часть 1: Boot ROM

Здравствуйте, коллеги.

Мне показалось интересным поделиться с сообществом информацией о внутреннем устройстве техники Apple, так как статей на эту тему крайне мало. Начать я решил с iPhone. Поэтому предлагаю вам вместе со мной попробовать разобраться в работе этого загадочного девайса.

Я попытаюсь ориентироваться на самые последние модели. Буду рад, если ваши комментарии укажут на ошибки и помогут нам всем лучше понять, как работают устройства, которые нас окружают.

Введение

Не знаю, удивит это кого-нибудь или нет, но запуск iPhone мало чем отличается от процесса запуска IBM-PC-совместимого компьютера в виде системного блока под столом, о котором написано уже достаточно много. Наверное поэтому так мало статей на подобную тематику, относящихся к мобильным устройствам.

Если смотреть на процесс запуска iPhone, как на целостную картину, то он представляет собой цепочку доверительных переходов от одной стадии загрузки к другой, которая так и называется «Chain of trust». В общем случае, в процессе участвуют 3 независимых программы: Boot ROM, iBoot и ядро XNU (расположены в порядке выполнения). Передача управления от одного к другому происходит после проверки подлинности того, кому управление следует передать. Каждый из них имеет криптографическую подпись Apple. Возникает резонный вопрос: как проверяется подлинность первого шага? Ответ: никак.

Самым первым получает управление Boot ROM. Он является неизменяемым компонентом системы, прошивается на заводе-изготовителе и больше не меняется. Его невозможно обновить (в отличие от BIOS и UEFI). Следовательно, нет смысла проверять его подлинность. Поэтому он имеет соответствующий статус: «Аппаратный корень доверия (Hardware root of trust)». В память Boot ROM вшивается публичный ключ корневого сертификата Apple (Apple Root certificate authority (CA) public key), с помощью которого проверяется подлинность iBoot. В свою очередь iBoot проверяет своим ключом подлинность ядра XNU. Такая цепочка проверок позволяет запускать только доверенное ПО.

Chain of trustChain of trust

По естественным причинам, слабым местом в этой цепочке является код Boot ROM. Именно за счет уязвимостей в этой части системы и невозможности её обновить, удаётся обходить проверку подлинности и производить Jailbreak (побег из тюрьмы). Поэтому разработчики Boot ROM стараются не включать в него лишний функционал. Тем самым сокращается вероятность возникновения ошибок в коде, поскольку он остается минималистичным. Собранный образ имеет размер около 150 Кбайт. Каждый этап отрабатывает независимо от других, по заранее известным адресам и выполняет четко обозначенную задачу. Несмотря на это прошивка Boot ROM и iBoot компилируются из одной кодовой базы. Поэтому имеют схожие подсистемы. Они делят между собой базовые драйверы устройств (AES, ANC, USB), примитивные абстракции (подсистема задач, куча), библиотеки (env, libc, image), средства отладки и платформозависимый код (работа с SoC, MMU, NAND). Каждый последующий элемент цепочки является более сложной системой, чем предыдущий. Например iBoot уже поддерживает файловые системы, работу с изображениями, дисплей и т.д.

Для лучшего понимания описываемых компонентов приведу таблицу.

Задача

Проверка подписи

Известные аналоги

Место исполнения

1. Boot ROM

Найти загрузчик и передать ему управление

Нет

BIOS, UEFI, coreboot

SRAM

2. iBoot

Найти ОС и инициировать её загрузку

Да

GNU GRUB, Windows Bootmgr, efibootmgr

SDRAM

3. XNU

Обеспечить безопасный интерфейс к железу

Да

Linux, NT kernel, GNU Hurd

SDRAM

4. iOS

Выполнение пользовательских задач

Нет

Ubuntu, Windows, Android

SDRAM

Питание

При выключенном устройстве отсутствует подача питания на центральный процессор. Однако критически важные компоненты системы обеспечиваются энергией постоянно (контроллеры беспроводного сетевого соединения не входят в список важных, поэтому смартфон не может передавать никаких, в том числе секретных, данных в выключенном состоянии и соответственно отследить его невозможно). Одним из таких компонентов является интегральная схема управления питанием (PMIC — Power Management Integrated Circuit). В качестве источника питания для PMIC может служить аккумулятор с зарядом, внешний источник, соединенный разъемом Lightning, или беспроводное зарядное устройство (посредством электромагнитной индукции). Но для успешной загрузки операционной системы требуется наличие заряда на исправном аккумуляторе. Хотя теоретически устройство может функционировать подпитывая себя исключительно внешними источниками. Кроме этого у каждого источника питания имеется свой отдельный контроллер, но в контексте этой статьи их достаточно лишь иметь в виду.

Для подачи питания на центральный процессор PMIC должен получить сигнал на старт процедуры Power-On. Подать такой сигнал можно двумя способами: подключив устройство к внешнему источнику питания или с помощью боковой кнопки (длинным нажатием). Рассмотрим более детально классический способ включения — нажатием кнопки.

Исторически так сложилось, что для запуска портативных устройств используется длинное нажатие. Вероятно, это сделано для защиты от случайного включения-выключения устройства. В целом, ничто не мешает использовать короткое нажатие для достижения той же цели. Можно вспомнить, что если попытаться на уже работающем устройстве нажать боковую кнопку тем и другим способом, то в результате мы получим отклик на совершенно разные действия. Из этого мы можем сделать вывод, что существует механизм, который обеспечивает такую возможность. Обычно в тандеме с PMIC используется небольшой Side-Button контроллер, в задачи которого, среди прочего, входит: отличить метод нажатия на кнопку (длинный от короткого). Контроллер кнопки может питаться от того же источника, что и PMIC или от самого PMIC. Контроллер может быть выполнен в виде D-триггера с асинхронным сбросом. В исходном состоянии на асинхронный вход сброса CLR поступает сигнал. В свою очередь, на этом входе установлена RC-цепь, реализующая постоянную времени задержки.

Приблизительная схема работы боковой кнопкиПриблизительная схема работы боковой кнопки

Простым нажатием кнопки мы замыкаем электрическую цепь триггера, и на выход CTLx подаётся результирующий сигнал по-умолчанию. Для подачи сигнала на инициализацию запуска устройства время удержания кнопки питания должно превышать время задержки асинхронного сброса. Во время удержания кнопки сигнал на CLR входе затухает, и при очередном такте синхронизирующего сигнала CLK триггер меняет свое состояние, выдавая на выходе CTLx другое значение, сообщающее PMIC начать процедуру запуска устройства посредством подачи питания на центральный процессор.

SoC и CPU

Массовое производство высокотехнологичных полупроводниковых устройств и поддержание самих фабрик по их изготовлению является довольно дорогой задачей. Поэтому в мире со времени массовой популярности технологий, основанных на полупроводниковых устройствах, существует тенденция заключения контракта с фирмами, специализирующимися именно на производстве полупроводников, для которых такая контрактная работа и является бизнесом. Фабрики таких фирм-изготовителей чаще всего находятся в странах с относительно дешевой рабочей силой. Поэтому для изготовления систем на кристалле (System on a Crystal — SoC) у Apple заключен многолетний контракт с изготовителем полупроводниковых устройств из Тайваня — TSMC (Taiwan Semiconductor Manufacturing Corporation). Инженеры Apple проектируют, разрабатывают и программируют устройства, тестируют их используя опытное производство. Затем составляется спецификация, по которой компания-изготовитель должна будет произвести и поставить оговоренное количество экземпляров. При этом, все права целиком и полностью принадлежат компании Apple.

SoC инкапсулирует в себя множество электронных элементов составляющих аппаратный фундамент устройства. Среди которых, непосредственно, центральный процессор, оперативная память, графический процессор, ИИ-ускоритель, различные периферийные устройства и другие. Имеется также и свой контроллер питания. При достижении стабильного уровня напряжения на контроллере питания SoC запитываются внутренние компоненты. Практически каждый микропроцессор имеет специальное устройство для сброса текущих параметров и установки их в исходное состояние. Такое устройство называется генератор начального сброса (Power-on reset / PoR generator). В основные задачи этого генератора входят: ожидание стабилизации питания, старт тактовых генераторов и сброс состояний регистров. PoR генератор продолжает держать процессор в режиме сброса некоторое непродолжительное время, которое заранее известно.

Процедура Power-on resetПроцедура Power-on reset

Поскольку в этом случае мы имеем дело с ещё одним таймером, то можно предположить, что это также некая RC-цепь с триггером. По достижении установленного порога напряжения на этой цепи триггер меняет состояние (таким образом заканчивается таймаут сброса), PoR генератор становится неактивным, центральный процессор выходит из режима сброса и начинает свою работу.

Центральный процессор должен начать работу с выполнения определенной программы. Для этого ему необходимо знать, где искать эту программу. Своей работой PoR генератор установил регистры в значения по-умолчанию (исходные значения). В регистр счетчика команд (Program Counter/PC register) установился адрес первой инструкции в пространстве физической памяти. Это значение называется вектором сброса (Reset vector). Конкретное значение вектора сброса определяется микроархитектурой процессора и теоретически может различаться среди разных поколений процессоров, но в нашем случае это адрес 0×100000000. На аппаратном уровне определенные диапазоны адресов закреплены за физическими устройствами хранения и составляют вместе физическое адресное пространство (не путать с виртуальным адресным пространством, которое доступно из операционной системы). В процессе дальнейшего запуска устройства диапазон адресов может быть переназначен в произвольном порядке для более эффективной работы с памятью.

Следует заметить, что современные процессоры имеют несколько ядер, каждое из которых может независимо исполнять инструкции. Чтобы избежать неопределенности, какое из ядер должно начать выполнение первой инструкции, производитель на аппаратном уровне выделяет основное ядро (primary core), которое и будет производить загрузку. В дальнейшем остальные ядра подключаются к работе программно.

Обычно вектор сброса указывает на ячейку в постоянной памяти (Read only memory — ROM). Она располагается внутри SoC. Эта память является энергонезависимой (сохраняет свое состояние после отключения питания) и не перезаписываемой (код программы прошивается туда единожды при производстве устройства). Записанная при производстве программа и является отправной точкой работы центрального процессора. Модуль постоянной памяти и сама программа, записанная туда называются Boot ROM. Рассмотрим его задачи и работу более подробно.

Boot ROM

Как упоминалось ранее, Boot ROM — это чип, включаемый внутрь SoC. На этапе изготовления на фабрике в его память записывается специальная программа-загрузчик. Загрузчик проектируется и программируется в Apple. Код написан на языке C с вызовами ассемблерных процедур, выполняющих машинно-зависимые команды процессора. По нулевому адресу в пространстве памяти Boot ROM, с которого и начнет выполнение процессор, располагается входная точка скомпилированной программы-загрузчика, а именно — стандартная метка _start. Код, с которого всё начинается, полностью состоит из ассемблерных инструкций arm64. Он производит следующие действия:

  1. Включается кэш второго уровня (L2 cache) и конфигурируется для использования в качестве временной оперативной памяти (объем 2 MiB). 

  2. Поскольку диапазон рабочих адресов памяти статически определен, выполняется проверка текущего адреса. Если он неверен, то запускается цикл релокации на корректный адрес.

  3. Устанавливается виртуальный адрес функции main (начало кода на языке C) в регистр LR. Так что при выполнении инструкции ret управление перейдет в функцию main.

  4. Инициализируются указатели на начало стека. Задаются адреса для стека исключений, прерываний, данных.

  5. Создаются таблицы страниц и создаётся защита кучи от переполнения.

  6. Происходит копирование данных в оперативную память, а затем передача управления в функцию main.

Разметка оперативной памяти для Boot ROMРазметка оперативной памяти для Boot ROM

Практически весь код, который будет исполняться дальше, написан на языке C.

Сперва функция main запускает процедуру программной инициализации CPU.
Стоит отдельно оговорить, что процессор имеет несколько уровней привилегий для выполнения инструкций, называемых Exception Levels (EL): EL0, EL1, EL2, EL3. Цифра на конце обозначает уровень привилегий. Чем она выше — тем выше уровень доступа. Внутри операционной системы пользователь имеет самый низкий уровень привилегий и не может полностью управлять состоянием машины (в целях собственной же безопасности). Множество регистров и команд недоступно. Однако поначалу, процессор начинает работу с самого высокого уровня привилегий, поэтому загрузчик может успешно произвести начальную настройку оборудования.
Возвращаясь к процедуре программной инициализации CPU опишем её основные шаги.

  1. Конфигурация регистра безопасности (Secure Configuration Register — SCR): выставляются биты стандартных режимов работы для обработчика аварийного завершения и обработчиков аппаратных прерываний (FIQ и IRQ). 

  2. Сброс кэшей процессора для инструкций и данных. 

  3. Конфигурация регистра управления системой (System Control Register: SCTLR): включается бит проверки выравнивания стека, первичная настройка и активация блока управления памятью (Memory Management Unit — MMU, является частью CPU), отключение возможности выполнения кода из сегментов памяти, помеченных как доступные для записи (установка Execute Never / XN бита — аналог NX-бита в x86 системах), активация кэша инструкций и кэша данных. 

  4. Активируется сопроцессор для операций с плавающей точкой. 

Управление возвращается в функцию main, и продолжается работа загрузчика.

Следующим шагом происходит программная настройка тактовых генераторов в значения по умолчанию:

  1. Устанавливается частота осциллятора контроллера питания.

  2. Инициализация подсистемы динамического масштабирования частоты и напряжения (DVFS — Dynamic voltage and frequency scaling).

  3. Подача питания на осцилляторы устройств, участвующих в загрузке BootROM.

  4. Подстановка характеристик частоты и напряжения для режима BootROM.

  5. Настройка подсистемы фазовой автоподстройки частоты (PLL — Phase Lock loop).

  6. Происходит включение сопроцессора защищенного анклава (SEP — Secure Enclave processor).

Сопроцессор защищенного анклава имеет свою собственную прошивку (sepOS), которая так же проходит стадии безопасной загрузки и проверки на подлинность. Но об этом в другой статье.

Далее следует инициализация шины внутренней памяти процессора (она же кэш-память). Роль кэш памяти играет статическая памяти с произвольным доступом (Static Random Access Memory — SRAM). Не путать с динамическим типом памяти, которую мы называем оперативной. Она обладает большим объемом (Dynamic Random Access Memory — DRAM). Различие в том, что ячейки SRAM основаны на триггерах, а у DRAM на конденсаторах. Память на триггерах требует большее количество транзисторов и соответственно занимает больше места на подложке. В свою очередь, ячейки памяти на конденсаторах со временем теряют заряд. Поэтому необходимо периодически производить холостую перезапись в фоновом режиме, что несколько влияет на быстроту взаимодействия. Таким образом SRAM используется в качестве кэша (небольшой объем, быстрый доступ), а DRAM в качестве основной памяти (больший объем, быстродействие вторично). На SoC инициализируются линии контактов GPIO (General Purpose Input/Output) и соответствующий драйвер. С помощью этих контактов следующим этапом, помимо прочего, проверяется состояние кнопок устройства, нажатие которых определяет необходимость принудительной загрузки в DFU режиме (Device Firmware Upgrade mode — режим обновления прошивки устройства или восстановления). Описание работы этого режима заслуживает отдельной статьи, поэтому не будем касаться его сейчас.

Представляя собой минималистичную разновидность базовой системы ввода/вывода (BIOS), Boot ROM выделяет соответствующие абстракции: подсистема задач (аналог процессов) и куча (heap). И производит их инициализацию. Подсистема задач позволяет выполнять инструкции в несколько потоков, хотя эта возможность не используется в Boot ROM.

Идем дальше: инициализация аппаратного обеспечения, специфичного для конкретной SoC. Для последних моделей iPhone приблизительный список таков:

  1. Инициализация драйвера контроллера питания

  2. Инициализация драйвера системных часов 

  3. Инициализация контроллера прерываний

  4. Старт таймеров

  5. Настройка контроллера питания и GPIO контактов для конкретной платформы

Настройка необходимого для старта оборудования выполнена, и запущены основные подсистемы. Затем выполняется программная проверка принудительной загрузки в DFU режиме. На этот раз таймер на отслеживание удержания соответствующих кнопок задается программно, после чего снимаются их состояния, и в зависимости от результата выполняется сброс системы на последующую загрузку в DFU режиме, либо происходит выборка устройства, с которого дальше производить загрузку.

Процесс запуска заходит в бесконечный цикл, в котором можно задержаться не более двух итераций. Если установлен DFU флаг, то в качестве устройства загрузки выбирается режим восстановления по USB, иначе будет произведена загрузка с устройства по-умолчанию для конкретной платформы (в нашем случае NAND флэш память).

if (dfu_enabled) 
  boot_fallback_step = -1;

while (1) {
  if (!get_boot_device(&device, &options))
    break;

  process_boot(device, options);

  if (boot_fallback_step < 0)
    continue;

  boot_fallback_step++;
}

reset();

Устройство может впасть в бесконечный цикл перезагрузки, если невозможно определить конфигурацию выборки дальнейшего загрузчика (такое может произойти только если оборудование физически повреждено). При возникновении любой другой проблемы, устройство будет переведено в режим восстановления по USB. Отсюда следует, что при исправном оборудовании невозможно сделать из устройства «кирпич».

Apple использует особый формат файлов для хранения примитивных исполняемых файлов — IMG4 (четвертая версия). Он представляет собой закодированные по DER схеме объекты стандарта ASN.1.

sequence [
   0: string "IMG4"
   1: payload   - IMG4 Payload, IM4P
   2: [0] (constructed) [
          manifest   - IMG4 Manifest, IM4M
      ]
]
sequence [
    0: string "IM4P"
    1: string type    - ibot, rdsk, sepi, ...
    2: string description    - 'iBoot-6723.102.4'
    3: octetstring    - the encrypted/raw data
    4: octetstring    - containing DER encoded KBAG values (optional)
        sequence [
            sequence [
                0: int: 01
                1: octetstring: iv
                2: octetstring: key
            ]
            sequence [
                0: int: 02
                1: octetstring: iv
                2: octetstring: key
            ]
        ]
]

Активируется утилита управления устройствами (UtilDM — Utility Device Manager), инициализируются ANC (Abstract NAND Chip) драйвер и производится сброс регистров контроллера флэш памяти. Затем дается команда NAND контроллеру перевести устройство в режим чтения, после чего из его памяти постранично считывается загрузчик iBoot. Из прочитанных байтов генерируется экземпляр структуры файла образа IMG4.
Экземпляр содержит заголовки, служебную информацию и указатель на сам образ в памяти. Дальше по этому указателю происходит обращение, и выгрузка образа в безопасный буфер. Там выполняется парсинг и валидация образа. Из текущих параметров системы собирается специальный объект — окружение (environment) и сопоставляется с характеристиками образа. Проверяются заголовки, манифест, сравниваются хэши, происходит проверка подписи образа по публичному ключу Boot ROM (Apple Root CA public key).

Если все прошло успешно, то наступает заключительный этап работы Boot ROM. Создается функция-трамплин, позволяющая выполнить безусловный переход к началу iBoot. Поскольку никакая информация не должна быть передана на следующую стадию запуска устройства, и невозможно было бы вернуться назад, перед прыжком функции-трамплина сбрасываются значения регистров, отключается кэширование, прерывания, таймеры и т.д.
iBoot начнет свою работу практически с чистого листа, словно он первый в этой цепочке.

На этом все. В следующей части мы попробуем разобраться как работает второй этап загрузки iPhone — iBoot.

Спасибо за внимание.

Источники:

Apple: Boot process for iOS and iPad devices
Apple: Hardware security overview
Design & Reuse: Method for Booting ARM Based Multi-Core SoCs
Maxim integrated: Power-on reset and related supervisory functions
The iPhone wiki
ARM: Documentation
Jonathan Levin: MacOS and *OS internals
Wikipedia
Алиса Шевченко: iBoot address space
Harry Moulton: Inside XNU Series
Ilhan Raja: checkra1n
Texas Instruments: Push-Button Circuit
iFixit: iPhone 12 and 12 Pro Teardown
Исходные коды SecureROM и iBoot, утекшие в сеть в феврале 2018 года

© Habrahabr.ru