plugins.md 15 KB

Easyboot 插件

默认情况下,Easyboot 从启动分区启动 ELF 和 PE 格式的 Multiboot2 兼容内核。如果您的内核使用不同的文件格式、 不同的启动协议,或者不在启动分区上,那么您将需要启动分区上的插件。您可以在 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 链接器来创建最终的二进制文件。 这是一个与架构无关的交叉链接器,无论插件是为哪种机器代码编译的,它都可以工作。最终的 .plg 大小只是它所生成 .o ELF 的一小部分。

Plugin API

插件的 C 源代码必须包含 src/loader.h 头文件,并且必须包含 EASY_PLUGIN 行。它有一个参数,即插件的类型,后面跟着标识符匹配规范。 加载器使用后者来确定何时使用该特定插件。

例如:

#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 内核准备环境 */
}

插件可以使用多个变量和函数,这些都是在头文件中定义并运行时链接的。

uint32_t verbose;

详细程度。如果该值不为零,则插件才允许打印任何输出,错误消息除外。该值越大,打印的详细信息就越多。

uint64_t file_size;

打开的文件的总大小(请参阅下面的openloadfile)。

uint8_t *root_buf;

当文件系统插件初始化时,它包含分区的前 128k(希望包括超级块)。稍后,文件系统插件可以将这 128k 缓冲区重新用于任何目的(FAT 缓存、inode 缓存等)。

uint8_t *tags_buf;

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

uint8_t *tags_ptr;

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

uint8_t *rsdp_ptr;

指向RSDP ACPI指针。

uint8_t *dsdt_ptr;

指向 DSDT(或 GUDT、FDT)硬件描述二进制文件。

efi_system_table_t *ST;

在 UEFI 机器上指向 EFI 系统表,否则为NULL

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 编译器也可能会生成对这些函数的调用)。

void *alloc(uint32_t num);

分配 num 页(4k)内存。插件不能分配太多,必须以最小的内存占用为目标。

void free(void *buf, uint32_t num);

释放先前分配的num页内存。

void printf(char *fmt, ...);

将格式化的字符串打印到启动控制台。

uint64_t pb_init(uint64_t size);

启动进度条,size 是其代表的总大小。返回一个像素值多少个字节。

void pb_draw(uint64_t curr);

绘制当前值的进度条。curr 必须介于 0 和总大小之间。

void pb_fini(void);

关闭进度栏,清除其在屏幕上的位置。

void loadsec(uint64_t sec, void *dst);

由文件系统插件使用,将扇区从磁盘加载到内存中。sec 是扇区的编号,相对于根分区。

void sethooks(void *o, void *r, void *c);

由文件系统插件使用,为根分区的文件系统设置open / read / close函数挂钩。

int open(char *fn);

打开根(或启动)分区上的文件进行读取,成功时返回 1。任何给定时间只能打开一个文件。如果事先没有sethooks调用,则它会在启动分区上进行操作。

uint64_t read(uint64_t offs, uint64_t size, void *buf);

从查找位置offs处打开的文件中读取数据到内存中,返回实际读取的字节数。

void close(void);

关闭打开的文件。

uint8_t *loadfile(char *path);

将文件从根(或启动)分区完全加载到新分配的内存缓冲区中,如果找到插件,则透明地解压缩它。大小在file_size中返回。

int loadseg(uint32_t offs, uint32_t filesz, uint64_t vaddr, uint32_t memsz);

从内核缓冲区加载一个段。这将检查内存vaddr是否可用,如果是高半部分,则映射该段。offs是文件偏移量,因此相对于内核缓冲区。如果memsz大于filesz, 则差值用零填充。

void _start(void);

文件系统插件(PLG_T_FS)的入口点。它应该解析root_buf中的超级块并调用sethooks。发生错误时它应该直接返回而不设置其钩子。

void _start(uint8_t *buf, uint64_t size);

内核插件的入口点(PLG_T_KERNEL)。接收内存中的内核映像,它应该重新定位其段,设置适当的环境并转移控制权。当没有错误时,它永远不会返回。

uint8_t *_start(uint8_t *buf);

解压插件的入口点(PLG_T_DECOMP)。接收压缩缓冲区(及其在file_size中的大小),并应返回一个分配的新缓冲区,其中包含未压缩的数据 (并在file_size中设置新缓冲区的大小)。它必须释放旧缓冲区(注意,file_size以字节为单位,但 free() 需要以页为单位的大小)。 出现错误时,file_size不得更改,并且它必须返回未修改的原始缓冲区。

void _start(void);

标签插件的入口点(PLG_T_TAG)。它们可能会在tags_ptr处添加新标签,并将该指针调整到新的 8 字节对齐位置。

本地函数

插件可以使用本地函数,但是由于 CLang 的一个错误,这些函数必须声明为static。(错误在于,CLang 会为这些函数生成 PLT 记录,即使在命令行上传递了 -fno-plt标志。使用static可以解决这个问题)。

低级文件格式规范

如果有人想用非 C 语言(例如汇编语言)编写插件,这里是文件格式的低级描述。

它与 a.out 格式非常相似。该文件由固定大小的标头和随后的长度不等的节组成。没有节头,每​​个节的数据直接跟在前一个节的数据后面,顺序如下:

(标头)
(标识符匹配记录)
(重定位记录)
(机器代码)
(只读数据)
(已初始化的可读写数据)

对于第一个实际部分(机器代码),包含对齐。对于所有其他部分,填充将添加到前一个部分的大小。

提示:如果将插件作为单个参数传递给plgld,那么它会转储文件中的各个部分,输出类似于readelf -aobjdump -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 指令开头:

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 部分

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