Easyboot 插件
=============

默认情况下，[Easyboot](https://gitlab.com/bztsrc/easyboot) 从启动分区启动 ELF 和 PE 格式的 Multiboot2 兼容内核。如果您的内核使用不同的文件格式、
不同的启动协议，或者不在启动分区上，那么您将需要启动分区上的插件。您可以在 [src/plugins](../../src/plugins) 目录中找到这些插件。

[[_TOC_]]

安装
----

要安装插件，只需将它们复制到 `(indir)` 参数中指定的目录中，该目录位于 menu.cfg 文件旁边的 `easyboot` 子目录下。

例如：
```
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 的一小部分。

### Plugin 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;
```
当文件系统插件初始化时，它包含分区的前 128k（希望包括超级块）。稍后，文件系统插件可以将这 128k 缓冲区重新用于任何目的（FAT 缓存、inode 缓存等）。

```c
uint8_t *tags_buf;
```
包含 Multiboot2 标签。内核插件可以解析此标记，将启动管理器提供的数据转换为内核所需的任何格式。此指针指向缓冲区的开头。

```c
uint8_t *tags_ptr;
```
此指针指向 Multiboot2 标签缓冲区的末尾。标签插件可能会在此处添加新标签并调整此指针。

```c
uint8_t *rsdp_ptr;
```
指向RSDP ACPI指针。

```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` 页（4k）内存。插件不能分配太多，必须以最小的内存占用为目标。

```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 语言（例如汇编语言）编写插件，这里是文件格式的低级描述。

它与 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 和 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 将从累加器第 1 个字节开始按偏移量步骤搜索到缓冲区末尾的魔法字节，如果找到，则返回匹配的偏移量作为值。

如果大小为零，则将累加器设置为该值。如果大小不为零，则检查该数量的字节是否与给定的魔法字节匹配。

例如，检查 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 | 全局偏移表相对间接寻址                                       |
|      10 |    13 | 直接掩码索引（0 - 15）                                      |
|      14 |    19 | 起始位（0 - 63）                                           |
|      20 |    25 | 结束位（0 - 63）                                           |
|      26 |    31 | 否定地址标志位位置（0 - 63）                                 |

偏移量字段与插件头中的魔法相关，它选择必须执行重定位的内存中的整数。

符号告诉要使用哪个地址。0 表示插件在内存中加载的基地址，也就是内存中标头的魔法地址。其他值从 GOT 中选择一个外部符号地址，在加载器或另一个插件中定义，
查看 plgld.c 源代码中的`plg_got`数组以查看哪个值对应哪个符号。如果 GOT 相对位为 1，则使用符号的 GOT 条目的地址，而不是符号的实际地址。

如果程序计数器相对位为 1，则首先从地址中减去偏移量（指令指针相对寻址模式）。

立即数掩码索引指示哪些位存储指令中的地址。如果该索引为 0，则地址将按原样写入偏移量，与体系结构无关。对于 x86_64，仅允许索引 0。
对于 ARM Aarch64：0 = 按原样，1 = 0x07ffffe0（向左移位 5 位），2 = 0x07fffc00（向左移位 10 位），3 = 0x60ffffe0（使用 ADR/ADRP 指令，
立即数被移位并拆分为两个位组）。未来的体系结构可能会定义更多不同的立即数位掩码。

使用立即数掩码，从内存中取出结束 - 开始 + 1 位，并进行有符号扩展。该值被添加到地址（加数，如果是内部引用，内部符号的地址也在此处编码）。

如果取反地址标志位不为 0，且地址为正数，则清除该位。如果地址为负数，则设置该位，并将地址取反。

最后，起始位和结束位选择将地址的哪一部分写入所选整数。这也定义了重定位的大小，超出此范围的位和不属于直接掩码的位保持不变。

### 代码部分

此部分包含标题中指定的体系结构的机器指令，并且具有与代码大小字段相同的字节数。

### 只读数据部分

这是一个可选部分，可能会缺失。它的长度与标题中的只读部分大小字段所示长度相同。所有常量变量都放在此部分中。

### 初始化数据部分

这是一个可选部分，可能会丢失。如果文件中的代码部分（或可选的只读数据部分）之后仍有字节，则这些字节都被视为数据部分。如果变量用非零值初始化，则将其放置在此部分中。

### BSS 部分

这是一个可选部分，可能会缺失。此部分永远不会存储在文件中。如果内存大小字段大于标头中的文件大小字段，则它们的差值将在内存中用零填充。
如果变量未初始化或初始化为零，则将其放置在此部分中。
