Русский
Русский
English
Статистика
Реклама

Загрузчик

Перевод Пишем загрузчик на Ассемблере и C. Часть 1

07.01.2021 16:12:32 | Автор: admin


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

О чем пойдет речь?


Мы рассмотрим написание кода программы и его копирование в загрузочный сектор образа флоппи-диска, после чего с помощью эмулятора bochs (x86) для Linux научимся проверять работоспособность полученной дискеты с загрузчиком.

О чем речь не пойдет


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

Структура статьи


Начнем мы со знакомства с основами, после чего перейдем к написанию самого кода. В целом план будет такой:

Знакомство с загрузочными устройствами.
Знакомсто со средой разработки.
Знакомство с микропроцессором.
Написание кода на ассемблере.
Написание кода на компиляторе.
Создание мини-программы для отображения прямоугольников.

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

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

Знакомство с загрузочными устройствами


Что происходит при включении стандартного компьютера?


Обычно при нажатии кнопки включения питания от нее подается сигнал блоку питания о необходимости подачи необходимого напряжения на внутреннее и внешнее оборудование компьютера, такое как процессор, монитор, клавиатура и пр. Процессор при этом инициализирует ПЗУ-чип BIOS (базовую систему ввода/вывода) для загрузки содержащейся в нем исполняемой программы, именуемой также BIOS.

После запуска BIOS выполняет следующие задачи:
Тестирование оборудования при подаче питания (Power On self Test).
Проверка частоты и доступности шин.
Проверка системных часов и аппаратной информации в CMOS RAM.
Проверка настроек системы, предустановок оборудования и т.д.
Тестирование подключенного оборудования, начиная с RAM, дисководов, оптических приводов, HDD и т.д.
В зависимости от определенной в разделе загрузочных устройств информации выполняет поиск загрузочного диска и переходит к его инициализации.

К сведению: все ЦПУ с архитектурой x86 в процессе загрузки запускаются в реальном режиме (Real Mode).

Что такое загрузочное устройство?


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

Что такое сектор?


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

Что такое загрузочный сектор?


Загрузочный сектор или блок загрузки это область загрузочного устройства, в которой содержится загружаемый в RAM машинный код, за что отвечает встроенная в ПК прошивка на стадии инициализации. На флоппи-диске размер сектора составляет 512 байт. Чуть позже о байтах будет сказано дополнительно.

Как работает загрузочное устройство?


При его инициализации BIOS находит и загружает первый сектор (загрузочный) в RAM и начинает его выполнение. Расположенный в загрузочном секторе код является первой программой, которую можно отредактировать для определения дальнейшего функционирования компьютера после его запуска. Здесь я имею в виду, что вы можете написать собственный код и скопировать его в загрузочный сектор, чтобы система работала так, как вам нужно. Сам же этот код и будет называться тем самым начальным загрузчиком.

Что такое начальный загрузчик?


В компьютерной области загрузчиком называется программа, выполняемая при каждой инициализации загрузочного устройства во время запуска и перезагрузки ПК. Технически это выполняемый машинный код, соответствующий архитектуре типа используемого в системе ЦПУ.

Какие есть виды микропроцессоров?


Я приведу основные:
16 битные
32 битные
64 битные

Чем больше значение бит, тем к большему объему памяти имеют доступ программы, получая большую производительность в плане временного хранилища, обработки и пр. На сегодня микропроцессоры производят две основные компании Intel и AMD. В этой же статьи я буду обращаться только к процессорам семейства Intel (x86).

В чем отличие процессоров Intel и AMD?


Каждый производитель использует свой уникальный способ проектирования микропроцессоров с аппаратной точки зрения и в плане используемых наборов инструкций.

Знакомство со средой разработки


Что такое реальный режим?


Я уже упоминал, что все процессоры с архитектурой x86 при загрузке с устройства запускаются в реальном режиме. Это очень важно иметь в виду при написании загрузочного кода для любого устройства. Реальный режим поддерживает только 16-битные инструкции. Поэтому создаваемый вами код для загрузки в загрузочную запись или сектор должен компилироваться в 16-битный формат. В реальном режиме инструкции могут работать только с 16 битами одновременно. Например, в 16-битном ЦПУ конкретная инструкция будет способна складывать в одном цикле два 16-битных числа. Если же для процесса будет необходимо сложить два 32-битных числа, то потребуется больше циклов, выполняющих сложение 16-битных чисел.

Что такое набор инструкций?


Это гетерогенная коллекция сущностей, ориентированных на конкретную архитектуру микропроцессора, с помощью которых пользователь может взаимодействовать с ним. Здесь я подразумеваю коллекцию сущностей, состоящую из внутренних типов данных, инструкций, регистров, режимов адресации, архитектуры памяти, обработки прерываний и исключений, а также внешнего I/O. Обычно для семейства микропроцессоров создаются общие наборы инструкций. Процессор Intel-8086 относится к семейству 8086, 80286, 80386, 80486, Pentium, Pentium I, II, III, которое также известно как семейство x86. В этой статье я будут использовать набор инструкций, относящийся именно к этому типу процессоров.

Как написать код для загрузочного сектора устройства?


Для реализации этой задачи необходимо иметь представление о:
Операционной системе (GNU Linux).
Ассемблере (GNU Assembler).
Наборе инструкций (x86).
Написании инструкций на GNU Assembler для x86 микропроцессоров.
Компиляторе (как вариант язык C).
Компоновщике (GNU linker ld)
Эмуляторе x86, например bochs, используемом для тестирования.

Что такое операционная система?


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

Отдельно отмечу, что все современные ОС работают в защищенном режиме.
Какие виды ОС бывают?
Windows
Linux
MAC


Что значит защищенный режим?


В отличие от реального режима, защищенный поддерживает 32-битные инструкции. Но вам об этом задумываться не стоит, так как нас не особо волнует процесс функционирования ОС.

Что такое Ассемблер?


Ассемблер преобразует передаваемые пользователем инструкции в машинный код.

Разве компилятор делает не то же самое?


На более высоком уровне да, но, фактически, внутри компилятора этим занимается именно ассемблер.

Почему компилятор не может генерировать машинный код напрямую?


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

Зачем нужна ОС для написания кода загрузочного сектора?


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

Какую ОС можно использовать?


Так как я писал загрузочные программы под Ubuntu, то и вам для ознакомления с данным руководством порекомендую именно эту ОС.

Какой следует использовать компилятор?


Я писал загрузчики при помощи GNU GCC и демонстрировать компиляцию кода я буду на нем же. Как протестировать рукописный код для загрузочного сектора? Я представлю вам эмулятор архитектуры x86, который помогает дорабатывать код, не требуя постоянной перезагрузки компьютера при редактировании загрузочного сектора устройства.

Знакомство с микропроцессором


Прежде чем изучать программирование микропроцессора, нам необходимо разобрать использование регистров.

Что такое регистры?


Регистры подобны утилитам микропроцессора, служащим для временного хранения данных и управления ими согласно нашим потребностям. Предположим, пользователь задает операцию сложения 2 и 3, для чего компьютер сохраняет число 3 в одном регистре, а 2 в другом, после чего складывает содержимое этих регистров. В итоге ЦПУ помещает результат в еще один регистр, который и представляет нужный пользователю вывод. Регистры разделяются на четыре основных типа:

регистры общего назначения;
сегментные регистры;
индексные регистры;
регистры стека.

Я дам краткое пояснение по каждому типу.

Регистры общего назначения: используются для хранения временных данных, необходимых программе в процессе выполнения. Каждый такой регистр имеет 16 бит в ширину и 2 байта в длину.
AX регистр сумматора;
BX регистр базового адреса;
CX регистр-счетчик;
DX регистр данных.

Сегментные регистры: служат для представления микропроцессору адреса памяти. Здесь нужно знать два термина:
Сегмент: независимый блок памяти, поддерживаемый аппаратно. Обычно обозначается начальным адресом.
Смещение: указывает индекс относительно начала сегмента.

Пример: у нас есть байт, представляющий значение X и расположенный в 10-й позиции от начала блока памяти со стартовым адресом 0x7c00. В данной ситуации мы выразим сегмент как 0x7c00, а смещение как 10.
Абсолютным адресом тогда будет 0x7c00 + 10.

Здесь я хочу выделить четыре категории:
CS сегмент кода;
SS сегмент стека;
DS сегмент данных;
ES расширенный сегмент.

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

movw $0x07c0, %axmovw %ax    , %dsmovw (0x0A) , %ax 


Здесь происходит:

загрузка значения 0x07c0 * 16 в AX;
загрузка содержимого AX в DS;
установка 0x7c00 + 0x0a в ax.

Регистры стека:
BP базовый указатель;
SP указатель стека.

Индексные регистры:
SI: регистр индекса источника.
DI: регистр индекса получателя.
AX: используется ЦПУ для арифметических операций.
BX: может содержать адрес процедуры или переменной (это также могут SI, DI и BP) и использоваться для выполнения арифметических операций и перемещения данных.
CX: выступает в роли счетчика цикла при повторении инструкций.
DX: содержит старшие 16 бит произведения при умножении, а также задействуется при делении.
CS: содержит базовый адрес всех выполняемых инструкций программы.
SS: содержит базовый адрес стека.
DS: содержит предустановленный адрес переменных.
ES: содержит дополнительный базовый адрес переменных памяти.
BP: содержит предполагаемое смещение из регистра SS. Часто используется подпрограммами для обнаружения переменных, переданных в стек вызывающей программой.
SP: содержит смещение вершины стека.
SI: используется в инструкциях перемещения строк. При этом на исходную строку указывает регистр SI.
DI: выступает в роли места назначения для инструкций перемещения строк.

Что такое бит?


В вычислительных средах бит является наименьшей единицей данных, представляющей их в двоичном формате, где 1 = да, а 0 = нет.

Дополнительно о регистрах:

Ниже описано дальнейшее подразделение регистров:
AX: первые 8 бит AX обозначаются как AL, последние 8 бит как AH.
BX: первые 8 бит BX обозначаются как BL, последние 8 как как BH.
CX: первые 8 бит CX обозначаются как CL, последние 8 бит как CH.
DX: первые 8 бит DX обозначаются как DL, последние 8 бит как DH.

Как обращаться к функциям BIOS?


BIOS предоставляет ряд функций, позволяющих распределять приоритеты ЦПУ. Доступ к этим возможностям BIOS можно получить с помощью прерываний.

Что такое прерывания?


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

Какое прерывание будем использовать мы?


Прерывание INT 0x10.

Написание кода на Ассемблере


Какие типы данных доступны в GNU Assembler?


Типы данных определяют их характеристики и могут быть следующими:

байт;
слово;
int;
ascii;
asciiz.

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

Int: целочисленный тип данных, состоящий из 32 бит, которые могут быть представлены четырьмя байтами или двумя словами.
Прим. Справедливости ради, стоит отметить, что размер Int зависит от архитектуры и может составлять от 16 до 64 бит (а на некоторых системах даже 8 бит). То, очем говорит автор это тип long. Подробнее о типах С можно прочесть по ссылке.

ascii: представляет группу байтов без нулевого символа.

asciiz: выражает группу байтов, завершающуюся нулевым символом.

Как генерировать код для реального режима в Ассемблере?


В процессе запуска ЦПУ в реальном режиме (16 бит) мы можем задействовать только встроенные функции BIOS. Я имею в виду, что с помощью этих функций можно написать собственный код загрузчика, поместить его в загрузочный сектор и выполнить загрузку. Давайте рассмотрим написание на Ассемблере небольшого фрагмента программы для генерации 16-битного кода ЦПУ через GNU Assembler.

Файл-образец: test.S  


.code16                   #генерирует 16-битный код.text                     #расположение исполняемого кода     .globl _start;_start:                   #точка входа     . = _start + 510     #перемещение из позиции 0 к 510-му байту      .byte 0x55           #добавление сигнатуры загрузки     .byte 0xaa           #добавление сигнатуры загрузки


Пояснения:

.code16: это директива, отдаваемая ассемблеру для генерации не 32-, а 16-битного кода. Зачем это нужно? Ассемблер вы будете использовать через операционную систему, а код загрузчика будете писать с помощью компилятора. Но вы также наверняка помните, что ОС работает в защищенном 32-битном режиме. Поэтому, по умолчанию ассемблер в такой ОС будет производить 32-битный код, что не соответствует нашей задаче. Данная же директива исправляет этот нюанс, и мы получаем 16-битный код.
.text: этот раздел содержит фактические машинные инструкции, составляющие вашу программу.
.globl _start: .global <символ> делает символ видимым для компоновщика. При определении символа в подпрограмме его значение становится доступным для других связанных подпрограмм. Иначе говоря, символ получает атрибуты от символа с таким же именем, находящегося в другом файле, связанном с этой программой.
_start: точка входа в основной код, а также предустановленная точка входа для компоновщика.
= _start + 510: переход от начальной позиции к 510-му байту.
.byte 0x55: первый байт, определяемый как часть сигнатуры загрузки (511-й байт).
.byte 0xaa: последний байт, определяемый как часть сигнатуры загрузки (512-й байт).

Как скомпилировать программу ассемблера?


Сохраните код в файле test.S и введите в командной строке:

as test.S -o test.o
ld Ttext 0x7c00 --oformat=binary test.o o test.bin

Что означают эти команды?


as test.S o test.o: преобразует заданный код в промежуточную объектную программу, которая затем преобразуется уже в машинный код.
--oformat=binary сообщает компоновщику, что выходной двоичный файл должен быть простым двоичным образом, т.е. не иметь кода запуска, связывания адресов и пр.
Ttext 0x7c00 сообщает компоновщику, что для вычисления абсолютного адреса нужно загрузить адрес text (сегмент кода) в 0x7c00.

Что такое сигнатура загрузки?


Давайте вспомним о загрузочном секторе, используемом BIOS для запуска системы, и подумаем, как BIOS узнает о наличии такого сектора на устройстве? Тут нужно пояснить, что состоит он из 512 байт, в которых для 510-го байта ожидается символ 0x55, а для 511-го символ 0xaa. Исходя из этого, BIOS проверяет соответствие двух последний байт загрузочного сектора этим значениям и либо продолжает загрузку, либо сообщает о ее невозможности. При помощи hex-редактора можно просматривать содержимое двоичного файла в более читабельном виде, и ниже в качестве примера я привел снимок этого файла.

Как скопировать исполняемый код на загрузочное устройство и протестировать его?


Чтобы создать образ для дискеты размером 1.4Мб, введите в командную строку следующее:
dd if=/dev/zero of=floppy.img bs=512 count=2880

Чтобы скопировать этот код в загрузочный сектор файла образа, введите:
dd if=test.bin of=floppy.img

Для тестирования программы введите:
bochs

Если bochs не установлен, тогда можно ввести следующее:
sudo apt-get install bochs-x

Файл-образец bochsrc.txt

megs: 32#romimage: file=/usr/local/bochs/1.4.1/BIOS-bochs-latest, address=0xf0000#vgaromimage: /usr/local/bochs/1.4.1/VGABIOS-elpin-2.40floppya: 1_44=floppy.img, status=insertedboot: alog: bochsout.txtmouse: enabled=0 


В результате должно отобразиться стандартное окно эмуляции bochs:



Просмотр:
Если теперь заглянуть в файл test.bin через hex-редактор, то вы увидите, что сигнатура загрузки находится после 510-го байта:



Пока что вы просто увидите сообщение Booting from Floppy, так как в коде мы еще ничего не прописали. Давайте рассмотрим пару других примеров создания кода на ассемблере.

Файл-образец: <i>test2.S</i> 

.code16                    #генерирует 16-битный код.text                      #расположение исполняемого кода     .globl _start;_start:                    #точка входа     movb $'X' , %al       #выводимый символ     movb $0x0e, %ah       #выводимый служебный код bios     int  $0x10            #прерывание цпу     . = _start + 510      #перемещение из позиции 0 к 510-му байту     .byte 0x55            #добавление сигнатуры загрузки     .byte 0xaa            #добавление сигнатуры загрузки


После ввода этого кода сохраните его в test2.S и выполните действия согласно прежней инструкции, изменив имя исходного файла. После компиляции, копирования кода в загрузочный сектор и выполнения bochs вы должны увидеть следующий экран, где теперь отображается прописанная нами в коде буква X.



Поздравляю, ваша первая программа в загрузочном секторе работает!

Просмотр:
В hex-редакторе вы увидите, что символ X находится во второй позиции от начального адреса.



Теперь давайте выведем на экран текст побуквенно.

Образец: test3.S

.code16                  #генерирует 16-битный код.text                    #расположение исполняемого кода     .globl _start;_start:                  #точка входа     #выводит 'H'      movb $'H' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'e'     movb $'e' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'l'     movb $'l' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'l'     movb $'l' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'o'      movb $'o' , %al     movb $0x0e, %ah     int  $0x10     #выводит ','     movb $',' , %al     movb $0x0e, %ah     int  $0x10     #выводит ' '     movb $' ' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'W'     movb $'W' , %al     movb $0x0e, %ah     int  $0x10     #выводит'o'     movb $'o' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'r'     movb $'r' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'l'     movb $'l' , %al     movb $0x0e, %ah     int  $0x10     #выводит 'd'     movb $'d' , %al     movb $0x0e, %ah     int  $0x10     . = _start + 510    #перемещение из позиции 0 к 510-му байту     .byte 0x55            #добавление сигнатуры загрузки     .byte 0xaa            #добавление сигнатуры загрузки


Сохраните файл как test3.S. После компиляции и всех сопутствующих действий перед вами отобразится следующий экран:



Просмотр:



Хорошо. Теперь давайте напишем программу, выводящую на экран фразу Hello, World.
При этом мы также определим функции и макросы, с помощью которых и будем выводить эту строку.

Файл-образец: test4.S

#генерирует 16-битный код.code16#расположение исполняемого кода.text.globl _start;#точка входа загрузочного кода_start:      jmp _boot                           #переход к загрузочному коду      welcome: .asciz "Hello, World\n\r"  #здесь мы определяем строку     .macro mWriteString str              #макрос, вызывающий функцию вывода строки          leaw  \str, %si          call .writeStringIn     .endm     #функция вывода строки     .writeStringIn:          lodsb          orb  %al, %al          jz   .writeStringOut          movb $0x0e, %ah          int  $0x10          jmp  .writeStringIn     .writeStringOut:     ret_boot:     mWriteString welcome     #перемещение от начала к 510-му байту и присоединение сигнатуры загрузки     . = _start + 510     .byte 0x55     .byte 0xaa  


Сохраните файл как test4.S. Теперь после компиляции и всего за ней следующего вы увидите:



Отлично! Если вы поняли все проделанные мной действия и успешно создали аналогичную программу, то я вас поздравляю еще раз!

Просмотр:



Что такое функция?


Функция это блок кода, имеющий имя и переиспользуемое свойство.

Что такое макрос?


Макрос это фрагмент кода с присвоенным именем, на место использования которого подставляется содержимое этого макроса.

В чем синтаксическое отличие функции от макроса?


Для вызова функции используется следующий синтаксис:

push call А для макроса такой:

macroname И поскольку синтаксис вызова и применения макроса проще, чем функции, я предпочел использовать в основном коде именно его.

Написание кода в компиляторе С



Что такое C?


С это язык программирования общего назначения, разработанный сотрудником Bell Labs Деннисом Ритчи в 1969-1973 годах.

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

Что нужно для написания кода на С?


Мы будем использовать компилятор GNU C под названием GCC и разберем написание в нем программы на примере.

Файл-образец: test.c

__asm__(".code16\n");__asm__("jmpl $0x0000, $main\n");void main() {} 


File: test.ld

ENTRY(main);SECTIONS{    . = 0x7C00;    .text : AT(0x7C00)    {        *(.text);    }    .sig : AT(0x7DFE)    {        SHORT(0xaa55);    }} 


Для компиляции программы введите в командной строке:

gcc -c -g -Os -march=i686 -ffreestanding -Wall -Werror test.c -o test.o
ld -static -Ttest.ld -nostdlib --nmagic -o test.elf test.o
objcopy -O binary test.elf test.bin

Что значат эти команды?


Первая преобразует код C в промежуточную объектную программу, которая в последствии преобразуется в машинный код.
gcc -c -g -Os -march=i686 -ffreestanding -Wall -Werror test.c -o test.o:

Что значат эти команды?


-c: используется для компиляции исходного кода без линковки.
-g: генерирует отладочную информацию для отладчика GDB.
-Os: оптимизация размера кода.
-march: генерирует код для конкретной архитектуры ЦПУ (в нашем случае i686).
-ffreestanding: в среде отдельных программ может отсутствовать стандартная библиотека, а инструкции запуска программы не обязательно располагаются в main.
-Wall: активирует все предупреждающие сообщения компилятора. Рекомендуется всегда использовать эту опцию.
-Werror: активирует трактовку предупреждений как ошибок.
test.c: имя входного исходного файла.
-o: генерация объектного кода.
test.o: имя выходного файла объектного кода.

С помощью всей этой комбинации флагов мы генерируем объектный код, помогающий нам в обнаружении ошибок и предупреждений, а также создаем более эффективный код для данного типа ЦПУ. Если не указать march=i686, будет сгенерирован код для используемой вами машины. В связи с этим нужно указывать, для какого именно типа ЦПУ он создается.

ld -static -Ttest.ld -nostdlib --nmagic test.elf -o test.o:
Эта команда вызывает компоновщик из командной строки, и ниже я поясню, как именно мы его используем.

Что значат эти флаги?


-static: не линковать с общими библиотеками.
-Ttest.ld: разрешить компоновщику следовать командам из его скрипта.
-nostdlib: разрешить компоновщику генерировать код, не линкуя функции запуска стандартной библиотеки C.
--nmagic: разрешить компоновщику генерировать код без фрагментов _start_SECTION и _stop_SECTION.
test.elf: имя входного файла (соответствующий платформе формат хранения исполняемых файлов. Windows: PE, Linux: ELF)
-o: генерация объектного кода.
test.o: имя выходного файла объектного кода.

Что такое компоновщик?


Он выполняет последний этап компиляции. ld (компоновщик) получает один или более объектных файлов либо библиотек и совмещает их в один, как правило, исполняемый файл. В ходе этого процесса он обрабатывает ссылки на внешние символы, присваивает конечные адреса процедурам/функциям и переменным, а также корректирует код и данные для отражения актуальных адресов.
Не забывайте, что мы не используем в коде стандартные библиотеки и сложные функции.

objcopy -O binary test.elf test.bin
Эта команда служит для генерации независимого от платформы кода. Обратите внимание, что в Linux исполняемые файлы хранятся не так, как в Windows. В каждой системе свой способ хранения, но мы создаем всего-навсего небольшой загрузочный код, который на данный момент не зависит от ОС.

Зачем в программе C использовать инструкции ассемблера?


В реальном режиме к функциям BIOS можно легко обратиться через прерывания при помощи именно инструкций ассемблера.

Как скопировать код на загрузочное устройство и проверить его?


Чтобы создать образ для дискеты размером 1.4Мб, введите в командную строку:

dd if=/dev/zero of=floppy.img bs=512 count=2880
Чтобы скопировать код в загрузочный сектор файла образа, введите:
dd if=test.bin of=floppy.img
Для проверки программы введите:
bochs

Должно отобразиться стандартное окно эмуляции:



Что мы видим: как и в первом нашем примере, пока что здесь отображается только сообщение Booting from Floppy.

Для вложения инструкция ассемблера в программу C мы используем ключевое слово __asm__.
Дополнительно мы задействуем __volatile__, указывая компилятору, что код нужно оставить как есть, без изменений.

Такой способ вложения называется встраивание ассемблерного кода.
Рассмотрим еще несколько примеров написания с помощью компилятора.

Пишем программу для вывода на экран X


Файл-образец: test2.c

__asm__(".code16\n");__asm__("jmpl $0x0000, $main\n");void main() {     __asm__ __volatile__ ("movb $'X'  , %al\n");     __asm__ __volatile__ ("movb $0x0e, %ah\n");     __asm__ __volatile__ ("int $0x10\n");}


Написав код, сохраните файл как test2.c и скомпилируйте его согласно все тем же инструкциям, изменив исходное имя. После компиляции, копирования кода в загрузочный сектор и выполнения команды bochs вы снова увидите экран, где отображается буква X:



Теперь напишем код для показа фразы Hello, World


Для вывода данной строки мы также определим функции и макросы.

Файл-образец: test3.c

/* генерирует 16-битный код */__asm__(".code16\n");/* переходит к точке входа загрузочного кода */__asm__("jmpl $0x0000, $main\n");void main() {     /* выводит 'H' */     __asm__ __volatile__("movb $'H' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'e' */     __asm__ __volatile__("movb $'e' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'l' */     __asm__ __volatile__("movb $'l' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'l' */     __asm__ __volatile__("movb $'l' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'o' */     __asm__ __volatile__("movb $'o' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит ',' */     __asm__ __volatile__("movb $',' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит ' ' */     __asm__ __volatile__("movb $' ' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'W' */     __asm__ __volatile__("movb $'W' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'o' */     __asm__ __volatile__("movb $'o' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'r' */     __asm__ __volatile__("movb $'r' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'l' */     __asm__ __volatile__("movb $'l' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");     /* выводит 'd' */     __asm__ __volatile__("movb $'d' , %al\n");     __asm__ __volatile__("movb $0x0e, %ah\n");     __asm__ __volatile__("int  $0x10\n");}


Сохраните этот код в файле test3.c и следуйте уже знакомым вам инструкциям, изменив имя исходного файла и скопировав скомпилированный код в загрузочный сектор дискеты. Теперь на этапе проверки должна отобразиться надпись Hello, World:



Напишем программу C для вывода строки Hello, World


При этом мы определим функцию, выводящую эту строку на экран.

Файл-образец: test4.c

/*генерирует 16-битный код*/__asm__(".code16\n");/*переход к точке входа в загрузочный код*/__asm__("jmpl $0x0000, $main\n");/* пользовательская функция для вывода серии знаков, завершаемых нулевым символом*/void printString(const char* pStr) {     while(*pStr) {          __asm__ __volatile__ (               "int $0x10" : : "a"(0x0e00 | *pStr), "b"(0x0007)          );          ++pStr;     }}void main() {     /* вызов функции <i>printString </i>со строкой в качестве аргумента*/     printString("Hello, World");} 


Сохраните этот код в файле test4.c и снова проследуйте всем инструкциям компиляции и загрузки, в результате чего на экране должно отобразиться следующее:



Все это время мы учились путем преобразования программ ассемблера в программы C. К настоящему моменту вы должны уже хорошо уяснить процесс их написания на обоих этих языках, а также уметь выполнять компиляцию и проверку.

Далее мы перейдем к написанию циклов и их использованию в функциях, а также познакомимся с другими службами BIOS.

Мини-проект отображения прямоугольников


Файл-образец: test5.c

/* генерирация 16-битного кода                                                 */__asm__(".code16\n");/* переход к главной функции или программному коду                                */__asm__("jmpl $0x0000, $main\n");#define MAX_COLS     320 /* количество столбцов экрана               */#define MAX_ROWS     200 /* количество строк экрана                  *//* функция вывода строки*//* input ah = 0x0e*//* input al = <выводимый символ>*//* прерывание: 0x10*//* мы используем прерывание 0x10 с кодом функции 0x0e для вывода байта из al*//* эта функция получает в качестве аргумента строку и выводит символ за символом, пока не достигнет нуля*/void printString(const char* pStr) {     while(*pStr) {          __asm__ __volatile__ (               "int $0x10" : : "a"(0x0e00 | *pStr), "b"(0x0007)          );          ++pStr;     }}/* функция, получающая сигнал о нажатии клавиши на клавиатуре *//* input ah = 0x00*//* input al = 0x00*//* прерывание: 0x10*//* эта функция регистрирует нажатие пользователем клавиши для продолжения выполнения */void getch() {     __asm__ __volatile__ (          "xorw %ax, %ax\n"          "int $0x16\n"     );}/* функция вывода на экран цветного пикселя в заданном столбце и строке *//* входной ah = 0x0c*//* входной al = нужный цвет*//* входной cx = столбец*//* входной dx = строка*//* прерывание: 0x10*/void drawPixel(unsigned char color, int col, int row) {     __asm__ __volatile__ (          "int $0x10" : : "a"(0x0c00 | color), "c"(col), "d"(row)     );}/* функции очистки экрана и установки видео-режима 320x200 пикселей*//* функция для очистки экрана *//* входной ah = 0x00 *//* входной al = 0x03 *//* прерывание = 0x10 *//* функция для установки видео режима *//* входной ah = 0x00 *//* входной al = 0x13 *//* прерывание = 0x10 */void initEnvironment() {     /* очистка экрана */     __asm__ __volatile__ (          "int $0x10" : : "a"(0x03)     );     __asm__ __volatile__ (          "int $0x10" : : "a"(0x0013)     );}/* функция вывода прямоугольников в порядке уменьшения их размера *//* я выбрал следующую последовательность отрисовки: *//* из левого верхнего угла в левый нижний, затем в правый нижний, оттуда в верхний правый и в завершении в верхний левый край */void initGraphics() {     int i = 0, j = 0;     int m = 0;     int cnt1 = 0, cnt2 =0;     unsigned char color = 10;     for(;;) {          if(m < (MAX_ROWS - m)) {               ++cnt1;          }          if(m < (MAX_COLS - m - 3)) {               ++cnt2;          }          if(cnt1 != cnt2) {               cnt1  = 0;               cnt2  = 0;               m     = 0;               if(++color > 255) color= 0;          }          /* верхний левый -> левый нижний */          j = 0;          for(i = m; i < MAX_ROWS - m; ++i) {               drawPixel(color, j+m, i);          }          /* левый нижний -> правый нижний */          for(j = m; j < MAX_COLS - m; ++j) {               drawPixel(color, j, i);          }          /* правый нижний -> правый верхний */          for(i = MAX_ROWS - m - 1 ; i >= m; --i) {               drawPixel(color, MAX_COLS - m - 1, i);          }          /* правый верхний -> левый верхний */          for(j = MAX_COLS - m - 1; j >= m; --j) {               drawPixel(color, j, m);          }          m += 6;          if(++color > 255)  color = 0;     }}/* эта функция является загрузочным кодом и вызывает следующие функции: *//* вывод на экран сообщения, предлагающего пользователю нажать любую клавишу для продолжения. После нажатия клавиши происходит отрисовка прямоугольников в порядке убывания их размера */void main() {     printString("Now in bootloader...hit a key to continue\n\r");     getch();     initEnvironment();     initGraphics();}


Сохраните все это в файле test5.c и следуйте все тем же инструкциям компиляции с последующим копированием кода в загрузочный сектор дискеты.

Теперь в качестве результата вы увидите:



Нажмите любую клавишу.






Просмотр:
Если внимательно рассмотреть содержимое исполняемого файла, то можно заметить, что в нем практически закончилось свободное пространство. Поскольку размер загрузочного сектора ограничен 512Кб, мы смогли вместить только несколько функций, а именно выполнить инициализацию среды и вывод цветных прямоугольников. Вот снимок содержимого файла:



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

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

Подробнее..

Перевод Пишем загрузчик на Ассемблере и С. Часть 2

09.01.2021 16:11:17 | Автор: admin


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

Здесь я ограничусь написанием программы на ассемблере и ее копированием в загрузочный сектор образа дискеты 3.5, после чего мы, как и в прошлой статье, протестируем записанный загрузочный код при помощи эмулятора bochs. Для реализации этих задач я задействую службы BIOS, что позволит нам лучше понять их функционирование и более уверенно работать в реальном режиме (Real Mode).

План статьи


Знакомство с сегментацией
Среда программирования
Чтение данных из RAM
Знакомство с устройствами хранения
Структура флоппи-диска
Взаимодействие с флоппи-диском

Знакомство с сегментацией


Прежде чем переходить к рассмотрению чтения флоппи-диска, давайте освежитм в памяти тему сегментации и ее назначения.

Что это такое?


Основная память разделена на сегменты, индексируемые специальными сегментными регистрами CS, DS, SS и ES.

Назначение сегментации


Когда мы указываем 16-битный адрес, ЦПУ автоматически вычисляет начальный адрес соответствующего сегмента. Тем не менее именно программист должен указывать начальный адрес каждого сегмента, особенно при написании такой программы, как загрузчик.

Какие бывают типы сегментов?


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

сегмент кода;
сегмент данных;
сегмент стека;
расширенный сегмент.

Сегмент кода
Один из разделов программы в памяти, содержащий исполняемые инструкции. Если вы загляните в мою предыдущую статью, то увидите метку .text, под которой мы размещаем исполняемые инструкции. При загрузке программы в память эти инструкции передаются в сегмент кода. В ЦПУ для обращения к этому сегменту мы используем регистр CS.

Сегмент данных
Раздел программы в памяти, содержащий статические и глобальные переменные. Для обращения к нему мы используем регистр DS.

Сегмент стека
Программист может использовать регистры для хранения, изменения и извлечения данных при написании программы. При этом ограниченное количество регистров зачастую приводит к усложнению логики. В итоге у разработчика постоянно возникает потребность в дополнительном пространстве, более гибком в плане хранения, обработки и извлечения данных. С целью решения подобных сложностей в ЦПУ был внедрен специальный сегмент стека. Для хранения в этом сегменте данных и их извлечения используются соответствующие инструкции push (добавление) и pop (удаление). Инструкцию push мы также используем для передачи аргументов в функции. Обращение к сегменту стека производится при помощи регистра SS. При этом важно помнить, что стек растет сверху вниз.

Расширенный сегмент
Обычно этот сегмент используется для загрузки данных, имеющих слишком большой объем для хранения в сегменте данных. Позже вы увидите, как я загружаю данные с дискеты в расширенный сегмент. Для обращения же к нему мы используем регистр ES.

Применение сегментных регистров


Мы не можем устанавливать эти регистры напрямую и вместо этого делаем следующее:

movw $0x07c0, %axmovw %ax, %ds 


Что здесь происходит?


Копирование данных в регистр общего назначения.
Их перенос в сегментный регистр.

Мы загружаем в регистр AX значение из 0x07c0, после чего копируем содержимое AX в DS. Абсолютный же адрес вычисляется так:

DS = 16 * AXDS = 0x7c00

Далее для перемещения и достижения нужной точки сегмента данных мы используем смещение.

Среда программирования


Операционная система (GNU Linux)
Ассемблер (GNU Assembler)
Компилятор (GNU GCC)
Компоновщик (GNU linker ld)
Эмулятор архитектуры x86 для тестирования (bochs).

Чтение данных из RAM


Теперь BIOS загружает нашу программу из 0x7c00, и та, в свою очередь, начинает поочередно выводить значения. Для обращения к данным в RAM мы устанавливаем сегмент данных со значением 0x7c00, также указывая смещение.

Пример


После загрузки программы из 0x7c00 можно прочесть данные из смещения 3 и 4 и вывести их на экран.

Программа: test.S

.code16                   # генерирует 16-битный код.text                     # расположение исполняемого кода     .globl _start;_start:                   # точка входа     jmp  _boot           # переход к загрузочному коду     data : .byte 'X'     # переменная     data1: .byte 'Z'     # переменная_boot:     movw $0x07c0, %ax    # установка ax = 0x07c0     movw %ax    , %ds    # установка ds = 16 * 0x07c0 = 0x7c00     # копируем данные в позиции 3 из 0x7c00:0x0000     #и выводим их на экран     movb 0x02   , %al    # копирование данных из 2-й позиции в %al     movb $0x0e  , %ah     int  $0x10    # копируем данные в позиции 4 из 0x7c00:0x0000    #и выводим их на экран     movb 0x03   , %al    # копирование данных из 3-й позиции в %al     movb $0x0e  , %ah     int  $0x10#бесконечный цикл_freeze:     jmp _freeze     . = _start + 510   #переход из позиции 0 к 510-му байту     .byte 0x55           #добавление сигнатуры загрузки     .byte 0xaa           #добавление сигнатуры загрузки

Теперь для генерации двоичного файла и копирования кода в загрузочный сектор дискеты в командной строке введите:

as test.S o test.o
ld Ttext=0x7c00 oformat=binary boot.o o boot.bin
dd if=/dev/zero of=floppy.img bs=512 count=2880
dd if=boot.bin of=floppy.img

Если открыть файл boot.bin в hex-редакторе, вы увидите такое окно:



Здесь X и Z находятся в третьей и четвертой позиции от начала 0x7c00.
Для проверки этого кода введите:

bochs



Пример 2


После того, как BIOS загрузит нашу программу из 0x7c00, мы прочитаем и выведем завершающуюся нулем строку из смещения 2.

Программа: test2.S

.code16                                     # генерирует 16-битный код.text                                       # расположение исполняемого кода     .globl _start;_start:                                     # точка входа     jmp  _boot                             # переход к загрузочному коду     data : .asciz "This is boot loader"    # переменная     #вызывает функцию printString, которая      #начинает вывод строки с этой позиции     .macro mprintString start_pos          # макрос вывода строки          pushw %si          movw  \start_pos, %si          call  printString          popw  %si     .endm      printString:                           # функция вывода строки     printStringIn:          lodsb          orb %al   , %al          jz  printStringOut          movb $0x0e, %ah          int  $0x10          jmp  printStringIn     printStringOut:     ret_boot:     movw $0x07c0, %ax                      # установка значения в ax = 0x07c0     movw %ax    , %ds                      # установка значения в ds = 16 * 0x07c0 = 0x7c00     mprintString $0x02_freeze:     jmp _freeze     . = _start + 510                       # перемещение из 0 позиции к 510-му байту      .byte 0x55                             # добавление сигнатуры загрузки     .byte 0xaa                             # добавление сигнатуры загрузки 

Если вы скомпилируете программу и откроете исполняемый файл в эмуляторе, то в качестве вывода увидите строку This is boot loader.



Знакомство с устройствами хранения


Что это такое?


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

Какие виды устройств хранения бывают?


За время эволюции информационных технологий было разработано множество их видов, включая:

Магнитные ленты;
Флоппи-диски;
CD и DVD-диски;
Жесткие диски;
USB-носители;


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

Что такое флоппи-диск?





Так выглядит старая-добрая дискета. Объем этого носителя невелик и составляет всего 1.4 мегабайта, чего для нашей задачи будет вполне достаточно. Дальше я кратко расскажу о способе измерения данных в вычислительных системах.

Что такое мегабайт?


В компьютерах для измерения данных используются следующие величины:

Бит: может хранить логическое значение 0 или 1.
Полубайт: 4 бита
Байт: 8 бит
Килобайт (Кб): 1024 байта
Мегабайт (Мб): 1 Кб * 1 Кб = 1,048,576 Байт = 1024 Кб = 1024 * 1024 байт
Гигабайт (ГБ): 1,073,741,824 Байт= 2^30 Байт = 1024 Мб = 1,048,576 Кб = 1024 * 1024 * 1024 байт
Терабайт (ТБ): 1,099,511,627,776 Байт= 2^40 Байт = 1024 ГБ = 1,048,576 Мб = 1024 * 1024 * 1024 * 1024 байт

Существуют и более крупные величины, но мы ограничимся этими.

Структура типичного флоппи-диска





Это общее схематичное строение дискеты, а вот характеристики используемой нами дискеты 3.5":

двухсторонняя;
стороны обозначаются согласно считывающим их магнитным головкам (head0, head1);
каждая сторона содержит 80 дорожек (Track);
каждая дорожка разбита на 18 секторов (Sector);
размер каждого сектора 512 байт.

Как вычислить размер дискеты?


Общий размер в байтах: кол-во сторон * кол-во дорожек * кол-во секторов в дорожке * байт в секторе.
Пример = 2 * 80 * 18 * 512 = 1474560 байт.

Общий размер в Кб: (кол-во сторон * кол-во дорожек * кол-во секторов в дорожке * байт в секторе)/1024.
Пример = (2 * 80 * 18 * 512)/1024 = 1474560/1024 = 1440Кб.

Общий размер в Мб: ((кол-во сторон * кол-во дорожек * кол-во секторов в дорожке * байт в секторе)/1024)/1024
Пример = ((2 * 80 * 18 * 512)/1024)/1024 = (1474560/1024)/1024 = 1440/1024 = 1.4Мб

Где на дискете находится загрузочный сектор?


Загрузочным является первый сектор диска.

Взаимодействие с флоппи-диском


Как считывать с него данные?


Поскольку в нашем случае потребуется считывать дискету в процессе загрузки в реальном режиме, для этого нам будут доступны только службы BIOS.

Какое прерывание мы будем использовать?


Interrupt 0x13
Service code 0x02


Как обратиться к диску с помощью прерывания 0x13?


Команда BIOS для считывания сектора:
AH = 0x02

Команда BIOS для считывания N-го цилиндра:
CH = N

Команда BIOS для считывания N-ой головки (стороны):
DH = N

Команда BIOS для считывания N-го сектора:
CL = N

Команда BIOS для считывания N секторов:
AL = N

Команда прерывания:
Int 0x13


Считывание данных с флоппи-диска


Давайте напишем программу для отображения меток нескольких секторов.

Программа: test.S

.code16                       # генерирует 16-битный код.text                         # расположение исполняемого кода.globl _start;                # точка входа_start:     jmp _boot                # переход к загрузочному коду     msgFail: .asciz "something has gone wrong..." # сообщение об ошибке операции      # макрос вывода строки с завершающим нулем      # этот макрос вызывает функцию PrintString     .macro mPrintString str          leaw \str, %si          call PrintString     .endm     # функция вывода строки с завершающим нулем     PrintString:          lodsb          orb  %al  , %al          jz   PrintStringOut          movb $0x0e, %ah          int  $0x10          jmp  PrintString     PrintStringOut:     ret     # макрос считывания сектора дискеты     #и его загрузки в расширенный сегмент     .macro mReadSectorFromFloppy num          movb $0x02, %ah     # функция чтения диска          movb $0x01, %al     # всего секторов для считывания          movb $0x00, %ch     # выбор нулевого цилиндра          movb $0x00, %dh     # выбор нулевой головки          movb \num, %cl      # начало чтения сектора          movb $0x00, %dl     # <b>???номер диска????</b>          int  $0x13          # прерывание ЦПУ          jc   _failure       # при сбое выбросить ошибку          cmpb $0x01, %al     # если общее число считываемых секторов != 1,          jne  _failure       #то выбросить ошибку     .endm     # отображение строки, внедренной в качестве идентификатора сектора     DisplayData:     DisplayDataIn:          movb %es:(%bx), %al          orb  %al      , %al          jz   DisplayDataOut          movb $0x0e    , %ah          int  $0x10          incw %bx          jmp  DisplayDataIn     DisplayDataOut:     ret_boot:     movw  $0x07c0, %ax       # инициализация сегмента данных      movw  %ax    , %ds       #с адресом 0x7c00     movw  $0x9000, %ax       # ax = 0x9000     movw  %ax    , %es       # es = 0x9000 = ax     xorw  %bx    , %bx       # bx = 0     mReadSectorFromFloppy $2 # чтение сектора дискеты     call DisplayData         # отображение метки сектора     mReadSectorFromFloppy $3 # чтение 3-го сектора дискеты     call DisplayData         # отображение метки сектора_freeze:                      # бесконечный цикл     jmp _freeze              _failure:                          mPrintString msgFail     # вывод сообщения об ошибке и      jmp _freeze              #переход к точке остановки     . = _start + 510         # перемещение из 0 позиции к 510-му байту     .byte 0x55               # добавление первой части сигнатуры загрузки     .byte 0xAA               # добавление второй части сигнатуры загрузки_sector2:                     # второй сектор дискеты     .asciz "Sector: 2\n\r"   # запись данных в начало сектора     . = _sector2 + 512       # перемещение в конец второго сектора_sector3:                     # третий сектор дискеты     .asciz "Sector: 3\n\r"   # запись данный в начало сектора     . = _sector3 + 512       # перемещение в конец третьего сектора


Компиляция кода


as test.S -o test.o
ld -Ttext=0x0000 --oformat=binary test.o -o test.bin
dd if=test.bin of=floppy.img

Если вы откроете test.bin в hex-редакторе, то увидите, что я вложил метку в сектора 2 и 3, которые выделены на снимке ниже:



После запуска программы через эмулятор bochs отобразится следующее:



Что делает эта программа?


В ней мы определяем макрос и функции для чтения и отображения содержимого вложенных в каждый сектор строк.

Я вкратце поясню эти макросы и функции.

# макрос вывода строки с завершающим нулем# этот макрос вызывает функцию PrintString.macro mPrintString str  leaw \str, %si  call PrintString.endm

Этот макрос получает в качестве аргумента строку и внутренне вызывает функцию PrintString, отвечающую за поочередное отображение символов на экране.

# функция вывода строки с завершающим нулемPrintString:  lodsb  orb  %al  , %al  jz   PrintStringOut  movb $0x0e, %ah  int  $0x10  jmp  PrintStringPrintStringOut:Ret

Эту функцию вызывает макрос mPrintString для отображения каждого байта строки, заверщшающейся нулем.

# макрос для считывания сектора дискеты #и его загрузки в расширенный сегмент .macro mReadSectorFromFloppy num      movb $0x02, %ah     # функция чтения диска      movb $0x01, %al     # всего считываемых секторов      movb $0x00, %ch     # выбор нулевого цилиндра      movb $0x00, %dh     # выбор нулевой головки      movb \num, %cl      # начало чтения сектора      movb $0x00, %dl     # номер дисковода      int  $0x13          # прерывание ЦПУ      jc   _failure       # при сбое выбросить ошибку      cmpb $0x01, %al     # если общее число считываемых секторов != 1,      jne  _failure       #выбросить ошибку .endm

Этот макрос mReadSectorFromFloppy считывает сектор и помещает его в расширенный сегмент для дальнейшей обработки. Номер сектора он получает в качестве аргумента.

# отображение строки, вставленной в качестве идентификатора сектораDisplayData:DisplayDataIn:  movb %es:(%bx), %al  orb  %al      , %al  jz   DisplayDataOut  movb $0x0e    , %ah  int  $0x10  incw %bx  jmp  DisplayDataInDisplayDataOut:Ret

Эта функция отображает каждый байт данных от начала и до завершающего символа.

_boot:     movw  $0x07c0, %ax       # инициализируем сегмент данных     movw  %ax    , %ds       #с адресом 0x7c00     movw  $0x9000, %ax       # ax = 0x9000     movw  %ax    , %es       # es = 0x9000 = ax     xorw  %bx    , %bx       # bx = 0

Это основной загрузочный код. Прежде чем начать вывод содержимого диска, мы устанавливаем в сегменте данных значения из 0x7x00, а в расширенном сегменте из 0x9000.

Зачем определять расширенный сегмент?


Причина в том, что для отображения содержимого сектора сначала мы считываем его в адрес памяти 0x9000.

mReadSectorFromFloppy $2 # чтение сектора дискетыcall DisplayData         # отображение метки сектораmReadSectorFromFloppy $3 # чтение 3-го сектора дискетыcall DisplayData         # отображение метки сектора

Мы вызываем макрос для считывания 2-го сектора, а затем отображаем его содержимое, после чего снова вызываем макрос для считывания 3-го сектора, также отображая его содержимое.

_freeze:                      # бесконечный циклjmp _freeze   

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

_failure:                     mPrintString msgFail     # вывод сообщения об ошибке иjmp _freeze              # переход к точке остановки

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

. = _start + 510         # перемещение от позиции 0 к 510-му байту.byte 0x55               # добавление первой части сигнатуры загрузки.byte 0xAA               # добавление второй части сигнатуры загрузки

Мы переходим к 510-му байту сектора и добавляем сигнатуру загрузки, необходимую для определения дискеты как загрузочной. В противном случае система выбросит ошибку, сообщив о невозможности загрузки.

_sector2:                     # второй сектор дискеты     .asciz "Sector: 2\n\r"   # запись данных в начало сектора     . = _sector2 + 512       # перемещение в конец второго сектора_sector3:                     # третий сектор дискеты     .asciz "Sector: 3\n\r"   # запись данных в начало сектора     . = _sector3 + 512       # перемещение в конец третьего сектора

Здесь мы добавляем строку в начало 2-го и 3-го сектора.

На этом вторая часть серии завершается. Предлагаю самостоятельно поэкспериментировать с чтением дискеты в реальном режиме и внедрением в загрузчик различной функциональности.

В третьей части серии я коротко расскажу о файловых системах и их важности. В качестве практики мы напишем простейший загрузчик для считывания дискеты в формате FAT12, научимся выполнять чтение/запись такой дискеты, а также создадим загрузчик второй стадии и разберем его назначение.

Подробнее..

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

10.05.2021 18:20:09 | Автор: admin

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

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

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

Введение

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

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

Самым первым получает управление Boot ROM. Онявляется неизменяемым компонентом системы, прошивается назаводе-изготовителе ибольше неменяется. Его невозможно обновить (вотличие отBIOS иUEFI). Следовательно, нет смысла проверять его подлинность. Поэтому онимеет соответствующий статус: Аппаратный корень доверия (Hardware root oftrust). Впамять 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 onaCrystal 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). Конкретное значение вектора сброса определяется микроархитектурой процессора итеоретически может различаться среди разных поколений процессоров, новнашем случае это адрес 0100000000. Нааппаратном уровне определенные диапазоны адресов закреплены зафизическими устройствами хранения исоставляют вместе физическое адресное пространство (непутать свиртуальным адресным пространством, которое доступно изоперационной системы). Впроцессе дальнейшего запуска устройства диапазон адресов может быть переназначен впроизвольном порядке для более эффективной работы спамятью.

Следует заметить, что современные процессоры имеют несколько ядер, каждое изкоторых может независимо исполнять инструкции. Чтобы избежать неопределенности, какое изядер должно начать выполнение первой инструкции, производитель нааппаратном уровне выделяет основное ядро (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 CApublic 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 года

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru