Плагины Easyboot
================

По умолчанию [Easyboot](https://gitlab.com/bztsrc/easyboot) загружает ядра, совместимые с Multiboot2, в форматах ELF и PE из
загрузочного раздела. Если ваше ядро использует другой формат файла, другой протокол загрузки или не находится в загрузочном
разделе, вам потребуются плагины в загрузочном разделе. Вы можете найти их в каталоге [src/plugins](../../src/plugins).

[[_TOC_]]

Установка
---------

Чтобы установить плагины, просто скопируйте их в каталог, указанный в параметре `(indir)`, в подкаталог `easyboot` рядом с файлом
Menu.cfg.

Например:
```
bootpart
|-- easyboot
|   |-- linux_x86.plg
|   |-- minix_x86.plg
|   `-- menu.cfg
|-- EFI
|   `-- BOOT
|-- kernel
`-- initrd
$ easyboot bootpart диск.img
```

Компиляция
----------

*С самого начала было очевидно, что ELF не подходит для этой задачи. Он слишком раздут и слишком сложен. Поэтому изначально я хотел
использовать struct exec (классический формат UNIX a.out), но, к сожалению, современные наборы инструментов больше не могут его
создать. Поэтому я решил создать свой собственный формат и собственный компоновщик для плагинов.*

Вы можете скомпилировать исходный код плагина с помощью любого стандартного кросс-компилятора ANSI C в объектный файл ELF, но тогда
вам придется использовать компоновщик [plgld](../../src/misc/plgld.c) для создания последний двоичный файл. Это кросс-линкер, не
зависящий от архитектуры, который будет работать независимо от того, для какого машинного кода был скомпилирован плагин.
Окончательный файл .plg представляет собой лишь часть размера файла .o ELF, из которого он был создан.

### API плагина

Исходный код плагина на языке C должен включать заголовочный файл `src/loader.h` и строку `EASY_PLUGIN`. Он имеет один параметр:
тип плагина, за которым следует спецификация соответствия идентификатора. Последнее используется загрузчиком, чтобы определить,
когда использовать этот конкретный плагин.

Например:

```c
#include "../loader.h"

/* магические байты, идентифицирующие ядро Linux */
EASY_PLUGIN(PLG_T_KERNEL) {
   /* смещение размер тип соответствия  магические байты */
    { 0x1fe,       2, PLG_M_CONST,      { 0xAA, 0x55, 0, 0 } },
    { 0x202,       4, PLG_M_CONST,      { 'H', 'd', 'r', 'S' } }
};

/* точка входа, прототип определяется типом плагина */
PLG_API void _start(uint8_t *buf, uint64_t size);
{
    /* подготовить среду для ядра Linux */
}
```

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

```c
uint32_t verbose;
```
Уровень многословности. Плагину разрешено печатать любые выходные данные, только если оно не равно нулю, за исключением сообщений
об ошибках. Чем больше его значение, тем больше деталей необходимо напечатать.

```c
uint64_t file_size;
```
Общий размер открытого файла (см. `open` и `loadfile` ниже).

```c
uint8_t *root_buf;
```
Когда плагин файловой системы инициализируется, он содержит первые 128 КБ раздела (надеюсь, включая суперблок). Позже плагин
файловой системы может повторно использовать этот буфер размером 128 КБ для любых целей (кэш FAT, кеш inode и т. д.).

```c
uint8_t *tags_buf;
```
Содержит теги Multiboot2. Плагин ядра может проанализировать это, чтобы преобразовать данные, предоставленные менеджером загрузки,
в любой формат, который ожидает ядро. Этот указатель указывает на начало буфера.

```c
uint8_t *tags_ptr;
```
Этот указатель указывает на конец буфера тегов Multiboot2. Плагины тегов могут добавлять сюда новые теги и корректировать этот
указатель.

```c
uint8_t *rsdp_ptr;
```
Указывает на указатель ACPI RSDP.

```c
uint8_t *dsdt_ptr;
```
Указывает на большой двоичный объект описания оборудования DSDT (или GUDT, FDT).

```c
efi_system_table_t *ST;
```
На машинах UEFI указывает на системную таблицу EFI, в противном случае — `NULL`.

```c
void memset(void *dst, uint8_t c, uint32_t n);
void memcpy(void *dst, const void *src, uint32_t n);
int  memcmp(const void *s1, const void *s2, uint32_t n);
```
Обязательные функции памяти (компилятор C может генерировать их вызовы, даже если прямого вызова нет).

```c
void *alloc(uint32_t num);
```
Выделяет `num` страниц (4 КБ) памяти. Плагины не должны выделять много памяти, они должны стремиться к минимальному использованию
памяти.

```c
void free(void *buf, uint32_t num);
```
Освободить ранее выделенную память для `num` страниц.

```c
void printf(char *fmt, ...);
```
Выведите отформатированную строку в загрузочную консоль.

```c
uint64_t pb_init(uint64_t size);
```
Запускает индикатор выполнения, `size` это общий размер, который он представляет. Возвращает количество байтов в одном пикселе.

```c
void pb_draw(uint64_t curr);
```
Рисует индикатор выполнения для текущего значения. `curr` должен быть между 0 и общим размером.


```c
void pb_fini(void);
```
Закрывает полосу выполнения, освобождает ее место на экране.

```c
void loadsec(uint64_t sec, void *dst);
```
Используется плагинами файловой системы, загружает сектор с диска в память. `sec` — номер сектора относительно корневого раздела.

```c
void sethooks(void *o, void *r, void *c);
```
Используется плагинами файловой системы, устанавливает перехваты функций open / read / close для файловой системы корневого раздела.

```c
int open(char *fn);
```
Открытие файла в корневом (или загрузочном) разделе для чтения, в случае успеха возвращает 1. В любой момент времени может быть
открыт только один файл. Если заранее не было вызова `sethooks`, он работает с загрузочным разделом.

```c
uint64_t read(uint64_t offs, uint64_t size, void *buf);
```
Считывает данные из открытого файла в позиции поиска `offs` в память, возвращает количество фактически прочитанных байтов.

```c
void close(void);
```
Закрывает открытый файл.

```c
uint8_t *loadfile(char *path);
```
Загрузите файл полностью из корневого (или загрузочного) раздела во вновь выделенный буфер памяти и прозрачно распакуйте его, если
плагин найден. Размер возвращается в `file_size`.

```c
int loadseg(uint32_t offs, uint32_t filesz, uint64_t vaddr, uint32_t memsz);
```
Загрузите сегмент из буфера ядра. Это проверяет, доступна ли память vaddr, и отображает сегмент, если он больше половины. `offs`
это смещение файла относительно буфера ядра. Если `memsz` больше, чем `filesz`, то разница заполняется нулями.

```c
void _start(void);
```
Точка входа для плагинов файловой системы (`PLG_T_FS`). Он должен проанализировать суперблок в `root_buf` и вызвать `sethooks`. В
случае ошибки он должен просто вернуться, не устанавливая хуки.

```c
void _start(uint8_t *buf, uint64_t size);
```
Точка входа для плагинов ядра (`PLG_T_KERNEL`). Получает образ ядра в памяти, ему следует переместить его сегменты, настроить
подходящее окружение и передать управление. Если ошибки нет, он никогда не возвращается.

```c
uint8_t *_start(uint8_t *buf);
```
Точка входа для плагинов декомпрессора (`PLG_T_DECOMP`). Получает сжатый буфер (и его размер в файле `file_size`) и должен вернуть
выделенный новый буфер с несжатыми данными (и установить размер нового буфера также в файле `file_size`). Он должен освободить
старый буфер (следите, `file_size` указывается в байтах, но free() ожидает размер в страницах). В случае ошибки `file_size` не
должен быть изменен и он должен возвращать неизмененный исходный буфер.

```c
void _start(void);
```
Точка входа для плагинов тегов (`PLG_T_TAG`). Они могут добавить новые теги в `tags_ptr` и настроить этот указатель на новую
позицию, выровненную по 8 байтам.

### Локальные функции

Плагины могут использовать локальные функции, однако из-за ошибки CLang они *должны* быть объявлены как `static`. (Ошибка в том,
что CLang генерирует записи PLT для этих функций, даже если в командной строке передается флаг «-fno-plt». Эту проблему можно
обойти с помощью `static`).

Низкоуровневая спецификация формата файла
-----------------------------------------

Если кто-то хочет написать плагин на языке, отличном от C (например, на Assembly), вот низкоуровневое описание формата файла.

Он очень похож на формат a.out. Файл состоит из заголовка фиксированного размера, за которым следуют разделы различной длины.
Заголовок раздела отсутствует, данные каждого раздела следуют непосредственно за данными предыдущего раздела в следующем порядке:

```
(заголовок)
(записи совпадений идентификаторов)
(записи о переезде)
(Машинный код)
(данные только для чтения)
(инициализированные данные, доступные для чтения и записи)
```

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

СОВЕТ: если вы передаете плагин в качестве одного аргумента в `plgld`, он выгружает разделы файла с выводом, аналогичным
`readelf -a` или `objdump -xd`.

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

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

| Смещение | Размер | Описание                                                        |
|---------:|-------:|-----------------------------------------------------------------|
|        0 |      4 | магические байты `EPLG`                                         |
|        4 |      4 | общий размер файла                                              |
|        8 |      4 | общий объем памяти, необходимый при загрузке файла              |
|       12 |      4 | размер раздела кода                                             |
|       16 |      4 | размер раздела данных, доступного только для чтения             |
|       20 |      4 | точка входа плагина                                             |
|       24 |      2 | код архитектуры (тот же, что и в ELF)                           |
|       26 |      2 | количество записей о переезде                                   |
|       28 |      1 | количество записей совпадений идентификаторов                   |
|       29 |      1 | запись GOT с наибольшим количеством ссылок                      |
|       30 |      1 | версия формата файла (на данный момент 0)                       |
|       31 |      1 | тип плагина (1=файловая система, 2=ядро, 3=декомпрессор, 4=тег) |

Код архитектуры такой же, как и в заголовках ELF, например 62 = x86_64, 183 = Aarch64 and 243 = RISC-V.

Тип плагина определяет прототип точки входа, ABI всегда SysV.

### Раздел соответствия идентификатора

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

| Смещение | Размер | Описание                                                        |
|---------:|-------:|-----------------------------------------------------------------|
|        0 |      2 | смещение                                                        |
|        2 |      1 | размер                                                          |
|        3 |      1 | тип                                                             |
|        4 |      4 | магические байты для сопоставления                              |

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

Поле типа сообщает, как интерпретировать смещение. Если это 1, то в качестве значения используется смещение плюс аккумулятор. Если
это 2, то по смещению берется 8-битное значение байта, 3 означает, что принимается 16-битное значение слова, а 4 означает, что
принимается 32-битное значение двойного слова. 5 означает, что нужно взять 8-битное байтовое значение и добавить к нему аккумулятор,
6 означает взять 16-битное значение слова и добавить к нему аккумулятор, а 7 — то же самое, но с 32-битным значением. 8 будет искать
магические байты от байта-аккумулятора до конца буфера с шагом смещения и, если они найдены, возвращает соответствующее смещение в
качестве значения.

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

Например, чтобы проверить, начинается ли исполняемый файл PE с инструкции NOP:
```c
EASY_PLUGIN(PLG_T_KERNEL) {
   /* смещение размер тип соответствия  магические байты */
    { 0,           2, PLG_M_CONST,      { 'M', 'Z', 0, 0 } }, /* проверить магические байты */
    { 60,          0, PLG_M_DWORD,      { 0, 0, 0, 0 } },     /* получить смещение заголовка PE в аккумуляторе */
    { 0,           4, PLG_M_CONST,      { 'P', 'E', 0, 0 } }, /* проверить магические байты */
    { 40,          1, PLG_M_DWORD,      { 0x90, 0, 0, 0 } }   /* проверьте наличие инструкции NOP в точке входа */
};
```

### Раздел переезда

Этот раздел содержит столько следующих записей, сколько указано в поле заголовка «количество записей о переезде».

| Смещение | Размер | Описание                                               |
|---------:|-------:|--------------------------------------------------------|
|        0 |      4 | смещение                                               |
|        4 |      4 | тип переезда                                           |

Значение битов в типе:

|      От | Чтобы | Описание                                                 |
|--------:|------:|----------------------------------------------------------|
|       0 |     7 | символ (0 - 255)                                         |
|       8 |     8 | Относительная адресация программа-счетчик                |
|       9 |     9 | GOT относительная косвенная адресация                    |
|      10 |    13 | индекс немедленной маски (0 - 15)                        |
|      14 |    19 | начало бит (0 - 63)                                      |
|      20 |    25 | конечный бит (0 - 63)                                    |
|      26 |    31 | позиция бита отрицательного флага адреса (0 - 63)        |

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

Символ указывает, какой адрес использовать. 0 означает BASE (БАЗОВЫЙ) адрес, по которому плагин был загружен в память, т.е. адрес
магии заголовка в памяти. Другие значения выбирают адрес внешнего символа из GOT, определенного в загрузчике или другом плагине.
Посмотрите на массив `plg_got` в исходном коде plgld.c, чтобы увидеть, какое значение соответствует какому символу. Если
относительный бит GOT равен 1, то вместо фактического адреса символа используется адрес записи GOT символа.

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

Индекс непосредственной маски сообщает, какие биты хранят адрес в инструкции. Если это 0, то адрес записывается в смещение как есть,
независимо от архитектуры. Для x86_64 разрешен только индекс 0. Для ARM Aarch64: 0 = как есть, 1 = 0x07ffffe0 (сдвиг влево на 5
бит), 2 = 0x07fffc00 (сдвиг влево на 10 бит), 3 = 0x60ffffe0 (с инструкциями ADR/ADRP непосредственный сдвиг сдвигается и
разделяется на две битовые группы). Будущие архитектуры могут определять больше и больше непосредственных битовых масок.

Используя непосредственную маску, из памяти берутся конец - начало + 1 бит и расширяется знак. Это значение добавляется к адресу
(добавляется, а в случае внутренних ссылок здесь также кодируется адрес внутреннего символа).

Если бит флага отрицательного адреса не равен 0, а адрес положителен, то этот бит очищается. Если адрес отрицательный, то этот бит
устанавливается, а адрес инвертируется.

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

### Раздел кода

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

### Раздел данных только для чтения

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

### Раздел инициализированных данных

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

### Раздел БСС

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