Linux/x86 引导协议(boot protocol)
1. Linux/x86 启动引导协议(特定于x86架构)
来自: https://www.kernel.org/doc/html/v5.6/x86/boot.html
如果你想分析Linux启动时候的 ...\linux内核\linux-2.6.38.5\arch\x86\boot\header.S 启动文件源码,那么就请细致的看下面和官方的英文,对照着进行阅读,我想收获会比较大的。
前提
1、首先你需要了解什么是实模式、保护模式
2、了解BIOS的一些工作方式。
3、不同内核源码在启动代码中的变化情况。
4、什么是x86架构
相关一些参考资料
本约定引导约定是特针对 x86 平台上的,Linux 内核设计使用了相当复杂的引导约定。这在一定程度上是由于历史方面的原因,以及早期希望内核本身成为可引导镜像的愿望、复杂的 PC 内存模型以及由于实际消亡导致的 PC 行业预期的改变。模式 DOS 作为主流操作系统。
当前,存在以下版本的 Linux/x86 引导协议。
旧内核 | 仅支持 zImage/Image。一些非常早期的内核甚至可能不支持命令行。 |
协议 2.00 | (Kernel 1.3.73)添加了 bzImage 和 initrd 支持,以及在引导加载程序和内核之间进行通信的正式方式。 setup.S 使可重定位,尽管传统的设置区域仍然假定为可写。 |
协议 2.01 | (Kernel 1.3.76)添加了堆溢出警告。 |
协议 2.02 | (Kernel 2.4.0-test3-pre3) 新的命令行协议。降低常规内存上限。不会覆盖传统设置区域,因此对于使用来自 SMM 或 32 位 BIOS 入口点的 EBDA 的系统来说,引导是安全的。zImage 已弃用但仍受支持。 |
协议 2.03 | (Kernel 2.4.18-pre1)显式地使引导加载程序可用的最高可能 initrd 地址。 |
协议 2.04 | (Kernel 2.6.14)将 syssize 字段扩展为四个字节。 |
协议 2.05 | (Kernel 2.6.20)使保护模式内核可重定位。引入 relocatable_kernel 和 kernel_alignment 字段。 |
协议 2.06 | (Kernel 2.6.22)添加了一个包含引导命令行大小的字段。 |
协议 2.07 | (Kernel 2.6.24)添加了准虚拟化引导协议。在 load_flags 中引入了 hardware_subarch 和 hardware_subarch_data 以及 KEEP_SEGMENTS 标志。 |
协议 2.08 | (Kernel 2.6.26)添加了 crc32 校验和和 ELF 格式有效装载。引入了 payload_offset 和 payload_length 字段以帮助定位有效载荷。 |
协议 2.09 | (Kernel 2.6.26) 添加一个64位物理指针字段到struct setup_data的单链表。 |
协议 2.10 | (Kernel 2.6.31)在添加的 kernel_alignment、新的 init_size 和 pref_address 字段之外添加了一个宽松对齐协议。添加了扩展引导加载程序 ID。 |
协议 2.11 | (内核 3.6)添加了 EFI handover protocol 入口点偏移量字段。 |
协议 2.12 | (Kernel 3.8) 在 struct boot_params 中添加了 xloadflags 字段和扩展字段,用于在 64 位中加载 4G 以上的 bzImage 和 ramdisk。 |
协议 2.13 | (Kernel 3.14)支持在 xloadflags 中设置 32 位和 64 位标志以支持从 32 位 EFI 引导 64 位内核 |
协议 2.14 | 被不正确的提交 ae7e1238e68f2a472a125673ab506d49158c1889 烧毁(x86/boot:将 ACPI RSDP 地址添加到 setup_header)不要使用!!!假设与 2.13 相同。 |
协议 2.15 | (Kernel 5.5) 添加了 kernel_info 和 kernel_info.setup_type_max。 |
笔记
仅当设置标头更改时才应更改协议版本号。如果更改了 boot_params 或 kernel_info,则无需更新版本号。此外,建议使用 xloadflags(在这种情况下也不应更新协议版本号)或 kernel_info 将支持的 Linux 内核功能传达给引导加载程序。由于原始设置标头中的可用空间非常有限,每次对其更新都应格外小心。从协议 2.15 开始,与引导加载程序通信的主要方式是 kernel_info。
1.1. 内存布局
用于 Image 或 zImage 内核的内核加载程序的传统内存映射通常如下所示:
| |
0A0000 +------------------------+
| 保留给BIOS | 不能使用. Reserved for BIOS EBDA(Extended Bios Data Area).
09A000 +------------------------+
| Command line |
| Stack/heap | 保留供内核实模式代码使用
098000 +------------------------+
| Kernel setup | 内核实模式代码
090200 +------------------------+
| Kernel boot sector | 内核传统引导扇区(legacy boot即以bios为引导的机器)
090000 +------------------------+
| Protected-mode kernel | 内核镜像主体
010000 +------------------------+
| Boot loader | <- 引导扇区入口 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中断调用来验证有多少低端内存可用。
BIOS 的 "INT 12h" 中断是控制器RAM测试的
不幸的是,如果 INT 12h 报告内存量太低,bootloader通常只能向用户报告错误。因此,bootloader 应设计为尽可能少地占用低端内存空间。对于需要将数据写入 0x90000 段的 zImage 或旧 bzImage 内核,bootloader应确保不要使用 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),然后检查引导扇区大小。
header 看起来像:
偏移量/尺寸 | Proto | Name | 意义 |
---|---|---|---|
01F1/1 | 全部(1) | setup_sects | 扇区设置的大小 |
01F2/2 | 全部 | root_flags | 如果设置,root 只读挂载 |
01F4/4 | 2.04+(2) | syssize |
16 字节参数中 32 位代码的大小 |
01F8/2 | 全部 | ram_size | 不要使用 - 仅供 bootsect.S 使用 |
01FA/2 | 全部 | vid_mode | 视频模式控制 |
01FC/2 | 全部 | root_dev | 默认根设备号 |
01FE/2 | 全部 | boot_flag | 0xAA55 幻数 |
0200/2 | 2.00+ | jump | 跳转指令 |
0202/4 | 2.00+ | header | 魔术签名 “HdrS”: header signature |
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 | 指向 struct setup_data 链表的 64 位物理指针 |
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 的偏移量 |
说明: 举例内核代码: E:\linux内核\linux-2.6.38.5\linux-2.6.38.5\arch\x86\boot\header.S 里面的代码符合这种约定 |
注意:
- 为了向后兼容,如果 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.
否则,“版本”字段包含协议版本,例如协议版本 2.01 将在此字段中包含 0x0201。在标头中设置字段时,必须确保只设置所使用的协议版本支持的字段。
1.3. Header字段的详细信息
对于每个字段,有些是内核给bootloader 的信息(“read”),有些是期望bootloader填写的(“write”),有些是期望bootloader读取和修改的(“modify” ”)。
所有通用引导加载程序都应写入标记为(必填)的字段。想要在非标准地址加载内核的引导加载程序应填写标记为 (reloc) 的字段;其他引导加载程序可以忽略这些字段。
所有字段的字节顺序都是 littleendian(毕竟这是 x86。)
字段名称: | setup_sects |
类型: | 读 |
偏移量/尺寸: | 0x1f1/1 |
协议: | 全部 |
512 字节扇区中设置代码的大小。如果此字段为 0,则实际值为 4。实模式代码由引导扇区(始终是一个 512 字节的扇区)和设置代码组成。
字段名称: | root_flags |
类型: | 修改(可选) |
偏移量/尺寸: | 0x1f2/2 |
协议: | 全部 |
如果此字段非零,则 root 默认为只读。不推荐使用该字段;请改用(内核)命令行上的“ro”或“rw”选项。
字段名称: | syssize |
类型: | 读 |
偏移量/尺寸: | 0x1f4/4(protocol 2.04+) 0x1f4/2(protocol ALL) |
协议: | 2.04+ |
以 16 字节段落为单位的保护模式代码的大小。对于早于 2.04 的协议版本,此字段只有两个字节宽,因此如果设置了 LOAD_HIGH 标志,则不能信任内核的大小。
字段名称: | ram_size |
类型: | 内核内部 |
偏移量/尺寸: | 0x1f8/2 |
协议: | 全部 |
是否在用 | 该字段已过时。 |
字段名称: | vid_mode |
类型: | 修改(强制) |
偏移量/尺寸: | 0x1fa/2 |
请参阅SPECIAL COMMAND LINE 选项部分。
字段名称: | root_dev |
类型: | 修改(可选) |
偏移量/尺寸: | 0x1fc/2 |
协议: | 全部 |
默认root设备设备号。不推荐使用此字段,而是使用(内核)命令行上的“root=”选项。
字段名称: | boot_flag |
类型: | 读 |
偏移量/尺寸: | 0x1fe/2 |
协议: | 全部 |
包含 0xAA55(mbr结束标记占据2字节)。这是旧 Linux 内核最接近神奇数字的东西。
字段名称: | jump |
类型: | 读 |
偏移量/尺寸: | 0x200/2 |
协议: | 2.00+ |
包含一条 x86 跳转指令,0xEB 后跟相对于字节 0x202 的有符号偏移量。这可用于确定header的大小。
字段名称: | header |
类型: | 读 |
偏移量/尺寸: | 0x202/4 |
协议: | 2.00+ |
包含魔幻数“HdrS”(0x53726448)。
字段名称: | version |
类型: | 读 |
偏移量/尺寸: | 0x206/2 |
协议: | 2.00+ |
包含引导协议版本,采用 (major << 8)+minor 格式,例如 0x0204 表示版本 2.04,0x0a11 表示假设版本 10.17。
字段名称: | realmode_swtch |
类型: | 修改(可选) |
偏移量/尺寸: | 0x208/4 |
协议: | 2.00+ |
引导加载程序钩子(请参阅下面的ADVANCED BOOT LOADER HOOKS 。)
字段名称: | start_sys_seg |
类型: | 读 |
偏移量/尺寸: | 0x20c/2 |
协议: | 2.00+ |
加载低段 (0x1000)。已过时的。 |
字段名称: | kernel_version |
类型: | 读 |
偏移量/尺寸: | 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 |
类型: | 写(必填) |
偏移量/尺寸: | 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(十六进制):
0 LILO(0x00 保留用于 2.00 之前的引导加载程序) 1 Loadlin 2 bootsect-loader(0x20,所有其他值保留) 3 Syslinux 4 Etherboot/gPXE/iPXE 5 ELILO 7 GRUB 8 U-Boot 9 Xen A Gujin 乙 Qemu C Arcturus Networks uCbootloader 丁 kexec-tools 乙 Extended(参见 ext_loader_type) F Special (0xFF = undefined) 10 Reserved 11 Minimal Linux Bootloader <http://sebastian-plotz.blogspot.de> 目前已经变为 https://sebastian-plotz.blogspot.com/ 12 OVMF UEFI virtualization stack 请联系 <hpa@zytor.com > 如果您需要分配引导加载程序 ID 值。
字段名称: | loadflags |
类型: | 修改(强制) |
偏移量/尺寸: | 0x211/1 |
协议: | 2.00+ |
该字段是一个位掩码。
Bit 0(读取):LOADED_HIGH
- 如果为 0,则保护模式代码加载到 0x10000。
- 如果为 1,则保护模式代码加载到 0x100000。
Bit 1(内核内部):KASLR_FLAG
由压缩内核在内部用于将 KASLR 状态传达给内核本身。
- 如果为 1,则启用 KASLR。
- 如果为 0,则禁用 KASLR。
Bit 5(写):QUIET_FLAG
如果为 0,则打印早期消息。
如果为 1,则抑制早期消息。
这要求内核(解压器和早期内核)不要写入需要直接访问显示硬件的早期消息。
Bit 6(写):KEEP_SEGMENTS
协议:2.07+
如果为 0,则重新加载 32 位入口点中的段寄存器。
如果为 1,则不重新加载 32 位入口点中的段寄存器。
假设 %cs %ds %ss %es 都设置为基数为 0(或与其环境等效)的平面段。
Bit 7(写):CAN_USE_HEAP
将此位设置为 1 以指示在 heap_end_ptr 中输入的值有效。如果清除此字段,将禁用某些设置代码功能。
字段名称: | setup_move_size |
类型: | 修改(强制) |
偏移量/尺寸: | 0x212/2 |
协议: | 2.00-2.01 |
当使用协议 2.00 或 2.01 时,如果实模式内核没有加载到 0x90000,它会在加载顺序中稍后移动到那里。如果除了实模式内核本身之外还需要移动其他数据(例如内核命令行),请填写此字段。
单位是字节,从引导扇区开始。
当协议为 2.02 或更高版本时,或者如果实模式代码加载到 0x90000,则可以忽略此字段。
字段名称: | code32_start |
类型: | 修改(可选,重定位) |
偏移量/尺寸: | 0x214/4 |
协议: | 2.00+ |
在保护模式下跳转到的地址。这默认为内核的加载地址,引导加载程序可以使用它来确定正确的加载地址。
出于两个目的可以修改此字段:
- 作为引导加载程序挂钩(请参阅下面的高级引导加载程序挂钩。)
- 如果未安装挂钩的引导加载程序在非标准地址加载可重定位内核,则它必须修改此字段以指向加载地址。
字段名称: | ramdisk_image |
类型: | 写(必填) |
偏移量/尺寸: | 0x218/4 |
协议: | 2.00+ |
初始 ramdisk 或 ramfs 的 32 位线性地址。如果没有初始 ramdisk/ramfs,则保留为零。
字段名称: | ramdisk_size |
类型: | 写(必填) |
偏移量/尺寸: | 0x21c/4 |
协议: | 2.00+ |
初始 ramdisk 或 ramfs 的大小。如果没有初始 ramdisk/ramfs,则保留为零。
字段名称: | bootsect_kludge |
类型: | 内核内部 |
偏移量/尺寸: | 0x220/4 |
协议: | 2.00+ |
该字段已过时。
字段名称: | heap_end_ptr |
类型: | 写(必填) |
偏移量/尺寸: | 0x224/2 |
协议: | 2.01+ |
将此字段设置为设置堆栈/堆末尾的偏移量(从实模式代码的开头)减去 0x0200。
字段名称: | ext_loader_ver |
类型: | 写(可选) |
偏移量/尺寸: | 0x226/1 |
协议: | 2.02+ |
该字段用作 type_of_loader 字段中版本号的扩展。总版本号被认为是 (type_of_loader & 0x0f) + (ext_loader_ver << 4)。
该字段的使用是特定于引导加载程序的。如果不写,则为零。
2.6.31 之前的内核无法识别此字段,但为协议版本 2.02 或更高版本编写是安全的。
字段名称: | ext_loader_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 |
类型: | 写(必填) |
偏移量/尺寸: | 0x228/4 |
协议: | 2.02+ |
将此字段设置为内核命令行的线性地址。内核命令行可以位于设置堆末尾和 0xA0000 之间的任何位置;它不必位于与实模式代码本身相同的 64K 段中。
即使您的引导加载程序不支持命令行,也请填写此字段,在这种情况下,您可以将其指向一个空字符串(或者更好的是,指向字符串“auto”。)如果此字段保留为零,内核将假定您的引导加载程序不支持 2.02+ 协议。
字段名称: | initrd_addr_max |
类型: | 读 |
偏移量/尺寸: | 0x22c/4 |
协议: | 2.03+ |
初始 ramdisk/ramfs 内容可能占用的最大地址。对于引导协议 2.02 或更早版本,此字段不存在,最大地址为 0x37FFFFFF。(这个地址被定义为最高安全字节的地址,所以如果你的 ramdisk 恰好是 131072 字节长并且这个字段是 0x37FFFFFF,你可以从 0x37FE0000 开始你的 ramdisk。)
字段名称: | kernel_alignment |
类型: | 读取/修改(重定位) |
偏移量/尺寸: | 0x230/4 |
协议: | 2.05+(阅读),2.10+(修改) |
内核需要的对齐单元(如果 relocatable_kernel 为 true。)以与该字段中的值不兼容的对齐方式加载的可重定位内核将在内核初始化期间重新对齐。
从协议版本 2.10 开始,这反映了为获得最佳性能而首选的内核对齐方式;加载程序可以修改此字段以允许较小的对齐。请参阅下面的 min_alignment 和 pref_address 字段。
字段名称: | relocatable_kernel |
类型: | 读取(重定位) |
偏移量/尺寸: | 0x234/1 |
协议: | 2.05+ |
如果该字段不为零,则内核的保护模式部分可以加载到满足 kernel_alignment 字段的任何地址。加载后,引导加载程序必须将 code32_start 字段设置为指向加载的代码,或引导加载程序挂钩。
字段名称: | min_alignment |
类型: | 读取(重定位) |
偏移量/尺寸: | 0x235/1 |
协议: | 2.10+ |
如果此字段非零,则表示内核启动所需的最小对齐(与首选对齐相反)的二次方。如果引导加载程序使用这个字段,它应该用所需的对齐单元更新 kernel_alignment 字段;通常:
kernel_alignment = 1 << min_alignment过度未对齐的内核可能会导致相当大的性能成本。因此,加载程序通常应尝试从 kernel_alignment 到此对齐的每个二次幂对齐。
字段名称: | xloadflags |
类型: | 读 |
偏移量/尺寸: | 0x236/2 |
协议: | 2.12+ |
该字段是一个位掩码。
Bit 0(读取):XLF_KERNEL_64
- 如果为 1,则此内核在 0x200 处具有传统的 64 位入口点。
Bit 1(读取):XLF_CAN_BE_LOADED_ABOVE_4G
- 如果为1,kernel/boot_params/cmdline/ramdisk可以在4G以上。
Bit 2(读):XLF_EFI_HANDOVER_32
- 如果为 1,则内核支持在 handover_offset 中给出的 32 位 EFI 切换入口点。
Bit 3(读取):XLF_EFI_HANDOVER_64
- 如果为 1,则内核支持在 handover_offset + 0x200 处给出的 64 位 EFI 切换入口点。
Bit 4(读):XLF_EFI_KEXEC
- 如果为 1,则内核支持具有 EFI 运行时支持的 kexec EFI 引导。
字段名称: | cmdline_size |
类型: | 读 |
偏移量/尺寸: | 0x238/4 |
协议: | 2.06+ |
没有结束0的命令行的最大大小。这意味着命令行最多可以包含 cmdline_size 个字符。对于协议版本 2.05 及更早版本,最大大小为 255。
字段名称: | hardware_subarch |
类型: | 写入(可选,默认为 x86/PC) |
偏移量/尺寸: | 0x23c/4 |
协议: | 2.07+ |
在半虚拟化环境中,中断处理、页表处理和访问进程控制寄存器等硬件低级架构部分需要以不同方式完成。
该字段允许引导加载程序通知内核我们处于其中一个环境中。
0x00000000 The default x86/PC environment 0x00000001 lguest 0x00000002 Xen 0x00000003 Moorestown MID 0x00000004 CE4100 TV Platform
字段名称: | hardware_subarch_data |
类型: | 写(依赖子目录) |
偏移量/尺寸: | 0x240/8 |
协议: | 2.07+ |
指向特定于硬件子架构的数据的指针 此字段当前未用于默认 x86/PC 环境,请勿修改。
字段名称: | payload_offset |
类型: | 读 |
偏移量/尺寸: | 0x248/4 |
协议: | 2.08+ |
如果非零,则此字段包含从保护模式代码开始到有效负载的偏移量。
有效载荷可以被压缩。压缩和未压缩数据的格式应使用标准幻数确定。当前支持的压缩格式是 gzip(幻数 1F 8B 或 1F 9E)、bzip2(幻数 42 5A)、LZMA(幻数 5D 00)、XZ(幻数 FD 37)和 LZ4(幻数 02 21)。未压缩的有效载荷目前始终是 ELF(幻数 7F 45 4C 46)。
字段名称: | payload_length |
类型: | 读 |
偏移量/尺寸: | 0x24c/4 |
协议: | 2.08+ |
有效载荷的长度。
字段名称: | setup_data |
类型: | 写(特殊) |
偏移量/尺寸: | 0x250/8 |
协议: | 2.09+ |
指向 NULL 的 64 位物理指针终止了 struct setup_data 的单链表。这用于定义更具扩展性的引导参数传递机制。结构体 setup_data 的定义如下:
struct setup_data { u64 next; u32 type; u32 len; u8 data[0]; };
其中,next是指向链表下一个节点的64位物理指针,最后一个节点的next字段为0;类型用于标识数据的内容;len是数据字段的长度;数据包含真正的有效载荷。
此列表可能会在启动过程中的多个点进行修改。因此,在修改此列表时,应始终确保考虑链表已包含条目的情况。
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; };
类型成员是 SETUP_INDIRECT | SETUP_* 类型。但是,它不能是 SETUP_INDIRECT 自己本身,因为使 setup_indirect 成为树结构可能需要大量堆栈空间来解析它,并且堆栈空间在引导上下文中可能会受到限制。
让我们举例说明如何使用 setup_indirect 指向 SETUP_E820_EXT 数据。在这种情况下,setup_data 和 setup_indirect 将如下所示:
struct setup_data { __u64 next = 0 or <addr_of_next_setup_data_struct>; __u32 type = SETUP_INDIRECT; __u32 len = sizeof(setup_data); __u8 data[sizeof(setup_indirect)] = struct setup_indirect { __u32 type = SETUP_INDIRECT | SETUP_E820_EXT; __u32 reserved = 0; __u64 len = <len_of_SETUP_E820_EXT_data>; __u64 addr = <addr_of_SETUP_E820_EXT_data>; } }
注意:
SETUP_INDIRECT | SETUP_NONE 对象无法与 SETUP_INDIRECT 本身正确区分。因此,引导加载程序无法提供此类对象。
字段名称: | pref_address |
类型: | 读取(重定位) |
偏移量/尺寸: | 0x258/8 |
协议: | 2.10+ |
如果该字段非零,则表示内核的首选加载地址。如果可能,重新定位的引导加载程序应尝试加载到该地址。
不可重定位内核将无条件地移动自身并运行在该地址。
字段名称: | init_size |
类型: | 读 |
偏移量/尺寸: | 0x260/4 |
该字段指示从内核运行时起始地址开始的线性连续内存量,内核在能够检查其内存映射之前需要这些内存。这与内核启动所需的内存总量不同,但重定位引导加载程序可以使用它来帮助为内核选择安全的加载地址。
内核运行时起始地址由以下算法确定:
if (relocatable_kernel) runtime_start = align_up(load_address, kernel_alignment) else runtime_start = pref_address
字段名称: | handover_offset |
类型: | 读 |
偏移量/尺寸: | 0x264/4 |
该字段是从内核映像开始到 EFI 切换协议入口点的偏移量。使用 EFI 切换协议引导内核的引导加载程序应跳转到此偏移量。
有关详细信息,请参阅下面的 EFI 交接协议。
字段名称: | kernel_info_offset |
类型: | 读 |
偏移量/尺寸: | 0x268/4 |
协议: | 2.15+ |
该字段是从内核镜像开始到 kernel_info 的偏移量。kernel_info 结构嵌入在未压缩的保护模式区域中的 Linux 镜像中。
1.4. 内核信息
headers之间的关系类似于各种数据部分:
setup_header = .data boot_params/setup_data = .bss
上面的列表中缺少什么?这是正确的:
kernel_info = .rodata
长期以来,我们一直(滥用)将 .data 用于可以进入 .rodata 或 .bss 的内容,因为缺乏替代方案,而且——尤其是在早期——惯性。此外,BIOS stub 负责创建 boot_params,因此它不可用于基于 BIOS 的加载器(但 setup_data 是)。
由于 2 字节跳转字段的范围,setup_header 永久限制为 144 字节,该字段兼作结构的长度字段,结合保护模式加载程序或 BIOS 的 struct boot_params 中“hole”的大小 stub 必须将其复制到。它目前有 119 个字节长,这给我们留下了 25 个非常宝贵的字节。如果不完全修改启动协议,这会破坏向后兼容性,这是无法解决的问题。
boot_params 本身限制为 4096 字节,但可以通过添加 setup_data 条目任意扩展。它不能用于传达内核映像的属性,因为它是 .bss 并且没有镜像提供的内容。
kernel_info 通过为有关内核镜像的信息提供一个可扩展的位置来解决这个问题。它是只读的,因为内核不能依赖bootloader将其内容复制到任何地方,但这没关系;如果有必要,它仍然可以包含启用的bootloader将被期望复制到 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. 镜像 Checksum
从引导协议版本 2.08 开始,使用特征多项式 0x04C11DB7 和初始余数 0xffffffff 在整个文件上计算 CRC-32。checksum 附加到文件;因此,文件的 CRC 直到标头的 syssize 字段中指定的限制始终为 0。
1.7. 内核命令行
内核命令行已经成为bootloader与kernel通信的重要方式。它的一些选项也与bootloader本身有关,请参阅下面的“special command line options”。
内核命令行是一个以 null 结尾的字符串。可以从字段 cmdline_size 中检索最大长度。在协议版本 2.06 之前,最大值为 255 个字符。太长的字符串会被内核自动截断。
如果引导协议版本为 2.02 或更高版本,则内核命令行的地址由header字段 cmd_line_ptr 给出(见上文)。该地址可以是设置堆末尾和 0xA0000 之间的任何位置。
如果协议版本不是2.02或更高版本,则使用以下协议进入kernel command line :
- 在偏移量 0x0020(字)“cmd_line_magic”处,输入魔幻数 0xA33F。
- 在偏移量 0x0022(字)“cmd_line_offset”处,输入内核命令行的偏移量(相对于实模式内核的开始)。
- 内核命令行必须在 setup_move_size 覆盖的内存区域内,因此您可能需要调整此字段。
1.8. 实模式代码的内存布局
实模式代码需要设置堆栈/堆,以及为内核命令行分配内存。这需要在低mb字节的实模式可访问内存中完成。
应该注意的是,现代机器通常具有相当大的EBDA(扩展 BIOS 数据区 )。因此,建议尽可能少地使用低megabytes(mb)字节。
不幸的是,在以下情况下必须使用 0x90000 内存段:
- 加载 zImage 内核时 ((loadflags & 0x01) == 0)。
- 加载 2.01 或更早版本的引导协议内核时。
注意:
对于 2.00 和 2.01 引导协议,实模式代码可以加载到另一个地址,但它在内部重定位到 0x90000。对于“旧”协议,实模式代码必须加载到 0x90000。
在 0x90000 加载时,避免使用 0x9a000 以上的内存。
对于引导协议 2.02 或更高版本,命令行不必位于与实模式设置代码相同的 64K 段;因此允许为堆栈/堆提供完整的 64K 段并将命令行定位在其上方。
内核命令行不应位于实模式代码下方,也不应位于高端内存中。
1.9. 引导配置示例
作为示例配置,假设实模式段的以下布局。
在 0x90000 以下加载时,使用整个段:
0x0000-0x7fff 实模式内核(Real mode kernel) 0x8000-0xdfff 堆栈和堆(Stack and heap) 0xe000-0xffff 内核命令行(Kernel command line) 在 0x90000 处加载或协议版本为 2.01 或更早时:
0x0000-0x7fff 实模式内核 0x8000-0x97ff 堆栈和堆 0x9800-0x9fff 内核命令行
这样的引导加载程序应该在header中输入以下字段:
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.10. 加载内核的其余部分
32 位(非实模式)内核从内核文件中的偏移量 (setup_sects+1)*512 开始(同样,如果 setup_sects == 0,则实际值为 4。)它应该在地址 0x10000 加载镜像/zImage 内核和 0x100000 用于 bzImage 内核。
如果协议 >= 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.11. 特殊命令行选项
如果引导加载程序提供的命令行由用户输入,则用户可能希望以下命令行选项起作用。它们通常不应从内核命令行中删除,即使并非所有这些对内核都真正有意义。引导加载程序本身需要额外命令行选项的引导加载程序作者应该在 Documentation/admin-guide/kernel-parameters.rst 中注册它们,以确保它们现在或将来不会与实际内核选项冲突。
- vga=<模式>
- <mode> 是一个整数(C 表示法,十进制、八进制或十六进制)或字符串“normal”(表示 0xFFFF)、“ext”(表示 0xFFFE)或“ask”(表示 0xFFFD)之一。这个值应该输入到 vid_mode 字段中,因为它在解析命令行之前由内核使用。
- 内存=<大小>
- <size> 是 C 表示法中的整数,可选地后跟(不区分大小写)K、M、G、T、P 或 E(意思是 << 10、<< 20、<< 30、<< 40、<< 50 或 < < 60)。这指定内核的内存结束。这会影响 initrd 的可能放置位置,因为 initrd 应该放置在靠近内存末尾的位置。请注意,这是内核和引导加载程序的一个选项!
- initrd=<文件>
- 应该加载一个 initrd。<file>的意思显然是bootloader-dependent,有些boot loader(如LILO)没有这样的命令。
此外,一些引导加载程序将以下选项添加到用户指定的命令行:
- BOOT_IMAGE=<文件>
- 加载的引导镜像。同样,<file> 的含义显然取决于引导加载程序。
- auto
- 内核在没有明确的用户干预的情况下启动。
如果引导加载程序添加了这些选项,强烈建议将它们放在首位,在用户指定或配置指定的命令行之前。否则,“init=/bin/sh”会被“auto”选项混淆。
1.12. 运行内核
内核通过跳转到内核入口点启动,该入口点位于距实模式内核开始的段偏移量 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.13. 高级引导加载程序钩子
如果bootloader在特别恶劣的环境中运行(例如在 DOS 下运行的 LOADLIN),则可能无法遵循标准内存位置要求。这样的引导加载程序可以使用以下钩子,如果设置了这些钩子,内核将在适当的时间调用这些钩子。这些钩子的使用应该被认为是绝对不得已的手段!
重要提示:所有挂钩都需要在调用过程中保留 %esp、%ebp、%esi 和 %edi。
- realmode_swtch:
- 在进入保护模式之前立即调用的 16 位实模式 far 子例程。默认例程禁用 NMI,因此您的例程也应该这样做。
- code32_start:
一个 32 位平面模式例程在转换到保护模式后立即跳转到,但在内核未压缩之前。除了 CS 之外,没有任何段可以保证被设置(当前内核可以,但旧内核不会);您应该自己将它们设置为 BOOT_DS (0x18)。
完成钩子后,您应该跳转到该字段中的地址,然后引导加载程序覆盖它(如果合适,重新定位)。
1.14. 32 位引导协议
对于一些非 legacy BIOS 的新BIOS的机器,如EFI、LinuxBIOS(又名Coreboot)等,以及kexec,不能使用基于legacy BIOS的内核中的16位实模式设置代码,因此需要一个32位的启动协议被定义。
在 32 位启动协议中,加载 Linux 内核的第一步应该是设置启动参数(struct boot_params,传统上称为“zero page”)。struct boot_params 的内存应该被分配并初始化为全零。然后应该将来自内核映像偏移量 0x01f1 的设置header加载到 struct boot_params 中并进行检查。setup header 的结束可以计算如下:
0x0202 + byte value at offset 0x0201
除了读取/修改/写入 struct boot_params 的设置header作为 16 位引导协议的设置header外,引导加载程序还应填充 struct boot_params 的附加字段,如 zero-page.txt 中所述。
设置好struct boot_params后,bootloader可以像16位引导协议一样加载32/64位内核。
在 32 位启动协议中,内核是通过跳转到 32 位内核入口点来启动的,该入口点是加载的 32/64 位内核的起始地址。
进入时,CPU 必须处于禁用分页的 32 位保护模式;GDT 必须加载选择器 __BOOT_CS(0x10) 和 __BOOT_DS(0x18) 的描述符;两个描述符都必须是 4G flat 段;__BOOT_CS必须有执行/读权限,__BOOT_DS必须有读/写权限;CS必须是__BOOT_CS,DS、ES、SS必须是__BOOT_DS;必须禁用中断;%esi 必须保存 struct boot_params 的基址;%ebp、%edi 和 %ebx 必须为零。
1.15. 64 位引导协议
对于具有 64 位 CPU 和 64 位内核的机器,我们可以使用 64 位引导加载程序,我们需要一个 64 位引导协议。
在 64 位启动协议中,加载 Linux 内核的第一步应该是设置启动参数(struct boot_params,传统上称为“zero page”)。struct boot_params 的内存可以在任何地方分配(甚至超过 4G)并初始化为全零。然后,应该将位于内核映像偏移量 0x01f1 处的设置标头加载到 struct boot_params 中并进行检查。setup header 的结束可以计算如下:
0x0202 + byte value at offset 0x0201
除了读取/修改/写入 struct boot_params 的设置标头作为 16 位引导协议的设置标头外,引导加载程序还应填充 struct boot_params 的附加字段,如 zero-page.txt 中所述。
设置好struct boot_params后,boot loader可以像16位boot protocol一样加载64位内核,但是内核可以加载到4G以上。
在64位启动协议中,内核是通过跳转到64位内核入口点来启动的,该入口点是加载的64位内核的起始地址加上0x200。
进入时,CPU 必须处于启用分页的 64 位模式。setup_header.init_size 从加载内核的起始地址和零页和命令行缓冲区获取标识映射的范围;GDT 必须加载选择器 __BOOT_CS(0x10) 和 __BOOT_DS(0x18) 的描述符;两个描述符都必须是 4G flat 段;__BOOT_CS必须有执行/读权限,__BOOT_DS必须有读/写权限;CS必须是__BOOT_CS,DS、ES、SS必须是__BOOT_DS;必须禁用中断;%rsi 必须保存 struct boot_params 的基址。
1.16. EFI 切换协议
该协议允许引导加载程序将初始化推迟到 EFI 引导存根。引导加载程序需要从引导介质加载内核/initrd(s) 并跳转到 EFI 切换协议入口点,即从 startup_{32,64} 开始的 hdr->handover_offset 字节。
切换入口点的函数原型如下所示:
efi_main(void *handle, efi_system_table_t *table, struct boot_params *bp)
'handle' 是由 EFI 固件传递给引导加载程序的 EFI 映像句柄,'table' 是 EFI 系统表 - 这些是 UEFI 规范第 2.3 节中描述的“切换状态”的前两个参数。'bp' 是引导加载程序分配的引导参数。
引导加载程序必须填写 bp 中的以下字段:
- hdr.code32_start - hdr.cmd_line_ptr - hdr.ramdisk_image (if applicable) - hdr.ramdisk_size (if applicable)
所有其他字段应为零。
https://www.kernel.org/doc/html/v5.6/x86/boot.html
http://lxr.linux.no/linux+v2.6.25.6/Documentation/i386/boot.txt