默认情况下,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 的一小部分。
插件的 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;
打开的文件的总大小(请参阅下面的open
和loadfile
)。
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 -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 指令开头:
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,且地址为正数,则清除该位。如果地址为负数,则设置该位,并将地址取反。
最后,起始位和结束位选择将地址的哪一部分写入所选整数。这也定义了重定位的大小,超出此范围的位和不属于直接掩码的位保持不变。
此部分包含标题中指定的体系结构的机器指令,并且具有与代码大小字段相同的字节数。
这是一个可选部分,可能会缺失。它的长度与标题中的只读部分大小字段所示长度相同。所有常量变量都放在此部分中。
这是一个可选部分,可能会丢失。如果文件中的代码部分(或可选的只读数据部分)之后仍有字节,则这些字节都被视为数据部分。如果变量用非零值初始化,则将其放置在此部分中。
这是一个可选部分,可能会缺失。此部分永远不会存储在文件中。如果内存大小字段大于标头中的文件大小字段,则它们的差值将在内存中用零填充。 如果变量未初始化或初始化为零,则将其放置在此部分中。