Написание совместимых с Easyboot ядер
=====================================

[Easyboot](https://gitlab.com/bztsrc/easyboot) поддерживает различные ядра с помощью [плагинов](plugins.md). Но если подходящий
плагин не найден, он возвращается к двоичным файлам ELF64 или PE32+ с упрощенным (не нужно ничего встраивать) вариантом протокола
Multiboot2.

Это тот же протокол, который использует [Simpleboot](https://gitlab.com/bztsrc/simpleboot), все примеры ядер в этом репозитории
также должны работать с **Easyboot**.

Вы можете использовать исходный заголовок multiboot2.h в репозитории GRUB или файл заголовка [easyboot.h](../../easyboot.h) C/C++,
чтобы упростить работу. используйте определения типов. Бинарный формат низкого уровня тот же, вы также можете использовать любые
существующие библиотеки Multiboot2, даже с языками, отличными от C, например [Rust](https://github.com/rust-osdev/multiboot2/tree/main/multiboot2/src)
(примечание: я никоим образом не связан с этими разработчиками, я просто искал «Rust Multiboot2», и это был первый результат).

[[_TOC_]]

Последовательность загрузки
---------------------------

### Запуск загрузчика

На машинах с *BIOS* самый первый сектор диска загружается прошивкой по адресу 0:0x7C00, и управление передается ему. В этом секторе
**Easyboot** имеет [boot_x86.asm](../../src/boot_x86.asm), который достаточно умен, чтобы найти и загрузить загрузчик 2-го этапа, а
также настроить для него длинный режим. .

На машинах *UEFI* тот же самый файл второго этапа, называемый EFI/BOOT/BOOTX64.EFI, загружается непосредственно прошивкой. Исходный
код этого загрузчика можно найти в [loader_x86.c](../../src/loader_x86.c). Вот и все, **Easyboot** — это не GRUB и не syslinux, оба
из которых требуют десятков и десятков системных файлов на диске. Здесь больше никаких файлов не требуется, только этот (плагины не
являются обязательными, они не нужны для обеспечения совместимости с Multiboot2).

На *Raspberry Pi* загрузчик называется «KERNEL8.IMG», скомпилированный из [loader_rpi.c](../../src/loader_rpi.c).

### Загрузчика

Этот загрузчик очень тщательно написан для работы с несколькими конфигурациями. Он загружает таблицу разделения GUID с диска и ищет
«EFI System Partition». Найдя его, он ищет файл конфигурации `easyboot/menu.cfg` в этом загрузочном разделе. После того, как выбран
вариант загрузки и известно имя файла ядра, загрузчик находит и загружает его.

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

Состояние компьютера
--------------------

Все, что написано в [спецификации Multiboot2](https://www.gnu.org/software/grub/manual/multiboot2/multiboot.html) (на английском
языке) о состоянии машины, за исключением регистров общего назначения. **Easyboot** передает два аргумента в точку входа вашего
ядра в соответствии с SysV ABI и Microsoft fastcall ABI. Первый параметр — это магия, второй — адрес физической памяти, указывающий
на список тегов информации о мультизагрузке (далее сокращенно MBI, см. ниже).

Мы также немного нарушаем протокол Multiboot2 для обработки ядер старших половин. Multiboot2 требует, чтобы память была сопоставлена
с идентификаторами. Что ж, в случае **Easyboot** это верно лишь отчасти: мы лишь гарантируем, что вся физическая оперативная память
обязательно сопоставлена с идентификаторами, как и ожидалось; однако некоторые регионы выше этого (в зависимости от заголовков
программы ядра) могут быть еще доступны. Это не нарушает работу обычных ядер, совместимых с Multiboot2, которые не должны иметь
доступ к памяти за пределами доступной физической оперативной памяти.

Ваше ядро загружается одинаково как в системах BIOS, так и в UEFI, а также в RPi, различия в прошивке - это просто «чужая проблема».
Единственное, что увидит ваше ядро, — это содержит ли MBI тег системной таблицы EFI или нет. Чтобы упростить вашу жизнь,
**Easyboot** также не генерирует тег карты памяти EFI (тип 17), он предоставляет только тег [Карта памяти](#memory_map) (тип 6) без
разбора на всех платформах (в том числе и в системах UEFI). там карта памяти просто преобразуется для вас, поэтому вашему ядру
приходится иметь дело только с одним типом тега списка памяти). Старые, устаревшие теги также опускаются и никогда не генерируются
этим менеджером загрузки.

Ядро работает на уровне супервизора (кольцо 0 на x86, EL1 на ARM), возможно, на всех ядрах ЦП параллельно.

GDT не указан, но допустим. Стек установлен в первых 640k и растет вниз (но вы должны изменить это как можно скорее на любой стек,
который вы считаете достойным). Когда включен SMP, все ядра имеют свои собственные стеки, а идентификатор ядра находится на вершине
стека (но вы также можете получить идентификатор ядра обычным платформенно-специфическим способом, используя cpuid / mpidr / и т. д.).

Вам следует рассматривать IDT как неопределенный; IRQ, NMI и программные прерывания отключены. Фиктивные обработчики исключений
настроены на отображение минимального дампа и остановку машины. На них следует полагаться только в том случае, если ваше ядро
выйдет из строя до того, как вы сможете настроить свои собственные IDT и обработчики, желательно как можно скорее. В ARM vbar_el1
настроен на вызов одних и тех же фиктивных обработчиков исключений (хотя, конечно, они сбрасывают разные регистры).

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

Важно никогда не возвращаться из ядра. Вы можете перезаписать в памяти любую часть загрузчика (как только закончите с тегами MBI),
так что возвращаться просто некуда. "Der Mohr hat seine Schuldigkeit getan, der Mohr kann gehen."

Информация о загрузке передается в ваше ядро (MBI)
--------------------------------------------------

На первый взгляд это не очевидно, но спецификация Multiboot2 на самом деле определяет два совершенно независимых набора тегов:

- Первый набор должен быть встроен в ядро, совместимое с Multiboot2, и называется заголовком Multiboot2 образа ОС (раздел 3.1.2),
  следовательно, *предоставляется ядром*. **Easyboot** не учитывает эти теги и не анализирует их в вашем ядре. Вам просто не нужны
  какие-либо специальные магические данные, встроенные в файл ядра: **Easyboot**, достаточно заголовков ELF и PE.

- Второй набор *передается ядру* динамически при загрузке, **Easyboot** использует только эти теги. Однако он не генерирует все,
  что указывает Multiboot2 (он просто опускает старые, устаревшие или устаревшие). Эти теги называются тегами MBI, см.
  [Информация о загрузке](https://www.gnu.org/software/grub/manual/multiboot2/multiboot.html#Boot-information-format) (раздел 3.6).

ПРИМЕЧАНИЕ: Спецификация Multiboot2 для тегов MBI чертовски ошибочна. Ниже вы можете найти исправленную версию, которая
соответствует заголовочному файлу multiboot2.h, который вы можете найти в исходном репозитории GRUB.

Первый параметр вашего ядра — это магический 0x36d76289 (в `rax`, `rcx` и `rdi`). Вы можете найти теги MBI, используя второй
параметр (в `rbx`, `rdx` и `rsi`). На платформе ARM магия находится в `x0`, а адрес в `x1`. На RISC-V и MIPS используются `a0` и
`a1` соответственно. Если и когда этот загрузчик переносится на другую архитектуру, то всегда должны использоваться регистры,
указанные SysV ABI для аргументов функции. Если на платформе есть другие общие ABI, которые не мешают SysV ABI, то значения также
следует дублировать в регистрах этих ABI (или на вершине стека).

### Заголовки

Передаваемый адрес всегда выравнивается по 8 байтам и начинается с заголовка MBI:

```
        +-------------------+
u32     | total_size        |
u32     | reserved          |
        +-------------------+
```

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

```
        +-------------------+
u32     | type              |
u32     | size              |
        +-------------------+
```

`type` содержит идентификатор содержимого остальной части тега. `size` содержит размер тега, включая поля заголовка, но не включая
отступы. Теги следуют друг за другом, дополняя их при необходимости, чтобы каждый тег начинался с 8-байтового выровненного адреса.

### Терминатор

```
        +-------------------+
u32     | type = 0          |
u32     | size = 8          |
        +-------------------+
```

Теги завершаются тегом типа `0` и размера `8`.

### Загрузочная командная строка

```
        +-------------------+
u32     | type = 1          |
u32     | size              |
u8[n]   | string            |
        +-------------------+
```

`string` содержит командную строку, указанную в строке `kernel` *menuentry* (без пути к ядру и имени файла). Командная строка
представляет собой обычную строку UTF-8 с нулевым завершением в стиле C.

### Имя загрузчика

```
        +----------------------+
u32     | type = 2             |
u32     | size = 17            |
u8[n]   | string "Easyboot"    |
        +----------------------+
```

`string` содержит имя загрузчика, загружающего ядро. Имя представляет собой обычную строку UTF-8 в стиле C с нулевым завершением.

### Модули

```
        +-------------------+
u32     | type = 3          |
u32     | size              |
u32     | mod_start         |
u32     | mod_end           |
u8[n]   | string            |
        +-------------------+
```

Этот тег указывает ядру, какой загрузочный модуль был загружен вместе с образом ядра и где его можно найти. `mod_start` и `mod_end`
содержат начальный и конечный физические адреса самого загрузочного модуля. Вы никогда не получите буфер, сжатый gzip, потому что
**Easyboot** прозрачно распаковывает его за вас (а если вы предоставите плагин, он также будет работать и с данными, отличными от
сжатых gzip). Поле `string` предоставляет произвольную строку, которая будет связана с этим конкретным загрузочным модулем; это
обычная строка UTF-8 с нулевым завершением в стиле C. Указывается в строке `module` *menuentry*, и его точное использование зависит
от операционной системы. В отличие от тега командной строки загрузки, теги модуля *также включают* путь и имя файла модуля.

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

Существует особый случай: если файл представляет собой таблицу ACPI DSDT большой двоичный объект FDT (dtb) или GUDT, тогда он не
будет отображаться как модуль, а будет исправлен старый RSDP ACPI (тип 14) или новый RSDP ACPI (тип 15) и их DSDT заменены
содержимым этого файла.

### Карта памяти

Этот тег предоставляет карту памяти.

```
        +-------------------+
u32     | type = 6          |
u32     | size              |
u32     | entry_size = 24   |
u32     | entry_version = 0 |
varies  | entries           |
        +-------------------+
```

`size` содержит размер всех записей, включая само это поле. `entry_size` всегда равен 24. `entry_version` имеет значение `0`.
Каждая запись имеет следующую структуру:

```
        +-------------------+
u64     | base_addr         |
u64     | length            |
u32     | type              |
u32     | reserved          |
        +-------------------+
```

`base_addr` — это начальный физический адрес. `length` — это размер области памяти в байтах. `type` — это разновидность
представленного диапазона адресов, где значение `1` указывает на доступную оперативную память, значение `3` указывает на доступную
для использования память, содержащую информацию ACPI, значение `4` указывает на зарезервированную память, которую необходимо
сохранять при гибернации, значение `5` указывает на память, занятую неисправными модулями ОЗУ, а все остальные значения в настоящее
время указывают на зарезервированную область. Для параметра `reserved` установлено значение `0` при загрузке BIOS.

Когда MBI создается на машине UEFI, различные записи карты памяти EFI сохраняются как тип `1` (доступная ОЗУ) или `2`
(зарезервированная ОЗУ), и если вам это понадобится, исходный тип памяти EFI помещается в `reserved` поле.

Предоставленная карта гарантированно содержит список всей стандартной оперативной памяти, которая должна быть доступна для
нормального использования, и она всегда упорядочивается по возрастанию `base_addr`. Однако этот доступный тип ОЗУ включает области,
занимаемые ядром, mbi, сегментами и модулями. Ядро должно позаботиться о том, чтобы не перезаписать эти регионы (**Easyboot** может
легко исключить эти регионы, но это нарушит совместимость Multiboot2).

### Информация о фреймбуфере

```
        +----------------------------------+
u32     | type = 8                         |
u32     | size = 38                        |
u64     | framebuffer_addr                 |
u32     | framebuffer_pitch                |
u32     | framebuffer_width                |
u32     | framebuffer_height               |
u8      | framebuffer_bpp                  |
u8      | framebuffer_type = 1             |
u16     | reserved                         |
u8      | framebuffer_red_field_position   |
u8      | framebuffer_red_mask_size        |
u8      | framebuffer_green_field_position |
u8      | framebuffer_green_mask_size      |
u8      | framebuffer_blue_field_position  |
u8      | framebuffer_blue_mask_size       |
        +----------------------------------+
```

Поле `framebuffer_addr` содержит физический адрес кадрового буфера. Поле `framebuffer_pitch` содержит длину одной строки в байтах.
Поля `framebuffer_width`, `framebuffer_height` содержат размеры кадрового буфера в пикселях. Поле `framebuffer_bpp` содержит
количество бит на пиксель. `framebuffer_type` всегда имеет значение 1, а `reserved` всегда содержит 0 в текущей версии спецификации
и должен игнорироваться образом ОС. Остальные поля описывают формат упакованных пикселей, положение и размер каналов в битах. Вы
можете использовать выражение `((~(0xffffffff << size)) <<position) & 0xffffffff`, чтобы получить маску канала, подобную UEFI GOP.

### Указатель 64-битной системной таблицы EFI

Этот тег существует только в том случае, если **Easyboot** работает на компьютере с UEFI. На машине с BIOS этот тег никогда не
генерируется.

```
        +-------------------+
u32     | type = 12         |
u32     | size = 16         |
u64     | pointer           |
        +-------------------+
```

Этот тег содержит указатель на EFI system table.

### Указатель дескриптора 64-битного изображения EFI

Этот тег существует только в том случае, если **Easyboot** работает на компьютере с UEFI. На машине с BIOS этот тег никогда не
генерируется.

```
        +-------------------+
u32     | type = 20         |
u32     | size = 16         |
u64     | pointer           |
        +-------------------+
```

Этот тег содержит указатель на EFI image handle. Обычно это дескриптор образа загрузчика.

### Таблицы SMBIOS

```
        +-------------------+
u32     | type = 13         |
u32     | size              |
u8      | major             |
u8      | minor             |
u8[6]   | reserved          |
        | smbios таблицы    |
        +-------------------+
```

Этот тег содержит копию таблиц SMBIOS, а также их версию.

### ACPI старый RSDP

```
        +-------------------+
u32     | type = 14         |
u32     | size              |
        | копия RSDPv1      |
        +-------------------+
```

Этот тег содержит копию RSDP, определенную в спецификации ACPI 1.0. (С 32-битным адресом.)

### ACPI новый RSDP

```
        +-------------------+
u32     | type = 15         |
u32     | size              |
        | копия RSDPv2      |
        +-------------------+
```

Этот тег содержит копию RSDP, определенную в спецификации ACPI 2.0 или более поздней версии. (Возможно, с 64-битным адресом.)

Они (типы 14 и 15) указывают на таблицу `RSDT` или `XSDT` с указателем на таблицу `FACP`, которая, в свою очередь, содержит два
указателя на таблицу `DSDT`, описывающую машину. **Easyboot** подделывает эти таблицы на компьютерах, которые в противном случае не
поддерживают ACPI. Кроме того, если вы предоставляете таблицу DSDT, большой двоичный объект FDT (dtb) или GUDT в качестве модуля,
**Easyboot** исправит указатели, чтобы они указывали на эту предоставленную пользователем таблицу. Для анализа этих таблиц вы
можете использовать мою независимую библиотеку с одним заголовком [hwdet](https://gitlab.com/bztsrc/hwdet) (или раздутую библиотеку
[apcica](https://github.com/acpica/) acpica) и [libfdt](https://github.com/dgibson/dtc)).

Теги, специфичные для ядра
--------------------------

Теги с `type` больше или равным 256 не являются частью спецификации Multiboot2, тем не менее, предоставленной **Easyboot**.
Они могут быть добавлены в список с помощью дополнительных [плагинов](plugins.md), если и когда они потребуются ядру.

### EDID

```
        +-------------------+
u32     | type = 256        |
u32     | size              |
        | копия EDID        |
        +-------------------+
```

Этот тег содержит копию списка поддерживаемых разрешений монитора согласно спецификации EDID.

### SMP

```
        +-------------------+
u32     | type = 257        |
u32     | size              |
u32     | numcores          |
u32     | running           |
u32     | bspid             |
        +-------------------+
```

Этот тег существует, если была указана директива multicore. `numcores` содержит количество ядер ЦП в системе, `running` — это
количество ядер, которые успешно инициализировались и параллельно запускали одно и то же ядро. `bspid` содержит идентификатор ядра
начальной загрузки (по идентификатору lAPIC x86), чтобы ядра могли различать приложения и запускать на них другой код. Все ядра
приложения имеют свой собственный стек, и на вершине стека будет идентификатор текущего ядра.

### Идентификаторы разделов

```
        +-------------------+
u32     | type = 258        |
u32     | size = 24 / 40    |
u128    | bootuuid          |
u128    | rootuuid          |
        +-------------------+
```

Этот тег содержит уникальные поля идентификаторов в GPT загрузочного и корневого разделов. Если загрузка не использует таблицу
разделов GUID, то `bootuuid` генерируется как `54524150-(код устройства)-(номер раздела)-616F6F7400000000`.

Схема памяти
------------

### BIOS-машины

| Начало   | Конец   | Описание                                                              |
|---------:|--------:|-----------------------------------------------------------------------|
|      0x0 |   0x400 | Interrupt Vector Table (можно использовать, реальный режим IDT)       |
|    0x400 |   0x4FF | BIOS Data Area (можно использовать)                                   |
|    0x4FF |   0x500 | код загрузочного диска BIOS (скорее всего, 0x80; можно использовать)  |
|    0x500 |   0x5A0 | данные синхронизации для SMP (можно использовать)                     |
|    0x5A0 |  0x1000 | стек обработчиков исключений (можно использовать после настройки IDT) |
|   0x1000 |  0x8000 | таблицы подкачки (можно использовать после настройки таблиц подкачки) |
|   0x8000 | 0x20000 | код и данные загрузчика (можно использовать после настройки IDT)      |
|  0x20000 | 0x40000 | конфигурация + теги (можно использовать после анализа MBI)            |
|  0x40000 | 0x90000 | plugin ids; сверху вниз: стек ядра                                    |
|  0x90000 | 0x9A000 | Только ядро Linux: zero page + cmdline                                |
|  0x9A000 | 0xA0000 | Extended BIOS Data Area (лучше не трогать)                            |
|  0xA0000 | 0xFFFFF | VRAM и BIOS ROM (не использовать)                                     |
| 0x100000 |       x | сегменты ядра, за которыми следуют модули, каждая страница выровнена  |

### UEFI-машины

Никто не знает. UEFI распределяет память по своему усмотрению. Ожидайте всего и вся. Вся область обязательно будет указана в карте
памяти как тип = 1 (`MULTIBOOT_MEMORY_AVAILABLE`) и reserved = 2 (`EfiLoaderData`), однако это не является исключительным, другие
виды памяти также могут быть указаны таким же образом (например, раздел bss менеджера загрузки).

### Raspberry Pi

| Начало   | Конец   | Описание                                                              |
|---------:|--------:|-----------------------------------------------------------------------|
|      0x0 |   0x500 | зарезервировано прошивкой (лучше не трогать)                          |
|    0x500 |   0x5A0 | данные синхронизации для SMP (можно использовать)                     |
|    0x5A0 |  0x1000 | стек обработчиков исключений (можно использовать после настройки VBAR) |
|   0x1000 |  0x9000 | таблицы подкачки (можно использовать после настройки таблиц подкачки) |
|   0x9000 | 0x20000 | код и данные загрузчика (можно использовать после настройки VBAR)     |
|  0x20000 | 0x40000 | конфигурация + теги (можно использовать после анализа MBI)            |
|  0x40000 | 0x80000 | FDT (dtb) предоставляемый прошивкой; сверху вниз: стек ядра           |
| 0x100000 |       x | сегменты ядра, за которыми следуют модули, каждая страница выровнена  |

Первые несколько байтов зарезервированы для [armstub](https://github.com/raspberrypi/tools/blob/master/armstubs/armstub8.S).
Запустилось только ядро 0, поэтому для запуска процессоров приложений запишите адрес функции в 0xE0 (ядро 1), 0xE8 (ядро 2), 0xF0
(ядро 3), какие адреса расположены в этой области. Это не имеет значения, если используется директива `multicore`, тогда все ядра
будут выполнять ядро.

Хотя изначально RPi не поддерживается, вы все равно получаете старый тег RSDP ACPI (тип 14) с поддельными таблицами. Таблица `APIC`
используется для передачи ядру количества доступных ядер ЦП. Адрес функции запуска хранится в поле RSD PTR -> RSDT -> APIC ->
cpu\[x].apic_id (и идентификатор ядра в cpu\[x].acpi_id, где BSP всегда равен cpu\[0].acpi_id = 0 и cpu\[0].apic_id = 0xD8. Будьте
осторожны, «acpi» и «apic» выглядят очень похоже).

Если встроенное FDT передает действительный большой двоичный объект FDT или если один из модулей представляет собой файл .dtb, .gud
или .aml, то также добавляется таблица FADT (с магическим `FACP`). В этой таблице указатель DSDT (32-битный, со смещением 40)
указывает на предоставленный плоский объект дерева устройств.

Несмотря на то, что прошивка не обеспечивает функцию карты памяти, вы все равно получите тег карты памяти (тип 6), в котором
перечислены обнаруженные ОЗУ и регион MMIO. Вы можете использовать это для определения базового адреса MMIO, который отличается на
RPi3 и RPi4.
