1. Linux/x86 引导协议¶
在 x86 平台上,Linux 内核采用了一种相当复杂的引导约定。这部分是由于历史因素演变而来,也由于早期希望内核本身就是一个可引导镜像的愿望,复杂的 PC 内存模型以及实模式 DOS 作为主流操作系统的有效消亡所引起的 PC 行业预期变化。
目前,Linux/x86 引导协议存在以下版本。
旧内核 |
仅支持 zImage/Image。一些非常早期的内核甚至可能不支持命令行。 |
协议 2.00 |
(内核 1.3.73) 增加了 bzImage 和 initrd 支持,以及引导加载程序和内核之间正式的通信方式。setup.S 变得可重定位,尽管传统的设置区域仍被假定为可写。 |
协议 2.01 |
(内核 1.3.76) 增加了堆溢出警告。 |
协议 2.02 |
(内核 2.4.0-test3-pre3) 新的命令行协议。降低常规内存上限。不覆盖传统设置区域,从而使使用 SMM 的 EBDA 或 32 位 BIOS 入口点的系统引导安全。zImage 已弃用但仍受支持。 |
协议 2.03 |
(内核 2.4.18-pre1) 明确将 initrd 的最高可能地址提供给引导加载程序。 |
协议 2.04 |
(内核 2.6.14) 将 syssize 字段扩展到四个字节。 |
协议 2.05 |
(内核 2.6.20) 使保护模式内核可重定位。引入 relocatable_kernel 和 kernel_alignment 字段。 |
协议 2.06 |
(内核 2.6.22) 增加了一个包含引导命令行大小的字段。 |
协议 2.07 |
(内核 2.6.24) 增加了半虚拟化引导协议。在 load_flags 中引入了 hardware_subarch 和 hardware_subarch_data 以及 KEEP_SEGMENTS 标志。 |
协议 2.08 |
(内核 2.6.26) 增加了 crc32 校验和及 ELF 格式的有效载荷。引入了 payload_offset 和 payload_length 字段以帮助定位有效载荷。 |
协议 2.09 |
(内核 2.6.26) 增加了一个指向 |
协议 2.10 |
(内核 2.6.31) 增加了在 kernel_alignment 之外的宽松对齐协议,新的 init_size 和 pref_address 字段。增加了扩展引导加载程序 ID。 |
协议 2.11 |
(内核 3.6) 增加了一个 EFI 交接协议入口点偏移量的字段。 |
协议 2.12 |
(内核 3.8) 增加了 xloadflags 字段和 |
协议 2.13 |
(内核 3.14) 支持在 xloadflags 中设置 32 位和 64 位标志,以支持从 32 位 EFI 引导 64 位内核。 |
协议 2.14 |
因错误的 COMMIT ae7e1238e68f2a472a125673ab506d49158c1889 (“x86/boot: Add ACPI RSDP address to setup_header”) 而烧毁,请勿使用!!!假定与 2.13 相同。 |
协议 2.15 |
(内核 5.5) 增加了 kernel_info 和 kernel_info.setup_type_max。 |
注意
只有当 setup header 更改时才应更改协议版本号。如果 boot_params 或 kernel_info 更改,则无需更新版本号。此外,建议使用 xloadflags (在这种情况下也不应更新协议版本号) 或 kernel_info 来向引导加载程序传达受支持的 Linux 内核功能。由于原始 setup header 中可用空间非常有限,因此对其进行的每一次更新都应谨慎考虑。从协议 2.15 开始,与引导加载程序通信的主要方式是 kernel_info。
1.1. 内存布局¶
用于 Image 或 zImage 内核的传统内核加载程序内存映射通常如下所示
| |
0A0000 +------------------------+
| Reserved for BIOS | Do not use. Reserved for BIOS EBDA.
09A000 +------------------------+
| Command line |
| Stack/heap | For use by the kernel real-mode code.
098000 +------------------------+
| Kernel setup | The kernel real-mode code.
090200 +------------------------+
| Kernel boot sector | The kernel legacy boot sector.
090000 +------------------------+
| Protected-mode kernel | The bulk of the kernel image.
010000 +------------------------+
| Boot loader | <- Boot sector entry point 0000:7C00
001000 +------------------------+
| Reserved for MBR/BIOS |
000800 +------------------------+
| Typically used by MBR |
000600 +------------------------+
| BIOS use only |
000000 +------------------------+
使用 bzImage 时,保护模式内核被重定位到 0x100000(“高内存”),并且内核实模式块(引导扇区、设置和栈/堆)被设置为可重定位到 0x10000 到低内存末尾之间的任何地址。不幸的是,在协议 2.00 和 2.01 中,0x90000+ 内存范围仍然由内核内部使用;协议 2.02 解决了这个问题。
希望将“内存上限”——引导加载程序触及的低内存最高点——保持在尽可能低的水平,因为一些较新的 BIOS 已经开始在低内存顶部附近分配一些相当大的内存量,称为扩展 BIOS 数据区。引导加载程序应该使用“INT 12h”BIOS 调用来验证可用低内存量。
不幸的是,如果 INT 12h 报告内存量过低,引导加载程序通常除了向用户报告错误外无能为力。因此,引导加载程序应设计为尽可能少地占用低内存空间。对于需要将数据写入 0x90000 段的 zImage 或旧 bzImage 内核,引导加载程序应确保不使用 0x9A000 点以上的内存;许多 BIOS 在该点以上会出错。
对于引导协议版本 >= 2.02 的现代 bzImage 内核,建议采用如下内存布局
~ ~
| Protected-mode kernel |
100000 +------------------------+
| I/O memory hole |
0A0000 +------------------------+
| Reserved for BIOS | Leave as much as possible unused
~ ~
| Command line | (Can also be below the X+10000 mark)
X+10000 +------------------------+
| Stack/heap | For use by the kernel real-mode code.
X+08000 +------------------------+
| Kernel setup | The kernel real-mode code.
| Kernel boot sector | The kernel legacy boot sector.
X +------------------------+
| Boot loader | <- Boot sector entry point 0000:7C00
001000 +------------------------+
| Reserved for MBR/BIOS |
000800 +------------------------+
| Typically used by MBR |
000600 +------------------------+
| BIOS use only |
000000 +------------------------+
... where the address X is as low as the design of the boot loader permits.
1.2. 实模式内核头¶
在以下文本以及内核引导序列的任何地方,“一个扇区”指 512 字节。它与底层介质的实际扇区大小无关。
加载 Linux 内核的第一步应该是加载实模式代码(引导扇区和设置代码),然后检查偏移量 0x01f1 处的以下头。实模式代码总共可达 32K,尽管引导加载程序可以选择仅加载前两个扇区(1K),然后检查引导扇区大小。
头文件如下所示
偏移量/大小 |
协议 |
名称 |
含义 |
|---|---|---|---|
01F1/1 |
ALL(1) |
setup_sects |
以扇区为单位的设置大小 |
01F2/2 |
ALL |
root_flags |
如果设置,根以只读方式挂载 |
01F4/4 |
2.04+(2) |
syssize |
32 位代码以 16 字节段为单位的大小 |
01F8/2 |
ALL |
ram_size |
请勿使用 - 仅供 bootsect.S 使用 |
01FA/2 |
ALL |
vid_mode |
视频模式控制 |
01FC/2 |
ALL |
root_dev |
默认根设备号 |
01FE/2 |
ALL |
boot_flag |
0xAA55 魔数 |
0200/2 |
2.00+ |
跳转 |
跳转指令 |
0202/4 |
2.00+ |
header |
魔术签名“HdrS” |
0206/2 |
2.00+ |
version |
支持的引导协议版本 |
0208/4 |
2.00+ |
realmode_swtch |
引导加载程序钩子(见下文) |
020C/2 |
2.00+ |
start_sys_seg |
加载低段 (0x1000) (已废弃) |
020E/2 |
2.00+ |
kernel_version |
指向内核版本字符串的指针 |
0210/1 |
2.00+ |
type_of_loader |
引导加载程序标识符 |
0211/1 |
2.00+ |
loadflags |
引导协议选项标志 |
0212/2 |
2.00+ |
setup_move_size |
移动到高内存大小(与钩子一起使用) |
0214/4 |
2.00+ |
code32_start |
引导加载程序钩子(见下文) |
0218/4 |
2.00+ |
ramdisk_image |
initrd 加载地址(由引导加载程序设置) |
021C/4 |
2.00+ |
ramdisk_size |
initrd 大小(由引导加载程序设置) |
0220/4 |
2.00+ |
bootsect_kludge |
请勿使用 - 仅供 bootsect.S 使用 |
0224/2 |
2.01+ |
heap_end_ptr |
设置结束后的空闲内存 |
0226/1 |
2.02+(3) |
ext_loader_ver |
扩展引导加载程序版本 |
0227/1 |
2.02+(3) |
ext_loader_type |
扩展引导加载程序 ID |
0228/4 |
2.02+ |
cmd_line_ptr |
指向内核命令行的 32 位指针 |
022C/4 |
2.03+ |
initrd_addr_max |
合法的最高 initrd 地址 |
0230/4 |
2.05+ |
kernel_alignment |
内核所需的物理地址对齐 |
0234/1 |
2.05+ |
relocatable_kernel |
内核是否可重定位 |
0235/1 |
2.10+ |
min_alignment |
最小对齐,作为 2 的幂 |
0236/2 |
2.12+ |
xloadflags |
引导协议选项标志 |
0238/4 |
2.06+ |
cmdline_size |
内核命令行的最大长度 |
023C/4 |
2.07+ |
hardware_subarch |
硬件子架构 |
0240/8 |
2.07+ |
hardware_subarch_data |
子架构特定数据 |
0248/4 |
2.08+ |
payload_offset |
内核有效载荷偏移量 |
024C/4 |
2.08+ |
payload_length |
内核有效载荷长度 |
0250/8 |
2.09+ |
setup_data |
指向 |
0258/8 |
2.10+ |
pref_address |
首选加载地址 |
0260/4 |
2.10+ |
init_size |
初始化期间所需的线性内存 |
0264/4 |
2.11+ |
handover_offset |
交接入口点偏移量 |
0268/4 |
2.15+ |
kernel_info_offset |
kernel_info 的偏移量 |
注意
为了向后兼容,如果 setup_sects 字段包含 0,则实际值为 4。
对于 2.04 之前的引导协议,syssize 字段的最高两个字节不可用,这意味着无法确定 bzImage 内核的大小。
对于引导协议 2.02-2.09,此字段被忽略,但设置是安全的。
如果在偏移量 0x202 处未找到“HdrS” (0x53726448) 魔数,则引导协议版本为“旧”。加载旧内核时,应假定以下参数
Image type = zImage
initrd not supported
Real-mode kernel must be located at 0x90000.
否则,“version”字段包含协议版本,例如,协议版本 2.01 将在此字段中包含 0x0201。设置头文件中的字段时,必须确保只设置当前协议版本支持的字段。
1.3. 头字段详情¶
对于每个字段,有些是从内核到引导加载程序的信息(“读取”),有些是期望由引导加载程序填充的(“写入”),还有一些是期望由引导加载程序读取和修改的(“修改”)。
所有通用引导加载程序都应写入标记为(强制)的字段。希望将内核加载到非标准地址的引导加载程序应填写标记为(重定位)的字段;其他引导加载程序可以忽略这些字段。
所有字段的字节序都是小端(毕竟这是 x86)。
字段名 |
setup_sects |
类型(Type) |
read |
偏移量/大小 |
0x1f1/1 |
协议 |
ALL |
设置代码的大小,以 512 字节扇区为单位。如果此字段为 0,则实际值为 4。实模式代码由引导扇区(始终为一个 512 字节扇区)加上设置代码组成。
字段名 |
root_flags |
类型(Type) |
修改(可选) |
偏移量/大小 |
0x1f2/2 |
协议 |
ALL |
如果此字段非零,则根默认以只读方式挂载。此字段的使用已弃用;请改用命令行上的“ro”或“rw”选项。
字段名 |
syssize |
类型(Type) |
read |
偏移量/大小 |
0x1f4/4 (协议 2.04+) 0x1f4/2 (所有协议) |
协议 |
2.04+ |
保护模式代码的大小,以 16 字节段为单位。对于 2.04 之前的协议版本,此字段仅为两个字节宽,因此如果设置了 LOAD_HIGH 标志,则无法信任其表示内核大小。
字段名 |
ram_size |
类型(Type) |
内核内部 |
偏移量/大小 |
0x1f8/2 |
协议 |
ALL |
此字段已废弃。
字段名 |
vid_mode |
类型(Type) |
修改(强制) |
偏移量/大小 |
0x1fa/2 |
请参阅有关特殊命令行选项的部分。
字段名 |
root_dev |
类型(Type) |
修改(可选) |
偏移量/大小 |
0x1fc/2 |
协议 |
ALL |
默认根设备的设备号。此字段的使用已弃用,请改用命令行上的“root=”选项。
字段名 |
boot_flag |
类型(Type) |
read |
偏移量/大小 |
0x1fe/2 |
协议 |
ALL |
包含 0xAA55。这是旧 Linux 内核最接近魔数的东西。
字段名 |
跳转 |
类型(Type) |
read |
偏移量/大小 |
0x200/2 |
协议 |
2.00+ |
包含一个 x86 跳转指令,0xEB 后跟相对于字节 0x202 的带符号偏移量。这可用于确定头的大小。
字段名 |
header |
类型(Type) |
read |
偏移量/大小 |
0x202/4 |
协议 |
2.00+ |
包含魔数“HdrS” (0x53726448)。
字段名 |
version |
类型(Type) |
read |
偏移量/大小 |
0x206/2 |
协议 |
2.00+ |
包含引导协议版本,格式为 (major << 8) + minor,例如,0x0204 表示版本 2.04,0x0a11 表示假想版本 10.17。
字段名 |
realmode_swtch |
类型(Type) |
修改(可选) |
偏移量/大小 |
0x208/4 |
协议 |
2.00+ |
引导加载程序钩子(参见下文的高级引导加载程序钩子。)
字段名 |
start_sys_seg |
类型(Type) |
read |
偏移量/大小 |
0x20c/2 |
协议 |
2.00+ |
加载低段 (0x1000)。已废弃。
字段名 |
kernel_version |
类型(Type) |
read |
偏移量/大小 |
0x20e/2 |
协议 |
2.00+ |
如果设置为非零值,则包含一个指向以 NUL 结尾的人类可读内核版本号字符串的指针,减去 0x200。这可用于向用户显示内核版本。此值应小于 (0x200 * setup_sects)。
例如,如果此值设置为 0x1c00,则内核版本号字符串可在内核文件中的偏移量 0x1e00 处找到。当且仅当“setup_sects”字段包含值 15 或更高时,此值才有效,因为
0x1c00 < 15 * 0x200 (= 0x1e00) but 0x1c00 >= 14 * 0x200 (= 0x1c00) 0x1c00 >> 9 = 14, So the minimum value for setup_secs is 15.
字段名 |
type_of_loader |
类型(Type) |
写入(强制) |
偏移量/大小 |
0x210/1 |
协议 |
2.00+ |
如果您的引导加载程序具有分配的 ID(参见下表),请在此处输入 0xTV,其中 T 是引导加载程序的标识符,V 是版本号。否则,在此处输入 0xFF。
对于 T > 0xD 的引导加载程序 ID,将 T = 0xE 写入此字段,并将扩展 ID 减去 0x10 写入 ext_loader_type 字段。类似地,ext_loader_ver 字段可用于为引导加载程序版本提供超过四位。
例如,对于 T = 0x15, V = 0x234,写入
type_of_loader <- 0xE4 ext_loader_type <- 0x05 ext_loader_ver <- 0x23已分配的引导加载程序 ID
0x0
LILO (0x00 保留给 2.00 之前的引导加载程序)
0x1
Loadlin
0x2
bootsect-loader (0x20,所有其他值保留)
0x3
Syslinux
0x4
Etherboot/gPXE/iPXE
0x5
ELILO
0x7
GRUB
0x8
U-Boot
0x9
Xen
0xA
Gujin
0xB
Qemu
0xC
Arcturus Networks uCbootloader
0xD
kexec-tools
0xE
扩展(参见 ext_loader_type)
0xF
特殊(0xFF = 未定义)
0x10
保留
0x11
最小 Linux 引导加载程序 <http://sebastian-plotz.blogspot.de>
0x12
OVMF UEFI 虚拟化栈
0x13
barebox
如果您需要分配引导加载程序 ID 值,请联系 <hpa@zytor.com>。
字段名 |
loadflags |
类型(Type) |
修改(强制) |
偏移量/大小 |
0x211/1 |
协议 |
2.00+ |
此字段是一个位掩码。
位 0 (读取): LOADED_HIGH
如果为 0,保护模式代码加载到 0x10000。
如果为 1,保护模式代码加载到 0x100000。
位 1 (内核内部): KASLR_FLAG
由压缩内核内部使用,将 KASLR 状态传递给内核本身。
如果为 1,KASLR 启用。
如果为 0,KASLR 禁用。
位 5 (写入): QUIET_FLAG
如果为 0,打印早期消息。
如果为 1,抑制早期消息。
这要求内核(解压缩器和早期内核)不写入需要直接访问显示硬件的早期消息。
位 6 (已废弃): KEEP_SEGMENTS
协议: 2.07+
此标志已废弃。
位 7 (写入): CAN_USE_HEAP
将此位置 1 以指示 heap_end_ptr 中输入的值有效。如果此字段为 0,则某些设置代码功能将被禁用。
字段名 |
setup_move_size |
类型(Type) |
修改(强制) |
偏移量/大小 |
0x212/2 |
协议 |
2.00-2.01 |
使用协议 2.00 或 2.01 时,如果实模式内核未加载到 0x90000,它将在加载序列的稍后阶段被移动到那里。如果您希望除实模式内核本身之外的其他数据(例如内核命令行)也被移动,请填写此字段。
单位是自引导扇区开始以来的字节数。
当协议版本为 2.02 或更高,或者实模式代码加载到 0x90000 时,此字段可以忽略。
字段名 |
code32_start |
类型(Type) |
修改(可选,重定位) |
偏移量/大小 |
0x214/4 |
协议 |
2.00+ |
在保护模式下要跳转到的地址。这默认为内核的加载地址,可由引导加载程序用于确定正确的加载地址。
此字段可用于两个目的
作为引导加载程序钩子(参见下面的高级引导加载程序钩子。)
如果一个未安装钩子的引导加载程序将可重定位内核加载到非标准地址,它将不得不修改此字段以指向加载地址。
字段名 |
ramdisk_image |
类型(Type) |
写入(强制) |
偏移量/大小 |
0x218/4 |
协议 |
2.00+ |
初始 ramdisk 或 ramfs 的 32 位线性地址。如果没有初始 ramdisk/ramfs,则保留为零。
字段名 |
ramdisk_size |
类型(Type) |
写入(强制) |
偏移量/大小 |
0x21c/4 |
协议 |
2.00+ |
初始 ramdisk 或 ramfs 的大小。如果没有初始 ramdisk/ramfs,则保留为零。
字段名 |
bootsect_kludge |
类型(Type) |
内核内部 |
偏移量/大小 |
0x220/4 |
协议 |
2.00+ |
此字段已废弃。
字段名 |
heap_end_ptr |
类型(Type) |
写入(强制) |
偏移量/大小 |
0x224/2 |
协议 |
2.01+ |
将此字段设置为设置栈/堆末尾的偏移量(从实模式代码开头算起),减去 0x0200。
字段名 |
ext_loader_ver |
类型(Type) |
写入(可选) |
偏移量/大小 |
0x226/1 |
协议 |
2.02+ |
此字段用作 type_of_loader 字段中版本号的扩展。总版本号被认为是 (type_of_loader & 0x0f) + (ext_loader_ver << 4)。
此字段的使用取决于引导加载程序。如果未写入,则为零。
2.6.31 之前的内核不识别此字段,但对于协议版本 2.02 或更高版本,写入此字段是安全的。
字段名 |
ext_loader_type |
类型(Type) |
写入(如果 (type_of_loader & 0xf0) == 0xe0,则强制) |
偏移量/大小 |
0x227/1 |
协议 |
2.02+ |
此字段用作 type_of_loader 字段中类型号的扩展。如果 type_of_loader 中的类型为 0xE,则实际类型为 (ext_loader_type + 0x10)。
如果 type_of_loader 中的类型不是 0xE,则此字段被忽略。
2.6.31 之前的内核不识别此字段,但对于协议版本 2.02 或更高版本,写入此字段是安全的。
字段名 |
cmd_line_ptr |
类型(Type) |
写入(强制) |
偏移量/大小 |
0x228/4 |
协议 |
2.02+ |
将此字段设置为内核命令行的线性地址。内核命令行可以位于设置堆的末尾和 0xA0000 之间的任何位置;它不必与实模式代码本身位于同一个 64K 段中。
即使您的引导加载程序不支持命令行,也要填写此字段,在这种情况下,您可以将其指向空字符串(或者更好的是,指向字符串“auto”)。如果此字段保留为零,则内核将假定您的引导加载程序不支持 2.02+ 协议。
字段名 |
initrd_addr_max |
类型(Type) |
read |
偏移量/大小 |
0x22c/4 |
协议 |
2.03+ |
初始 ramdisk/ramfs 内容可能占用的最大地址。对于 2.02 或更早的引导协议,此字段不存在,最大地址为 0x37FFFFFF。(此地址定义为最高安全字节的地址,因此如果您的 ramdisk 恰好为 131072 字节长且此字段为 0x37FFFFFF,则可以将 ramdisk 从 0x37FE0000 开始。)
字段名 |
kernel_alignment |
类型(Type) |
读取/修改(重定位) |
偏移量/大小 |
0x230/4 |
协议 |
2.05+ (读取), 2.10+ (修改) |
内核所需的对齐单位(如果 relocatable_kernel 为 true)。如果一个可重定位内核加载时的对齐方式与此字段中的值不兼容,则将在内核初始化期间重新对齐。
从协议版本 2.10 开始,这反映了为最佳性能首选的内核对齐方式;加载程序可以修改此字段以允许更小的对齐方式。请参阅下面的 min_alignment 和 pref_address 字段。
字段名 |
relocatable_kernel |
类型(Type) |
读取(重定位) |
偏移量/大小 |
0x234/1 |
协议 |
2.05+ |
如果此字段非零,则内核的保护模式部分可以加载到满足 kernel_alignment 字段的任何地址。加载后,引导加载程序必须设置 code32_start 字段以指向加载的代码,或指向引导加载程序钩子。
字段名 |
min_alignment |
类型(Type) |
读取(重定位) |
偏移量/大小 |
0x235/1 |
协议 |
2.10+ |
如果此字段非零,则以 2 的幂次形式指示内核启动所需的最小对齐方式,而非首选对齐方式。如果引导加载程序使用此字段,它应该用所需的对齐单位更新 kernel_alignment 字段;通常
kernel_alignment = 1 << min_alignment;内核过度不对齐可能会导致显著的性能开销。因此,加载程序通常应尝试从 kernel_alignment 到此对齐方式的每个 2 的幂对齐。
字段名 |
xloadflags |
类型(Type) |
read |
偏移量/大小 |
0x236/2 |
协议 |
2.12+ |
此字段是一个位掩码。
位 0 (读取): XLF_KERNEL_64
如果为 1,此内核在 0x200 处具有传统的 64 位入口点。
位 1 (读取): XLF_CAN_BE_LOADED_ABOVE_4G
如果为 1,内核/boot_params/cmdline/ramdisk 可以位于 4G 以上。
位 2 (读取): XLF_EFI_HANDOVER_32
如果为 1,内核支持在 handover_offset 处给出的 32 位 EFI 交接入口点。
位 3 (读取): XLF_EFI_HANDOVER_64
如果为 1,内核支持在 handover_offset + 0x200 处给出的 64 位 EFI 交接入口点。
位 4 (读取): XLF_EFI_KEXEC
如果为 1,内核支持带有 EFI 运行时支持的 kexec EFI 引导。
字段名 |
cmdline_size |
类型(Type) |
read |
偏移量/大小 |
0x238/4 |
协议 |
2.06+ |
不包含终止零的命令行最大长度。这意味着命令行最多可以包含 cmdline_size 个字符。对于协议版本 2.05 及更早版本,最大长度为 255。
字段名 |
hardware_subarch |
类型(Type) |
写入(可选,默认为 x86/PC) |
偏移量/大小 |
0x23c/4 |
协议 |
2.07+ |
在半虚拟化环境中,硬件底层架构部分,例如中断处理、页表处理和访问进程控制寄存器,需要以不同的方式完成。
此字段允许引导加载程序通知内核我们处于这些环境中的一个。
0x00000000
默认的 x86/PC 环境
0x00000001
lguest
0x00000002
Xen
0x00000003
Intel MID (Moorestown, CloverTrail, Merrifield, Moorefield)
0x00000004
CE4100 电视平台
字段名 |
hardware_subarch_data |
类型(Type) |
写入(依赖于子架构) |
偏移量/大小 |
0x240/8 |
协议 |
2.07+ |
指向硬件子架构特定数据的指针。此字段目前在默认 x86/PC 环境中未使用,请勿修改。
字段名 |
payload_offset |
类型(Type) |
read |
偏移量/大小 |
0x248/4 |
协议 |
2.08+ |
如果非零,则此字段包含从保护模式代码开头到有效载荷的偏移量。
有效载荷可能已压缩。压缩和未压缩数据的格式应使用标准魔数确定。目前支持的压缩格式有 gzip(魔数 1F 8B 或 1F 9E)、bzip2(魔数 42 5A)、LZMA(魔数 5D 00)、XZ(魔数 FD 37)、LZ4(魔数 02 21)和 ZSTD(魔数 28 B5)。未压缩的有效载荷目前始终是 ELF(魔数 7F 45 4C 46)。
字段名 |
payload_length |
类型(Type) |
read |
偏移量/大小 |
0x24c/4 |
协议 |
2.08+ |
有效载荷的长度。
字段名 |
setup_data |
类型(Type) |
写入(特殊) |
偏移量/大小 |
0x250/8 |
协议 |
2.09+ |
指向
struct setup_data空终止单链表的 64 位物理指针。这用于定义一个更具可扩展性的引导参数传递机制。struct setup_data的定义如下:struct setup_data { __u64 next; __u32 type; __u32 len; __u8 data[]; }其中,next 是指向链表下一个节点的 64 位物理指针,最后一个节点的 next 字段为 0;type 用于标识数据内容;len 是数据字段的长度;data 存储实际有效载荷。
此列表在引导过程中可能会在多个点被修改。因此,修改此列表时应始终确保考虑链表已包含条目的情况。
setup_data 对于超大数据对象来说有点不便使用,原因在于 setup_data 头必须与数据对象相邻,并且它只有一个 32 位长度字段。然而,引导过程的中间阶段必须有一种方法来识别哪些内存块被内核数据占用,这一点很重要。
因此,在协议 2.15 中引入了 `setup_indirect 结构体` 和 `SETUP_INDIRECT` 类型
struct setup_indirect { __u32 type; __u32 reserved; /* Reserved, must be set to zero. */ __u64 len; __u64 addr; };type 成员是 SETUP_INDIRECT | SETUP_* 类型。然而,它不能是 SETUP_INDIRECT 本身,因为将 setup_indirect 制作成树形结构可能需要大量栈空间,而引导上下文中的栈空间可能有限。
让我们举例说明如何使用 setup_indirect 指向 SETUP_E820_EXT 数据。在这种情况下,setup_data 和 setup_indirect 将如下所示:
struct setup_data { .next = 0, /* or <addr_of_next_setup_data_struct> */ .type = SETUP_INDIRECT, .len = sizeof(setup_indirect), .data[sizeof(setup_indirect)] = (struct setup_indirect) { .type = SETUP_INDIRECT | SETUP_E820_EXT, .reserved = 0, .len = <len_of_SETUP_E820_EXT_data>, .addr = <addr_of_SETUP_E820_EXT_data>, }, }
注意
SETUP_INDIRECT | SETUP_NONE 对象无法与 SETUP_INDIRECT 本身正确区分。因此,引导加载程序无法提供此类对象。
字段名 |
pref_address |
类型(Type) |
读取(重定位) |
偏移量/大小 |
0x258/8 |
协议 |
2.10+ |
如果此字段非零,则表示内核的首选加载地址。如果可能,重定位引导加载程序应尝试在此地址加载。
不可重定位的内核将无条件地将自身移动到此地址并运行。可重定位的内核如果加载到此地址以下,则会将其自身移动到此地址。
字段名 |
init_size |
类型(Type) |
read |
偏移量/大小 |
0x260/4 |
此字段指示从内核运行时起始地址开始的线性连续内存量,这是内核在能够检查其内存映射之前所需的。这与内核启动所需的总内存量不同,但可由重定位引导加载程序用于帮助为内核选择安全的加载地址。
内核运行时起始地址由以下算法确定
if (relocatable_kernel) { if (load_address < pref_address) load_address = pref_address; runtime_start = align_up(load_address, kernel_alignment); } else { runtime_start = pref_address; }
因此,引导加载程序可以估算所需的内存窗口位置和大小为
memory_window_start = runtime_start;
memory_window_size = init_size;
字段名 |
handover_offset |
类型(Type) |
read |
偏移量/大小 |
0x264/4 |
此字段是从内核镜像开头到 EFI 交接协议入口点的偏移量。使用 EFI 交接协议引导内核的引导加载程序应跳转到此偏移量。
有关更多详细信息,请参见下面的 EFI 交接协议。
字段名 |
kernel_info_offset |
类型(Type) |
read |
偏移量/大小 |
0x268/4 |
协议 |
2.15+ |
此字段是从内核镜像开头到 kernel_info 的偏移量。kernel_info 结构嵌入在 Linux 镜像的未压缩保护模式区域中。
1.4. kernel_info¶
头文件之间的关系类似于各种数据段
setup_header = .data
boot_params/setup_data = .bss
上述列表中缺少什么?没错
kernel_info = .rodata
长期以来,我们一直(滥)用 .data 来存储可能属于 .rodata 或 .bss 的内容,这是由于缺乏替代方案,尤其是在早期,也因为惯性。此外,BIOS 存根负责创建 boot_params,因此它对基于 BIOS 的加载程序不可用(但 setup_data 可用)。
setup_header 永久限制为 144 字节,这是由于 2 字节跳转字段的范围,该字段兼作结构的长度字段,并结合 struct boot_params 中受保护模式加载程序或 BIOS 存根必须将其复制进去的“孔洞”大小。它目前长 119 字节,这给我们留下了 25 个非常宝贵的字节。如果不彻底修改引导协议,打破向后兼容性,这是无法解决的问题。
boot_params 本身限于 4096 字节,但可以通过添加 setup_data 条目任意扩展。它不能用于传递内核镜像的属性,因为它属于 .bss 且没有镜像提供的内容。
kernel_info 通过为有关内核镜像的信息提供一个可扩展的位置来解决这个问题。它是只读的,因为内核不能依赖引导加载程序将其内容复制到任何地方,但这没关系;如果需要,它仍然可以包含启用的引导加载程序应该复制到 setup_data 块中的数据项。
所有 kernel_info 数据都应该属于这个结构体。固定大小的数据必须放在 kernel_info_var_len_data 标签之前。可变大小的数据必须放在 kernel_info_var_len_data 标签之后。每个可变大小的数据块都必须以 header/magic 及其大小为前缀,例如:
kernel_info:
.ascii "LToP" /* Header, Linux top (structure). */
.long kernel_info_var_len_data - kernel_info
.long kernel_info_end - kernel_info
.long 0x01234567 /* Some fixed size data for the bootloaders. */
kernel_info_var_len_data:
example_struct: /* Some variable size data for the bootloaders. */
.ascii "0123" /* Header/Magic. */
.long example_struct_end - example_struct
.ascii "Struct"
.long 0x89012345
example_struct_end:
example_strings: /* Some variable size data for the bootloaders. */
.ascii "ABCD" /* Header/Magic. */
.long example_strings_end - example_strings
.asciz "String_0"
.asciz "String_1"
example_strings_end:
kernel_info_end:
这样,kernel_info 就是一个自包含的 blob。
注意
每个可变大小数据头/魔数可以是任何 4 个字符的字符串,字符串末尾没有 0,并且不与现有可变长度数据头/魔数冲突。
1.5. kernel_info 字段详情¶
字段名 |
header |
偏移量/大小 |
0x0000/4 |
包含魔数“LToP” (0x506f544c)。
字段名 |
size |
偏移量/大小 |
0x0004/4 |
此字段包含 kernel_info 的大小,包括 kernel_info.header。它不计算 kernel_info.kernel_info_var_len_data 的大小。引导加载程序应使用此字段来检测 kernel_info 中支持的固定大小字段以及 kernel_info.kernel_info_var_len_data 的起始位置。
字段名 |
size_total |
偏移量/大小 |
0x0008/4 |
此字段包含 kernel_info 的大小,包括 kernel_info.header 和 kernel_info.kernel_info_var_len_data。
字段名 |
setup_type_max |
偏移量/大小 |
0x000c/4 |
此字段包含 setup_data 和 setup_indirect 结构体的最大允许类型。
1.6. 内核命令行¶
内核命令行已成为引导加载程序与内核通信的重要方式。它的一些选项也与引导加载程序本身相关,请参阅下面的“特殊命令行选项”。
内核命令行是一个以 null 结尾的字符串。最大长度可以从 cmdline_size 字段检索。在协议版本 2.06 之前,最大长度为 255 个字符。过长的字符串将被内核自动截断。
如果引导协议版本为 2.02 或更高,则内核命令行的地址由头字段 cmd_line_ptr 给出(见上文)。此地址可以位于设置堆的末尾和 0xA0000 之间的任何位置。
如果协议版本**不是** 2.02 或更高,则使用以下协议输入内核命令行:
在偏移量 0x0020 (字),即“cmd_line_magic”,输入魔数 0xA33F。
在偏移量 0x0022 (字),即“cmd_line_offset”,输入内核命令行的偏移量(相对于实模式内核的起始)。
内核命令行**必须**在 setup_move_size 覆盖的内存区域内,因此您可能需要调整此字段。
1.7. 实模式代码的内存布局¶
实模式代码需要设置栈/堆,以及为内核命令行分配内存。这需要在底部兆字节中实模式可访问的内存中完成。
需要注意的是,现代机器通常具有相当大的扩展 BIOS 数据区 (EBDA)。因此,建议尽可能少地使用低兆字节内存。
不幸的是,在以下情况下必须使用 0x90000 内存段:
加载 zImage 内核时 ((loadflags & 0x01) == 0)。
加载 2.01 或更早的引导协议内核时。
注意
对于 2.00 和 2.01 引导协议,实模式代码可以加载到另一个地址,但它会在内部重定位到 0x90000。对于“旧”协议,实模式代码必须加载到 0x90000。
当加载到 0x90000 时,避免使用 0x9a000 以上的内存。
对于引导协议 2.02 或更高版本,命令行不必与实模式设置代码位于同一个 64K 段中;因此允许为栈/堆提供完整的 64K 段,并将命令行放置在其上方。
内核命令行不应位于实模式代码下方,也不应位于高内存中。
1.8. 引导配置示例¶
作为一个示例配置,假设实模式段的布局如下。
当加载到 0x90000 以下时,使用整个段:
0x0000-0x7fff
实模式内核
0x8000-0xdfff
栈和堆
0xe000-0xffff
内核命令行
当加载到 0x90000 或协议版本为 2.01 或更早时:
0x0000-0x7fff
实模式内核
0x8000-0x97ff
栈和堆
0x9800-0x9fff
内核命令行
这样的引导加载程序应该在头文件中填写以下字段:
unsigned long base_ptr; /* base address for real-mode segment */
if (setup_sects == 0)
setup_sects = 4;
if (protocol >= 0x0200) {
type_of_loader = <type code>;
if (loading_initrd) {
ramdisk_image = <initrd_address>;
ramdisk_size = <initrd_size>;
}
if (protocol >= 0x0202 && loadflags & 0x01)
heap_end = 0xe000;
else
heap_end = 0x9800;
if (protocol >= 0x0201) {
heap_end_ptr = heap_end - 0x200;
loadflags |= 0x80; /* CAN_USE_HEAP */
}
if (protocol >= 0x0202) {
cmd_line_ptr = base_ptr + heap_end;
strcpy(cmd_line_ptr, cmdline);
} else {
cmd_line_magic = 0xA33F;
cmd_line_offset = heap_end;
setup_move_size = heap_end + strlen(cmdline) + 1;
strcpy(base_ptr + cmd_line_offset, cmdline);
}
} else {
/* Very old kernel */
heap_end = 0x9800;
cmd_line_magic = 0xA33F;
cmd_line_offset = heap_end;
/* A very old kernel MUST have its real-mode code loaded at 0x90000 */
if (base_ptr != 0x90000) {
/* Copy the real-mode kernel */
memcpy(0x90000, base_ptr, (setup_sects + 1) * 512);
base_ptr = 0x90000; /* Relocated */
}
strcpy(0x90000 + cmd_line_offset, cmdline);
/* It is recommended to clear memory up to the 32K mark */
memset(0x90000 + (setup_sects + 1) * 512, 0, (64 - (setup_sects + 1)) * 512);
}
1.9. 加载内核的其余部分¶
32 位(非实模式)内核在内核文件中从偏移量 (setup_sects + 1) * 512 处开始(同样,如果 setup_sects == 0,实际值为 4)。对于 Image/zImage 内核,它应该加载到地址 0x10000,对于 bzImage 内核,则加载到 0x100000。
如果协议版本 >= 2.00 且 loadflags 字段中的 0x01 位 (LOAD_HIGH) 已设置,则内核是 bzImage 内核
is_bzImage = (protocol >= 0x0200) && (loadflags & 0x01);
load_address = is_bzImage ? 0x100000 : 0x10000;
注意
Image/zImage 内核的大小可达 512K,因此使用 0x10000-0x90000 的整个内存范围。这意味着对于这些内核来说,将实模式部分加载到 0x90000 几乎是一个要求。bzImage 内核则提供了更大的灵活性。
1.10. 特殊命令行选项¶
如果用户输入了引导加载程序提供的命令行,则用户可能期望以下命令行选项能够工作。通常不应将它们从内核命令行中删除,即使并非所有选项对内核都实际有意义。需要为引导加载程序本身提供额外命令行选项的引导加载程序作者应在内核的命令行参数中注册它们,以确保它们现在或将来不会与实际的内核选项冲突。
- vga=<模式>
这里的 <模式> 可以是整数(以 C 语言表示法,可以是十进制、八进制或十六进制),也可以是字符串“normal”(表示 0xFFFF)、“ext”(表示 0xFFFE)或“ask”(表示 0xFFFD)之一。此值应输入到 vid_mode 字段中,因为它在命令行解析之前由内核使用。
- mem=<大小>
<大小> 是一个 C 语言表示法中的整数,可选地后跟(不区分大小写)K、M、G、T、P 或 E(分别表示 << 10、<< 20、<< 30、<< 40、<< 50 或 << 60)。这指定了内核的内存末端。这会影响 initrd 的可能放置位置,因为 initrd 应该放置在内存末端附近。请注意,这是一个**同时**适用于内核和引导加载程序的选项!
- initrd=<文件>
应加载 initrd。<文件> 的含义显然取决于引导加载程序,并且某些引导加载程序(例如 LILO)没有这样的命令。
此外,一些引导加载程序会将以下选项添加到用户指定的命令行:
- BOOT_IMAGE=<文件>
已加载的引导镜像。同样,<文件> 的含义显然取决于引导加载程序。
- auto
内核是在没有明确用户干预的情况下引导的。
如果这些选项是由引导加载程序添加的,强烈建议它们位于用户指定或配置指定的命令行**之前**。否则,“init=/bin/sh”会被“auto”选项混淆。
1.11. 运行内核¶
内核通过跳转到内核入口点启动,该入口点位于实模式内核起始处**段**偏移量 0x20。这意味着如果您将实模式内核代码加载到 0x90000,则内核入口点是 9020:0000。
进入时,ds = es = ss 应指向实模式内核代码的起始(如果代码加载在 0x90000,则为 0x9000),sp 应正确设置,通常指向堆顶,并且应禁用中断。此外,为防止内核中的错误,建议引导加载程序设置 fs = gs = ds = es = ss。
在我们上面的示例中,我们将这样做:
/*
* Note: in the case of the "old" kernel protocol, base_ptr must
* be == 0x90000 at this point; see the previous sample code.
*/
seg = base_ptr >> 4;
cli(); /* Enter with interrupts disabled! */
/* Set up the real-mode kernel stack */
_SS = seg;
_SP = heap_end;
_DS = _ES = _FS = _GS = seg;
jmp_far(seg + 0x20, 0); /* Run the kernel */
如果您的引导扇区访问软盘驱动器,建议在运行内核之前关闭软盘电机,因为内核引导会关闭中断,因此电机将不会被关闭,特别是如果加载的内核将软盘驱动程序作为按需加载模块!
1.12. 高级引导加载程序钩子¶
如果引导加载程序在特别恶劣的环境中运行(例如在 DOS 下运行的 LOADLIN),可能无法遵循标准的内存位置要求。这样的引导加载程序可以使用以下钩子,如果设置,它们将在适当的时候由内核调用。使用这些钩子可能应该被视为最后的手段!
重要提示:所有钩子都要求在调用过程中保留 %esp、%ebp、%esi 和 %edi。
- realmode_swtch
在进入保护模式之前立即调用的 16 位实模式远子例程。默认例程禁用 NMI,因此您的例程也应该这样做。
- code32_start
在转换到保护模式之后,但在内核解压缩之前立即**跳转**到的 32 位平坦模式例程。除 CS 外,不保证设置任何段(当前内核会设置,但旧内核不会);您应该自己将它们设置为 BOOT_DS (0x18)。
完成您的钩子后,您应该跳转到您的引导加载程序覆盖它之前(如果适用,已重定位)此字段中存在的地址。
1.13. 32 位引导协议¶
对于具有 EFI、LinuxBIOS 等非传统 BIOS 的机器以及 kexec,内核中基于传统 BIOS 的 16 位实模式设置代码无法使用,因此需要定义一个 32 位引导协议。
在 32 位引导协议中,加载 Linux 内核的第一步应该是设置引导参数(struct boot_params,传统上称为“零页”)。struct boot_params 的内存应被分配并初始化为全零。然后,内核镜像偏移量 0x01f1 处的设置头应加载到 struct boot_params 并进行检查。设置头的末尾可以按如下方式计算:
0x0202 + byte value at offset 0x0201
除了像 16 位引导协议那样读取/修改/写入 struct boot_params 的设置头之外,引导加载程序还应填写 struct boot_params 的附加字段,如零页章节所述。
设置 struct boot_params 后,引导加载程序可以像 16 位引导协议那样加载 32/64 位内核。
在 32 位引导协议中,内核通过跳转到 32 位内核入口点启动,该入口点是已加载的 32/64 位内核的起始地址。
进入时,CPU 必须处于禁用分页的 32 位保护模式;必须加载一个 GDT,其中包含选择器 __BOOT_CS(0x10) 和 __BOOT_DS(0x18) 的描述符;两个描述符都必须是 4G 平坦段;__BOOT_CS 必须具有执行/读取权限,__BOOT_DS 必须具有读取/写入权限;CS 必须是 __BOOT_CS,DS、ES、SS 必须是 __BOOT_DS;中断必须禁用;%esi 必须保存 struct boot_params 的基地址;%ebp、%edi 和 %ebx 必须为零。
1.14. 64 位引导协议¶
对于具有 64 位 CPU 和 64 位内核的机器,我们可以使用 64 位引导加载程序,并且我们需要一个 64 位引导协议。
在 64 位引导协议中,加载 Linux 内核的第一步应该是设置引导参数(struct boot_params,传统上称为“零页”)。struct boot_params 的内存可以分配在任何位置(甚至 4G 以上),并初始化为全零。然后,内核镜像偏移量 0x01f1 处的设置头应加载到 struct boot_params 并进行检查。设置头的末尾可以按如下方式计算:
0x0202 + byte value at offset 0x0201
除了像 16 位引导协议那样读取/修改/写入 struct boot_params 的设置头之外,引导加载程序还应填写 struct boot_params 的附加字段,如零页章节所述。
设置 struct boot_params 后,引导加载程序可以像 16 位引导协议那样加载 64 位内核,但内核可以加载到 4G 以上。
在 64 位引导协议中,内核通过跳转到 64 位内核入口点启动,该入口点是已加载的 64 位内核的起始地址加上 0x200。
进入时,CPU 必须处于启用分页的 64 位模式。从已加载内核的起始地址和零页以及命令行缓冲区开始,范围为 setup_header.init_size 的内存获得同一映射;必须加载一个 GDT,其中包含选择器 __BOOT_CS(0x10) 和 __BOOT_DS(0x18) 的描述符;两个描述符都必须是 4G 平坦段;__BOOT_CS 必须具有执行/读取权限,__BOOT_DS 必须具有读取/写入权限;CS 必须是 __BOOT_CS,DS、ES、SS 必须是 __BOOT_DS;中断必须禁用;%rsi 必须保存 struct boot_params 的基地址。
1.15. EFI 交接协议(已弃用)¶
此协议允许引导加载程序将初始化推迟到 EFI 引导存根。引导加载程序需要从引导介质加载内核/initrd(s) 并跳转到 EFI 交接协议入口点,该入口点位于 startup_{32,64} 开头 hdr->handover_offset 字节处。
引导加载程序在处理节对齐、可执行镜像超出文件本身大小的内存占用以及 PE/COFF 头中可能影响镜像在 EFI 固件提供的执行上下文中作为 PE/COFF 二进制文件正确运行的任何其他方面时,**必须**遵守内核的 PE/COFF 元数据。
交接入口点的函数原型如下所示:
void efi_stub_entry(void *handle, efi_system_table_t *table, struct boot_params *bp);
“handle”是 EFI 固件传递给引导加载程序的 EFI 镜像句柄,“table”是 EFI 系统表——这些是 UEFI 规范 2.3 节中描述的“交接状态”的前两个参数。“bp”是引导加载程序分配的引导参数。
引导加载程序**必须**在 bp 中填写以下字段:
- hdr.cmd_line_ptr
- hdr.ramdisk_image (if applicable)
- hdr.ramdisk_size (if applicable)
所有其他字段应为零。
注意
EFI 交接协议已弃用,取而代之的是下面描述的普通 PE/COFF 入口点。
1.16. PE/COFF 入口点¶
当使用 CONFIG_EFI_STUB=y 编译时,内核可以作为常规 PE/COFF 二进制文件执行。有关实现细节,请参阅EFI 引导存根。
存根加载程序可以通过 UEFI 协议请求 initrd。为此,固件或引导加载程序需要注册一个句柄,该句柄包含 EFI_LOAD_FILE2 协议的实现以及暴露 LINUX_EFI_INITRD_MEDIA_GUID 供应商媒体设备路径的设备路径协议。在这种情况下,通过 EFI 存根引导的内核将调用注册协议上的 LoadFile2::LoadFile() 方法,指示固件将 initrd 加载到由内核/EFI 存根选择的内存位置。
这种方法消除了 EFI 引导加载程序需要了解 boot_params 的内部表示、命令行和 ramdisk 在内存中的放置要求/限制,或者内核镜像本身的放置等问题。
有关示例实现,请参阅原始 u-boot 实现或OVMF 实现。