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 或 32 位 BIOS 入口点的 EBDA 的系统来说是安全的。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) 添加了半虚拟化启动协议。引入了 hardware_subarch 和 hardware_subarch_data 以及 load_flags 中的 KEEP_SEGMENTS 标志。

协议 2.08

(内核 2.6.26) 添加了 crc32 校验和和 ELF 格式有效负载。引入了 payload_offset 和 payload_length 字段以帮助定位有效负载。

协议 2.09

(内核 2.6.26) 添加了一个 64 位物理指针字段,指向 struct setup_data 的单链表。

协议 2.10

(内核 2.6.31) 添加了一个协议,用于放宽超出添加的 kernel_alignment 的对齐方式,新的 init_size 和 pref_address 字段。添加了扩展启动加载器 ID。

协议 2.11

(内核 3.6) 添加了一个 EFI 交接协议入口点偏移量的字段。

协议 2.12

(内核 3.8) 添加了 xloadflags 字段和 struct boot_params 的扩展字段,用于在 64 位中加载 4G 以上的 bzImage 和 ramdisk。

协议 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。

注意

只有在更改了设置头时才应更改协议版本号。如果更改了 boot_params 或 kernel_info,则无需更新版本号。 此外,建议使用 xloadflags(在这种情况下,也不应更新协议版本号)或 kernel_info 将受支持的 Linux 内核特性传达给启动加载器。 由于原始设置头中可用的空间非常有限,因此应谨慎考虑对其进行的每次更新。 从协议 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

全部 (1)

setup_sects

设置在扇区中的大小

01F2/2

全部

root_flags

如果设置,则以只读方式挂载根

01F4/4

2.04+(2)

syssize

32 位代码的大小,单位为 16 字节段落

01F8/2

全部

ram_size

请勿使用 - 仅用于 bootsect.S

01FA/2

全部

vid_mode

视频模式控制

01FC/2

全部

root_dev

默认根设备号

01FE/2

全部

boot_flag

0xAA55 魔数

0200/2

2.00+

跳跃

跳转指令

0202/4

2.00+

标头

魔术签名“HdrS”

0206/2

2.00+

版本

支持的启动协议版本

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 的偏移量

注意

  1. 为了向后兼容,如果 setup_sects 字段包含 0,则实际值为 4。

  2. 对于早于 2.04 的启动协议,syssize 字段的高两位字节不可用,这意味着如果设置了 LOAD_HIGH 标志,则无法确定 bzImage 内核的大小。

  3. 忽略,但可以安全设置,用于启动协议 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

类型

读取

偏移量/大小

0x1f1/1

协议

全部

设置代码的大小,单位为 512 字节扇区。如果此字段为 0,则实际值为 4。实模式代码由引导扇区(始终为一个 512 字节扇区)加上设置代码组成。

字段名称

root_flags

类型

修改(可选)

偏移量/大小

0x1f2/2

协议

全部

如果此字段为非零值,则根默认设置为只读。 不建议使用此字段; 请改用命令行上的“ro”或“rw”选项。

字段名称

syssize

类型

读取

偏移量/大小

0x1f4/4 (协议 2.04+) 0x1f4/2 (所有协议)

协议

2.04+

保护模式代码的大小,单位为 16 字节段落。 对于早于 2.04 的协议版本,此字段仅为两个字节宽,因此,如果设置了 LOAD_HIGH 标志,则不能信任内核的大小。

字段名称

ram_size

类型

内核内部

偏移量/大小

0x1f8/2

协议

全部

此字段已过时。

字段名称

vid_mode

类型

修改(强制性)

偏移量/大小

0x1fa/2

请参阅有关特殊命令行选项的部分。

字段名称

root_dev

类型

修改(可选)

偏移量/大小

0x1fc/2

协议

全部

默认根设备号。 不建议使用此字段,请改用命令行上的“root=”选项。

字段名称

boot_flag

类型

读取

偏移量/大小

0x1fe/2

协议

全部

包含 0xAA55。 这是旧 Linux 内核所拥有的最接近魔数的东西。

字段名称

跳跃

类型

读取

偏移量/大小

0x200/2

协议

2.00+

包含一个 x86 跳转指令,0xEB 后跟一个相对于字节 0x202 的带符号偏移量。 这可用于确定标头的大小。

字段名称

标头

类型

读取

偏移量/大小

0x202/4

协议

2.00+

包含魔数“HdrS” (0x53726448)。

字段名称

版本

类型

读取

偏移量/大小

0x206/2

协议

2.00+

包含启动协议版本,格式为(主版本 << 8) + 次版本,例如,版本 2.04 为 0x0204,假设版本 10.17 则为 0x0a11。

字段名称

realmode_swtch

类型

修改(可选)

偏移量/大小

0x208/4

协议

2.00+

启动加载器钩子(请参见下面的高级启动加载器钩子。)

字段名称

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 保留给 pre-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

B

Qemu

C

Arcturus Networks uCbootloader

D

kexec-tools

E

扩展(请参见 ext_loader_type)

F

特殊 (0xFF = 未定义)

10

已保留

11

Minimal Linux Bootloader <http://sebastian-plotz.blogspot.de>

12

OVMF UEFI 虚拟化堆栈

13

barebox

如果您需要分配启动加载器 ID 值,请联系 <hpa@zytor.com>。

字段名称

loadflags

类型

修改(强制性)

偏移量/大小

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 中输入的值有效。 如果清除此字段,则将禁用某些设置代码功能。

字段名称

setup_move_size

类型

修改(强制性)

偏移量/大小

0x212/2

协议

2.00-2.01

使用协议 2.00 或 2.01 时,如果实模式内核未加载到 0x90000,则稍后将在加载序列中将其移动到此处。 如果除了实模式内核本身之外,你还想移动其他数据(例如内核命令行),请填写此字段。

该单位是从引导扇区开始的字节。

当协议为 2.02 或更高版本,或者实模式代码加载到 0x90000 时,可以忽略此字段。

字段名称

code32_start

类型

修改(可选,重定位)

偏移量/大小

0x214/4

协议

2.00+

要跳转到的保护模式地址。 这默认为内核的加载地址,引导加载器可以使用它来确定正确的加载地址。

可以修改此字段以实现两个目的

  1. 作为启动加载器钩子(请参见下面的高级启动加载器钩子。)

  2. 如果未安装钩子的启动加载器以非标准地址加载可重定位内核,则它将必须修改此字段以指向加载地址。

字段名称

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+

将此字段设置为 setup 堆栈/堆结束的偏移量(从实模式代码的开头算起),减去 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+

将此字段设置为内核命令行的线性地址。内核命令行可以位于 setup 堆的末尾和 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 为真)。如果以与此字段中的值不兼容的对齐方式加载可重定位内核,则将在内核初始化期间重新对齐。

从协议版本 2.10 开始,这反映了内核为获得最佳性能而首选的对齐方式;加载程序可以修改此字段以允许较小的对齐方式。请参阅下面的 min_alignment 和 pref_address 字段。

字段名称

relocatable_kernel

类型

读取(重定位)

偏移量/大小

0x234/1

协议

2.05+

如果此字段非零,则可以在满足 kernel_alignment 字段的任何地址加载内核的保护模式部分。加载后,引导加载程序必须设置 code32_start 字段以指向加载的代码,或指向引导加载程序钩子。

字段名称

min_alignment

类型

读取(重定位)

偏移量/大小

0x235/1

协议

2.10+

如果此字段非零,则以 2 的幂表示内核启动所需的最小对齐方式,而不是首选的对齐方式。如果引导加载程序使用此字段,则应使用所需的对齐单元更新 kernel_alignment 字段;通常

kernel_alignment = 1 << min_alignment;

内核过度未对齐可能会导致相当大的性能成本。因此,加载程序通常应尝试从 kernel_alignment 到此对齐方式的每个 2 的幂的对齐方式。

字段名称

xloadflags

类型

读取

偏移量/大小

0x236/2

协议

2.12+

此字段是一个位掩码。

位 0(读取):XLF_KERNEL_64

  • 如果为 1,则此内核在 0x200 处具有传统的 64 位入口点。

位 1(读取):XLF_CAN_BE_LOADED_ABOVE_4G

  • 如果为 1,则 kernel/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

类型

读取

偏移量/大小

0x238/4

协议

2.06+

不带终止零的命令行的最大大小。这意味着命令行最多可以包含 cmdline_size 个字符。在协议版本 2.05 及更早版本中,最大大小为 255。

字段名称

hardware_subarch

类型

写入(可选,默认为 x86/PC)

偏移量/大小

0x23c/4

协议

2.07+

在半虚拟化环境中,硬件低级架构组件(例如中断处理、页表处理和访问进程控制寄存器)需要以不同的方式完成。

此字段允许引导加载程序通知内核我们处于其中一个环境中。

0x00000000

默认的 x86/PC 环境

0x00000001

lguest

0x00000002

Xen

0x00000003

Intel MID(Moorestown、CloverTrail、Merrifield、Moorefield)

0x00000004

CE4100 TV 平台

字段名称

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)和 ZSTD(魔数 28 B5)。未压缩的有效负载当前始终为 ELF(魔数 7F 45 4C 46)。

字段名称

payload_length

类型

读取

偏移量/大小

0x24c/4

协议

2.08+

有效负载的长度。

字段名称

setup_data

类型

写入(特殊)

偏移量/大小

0x250/8

协议

2.09+

指向 NULL 终止的 struct setup_data 单链表的 64 位物理指针。这用于定义更可扩展的引导参数传递机制。struct setup_data 的定义如下

struct setup_data {
     __u64 next;
     __u32 type;
     __u32 len;
     __u8 data[];
}

其中,next 是指向链表下一个节点的 64 位物理指针,最后一个节点的 next 字段为 0;type 用于标识数据的 内容;len 是数据字段的长度;数据保存实际有效负载。

此列表可能会在启动过程中的多个点被修改。因此,在修改此列表时,应始终确保考虑链表已包含条目的情况。

setup_data 对于非常大的数据对象来说使用起来有点笨拙,既因为 setup_data 标头必须与数据对象相邻,又因为它具有 32 位长度字段。然而,重要的是引导过程的中间阶段有一种方法来识别哪些内存块被内核数据占用。

因此,协议 2.15 中引入了 setup_indirect struct 和 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

类型

读取(重定位)

偏移量/大小

0x258/8

协议

2.10+

如果此字段非零,则表示内核的首选加载地址。如果可能,重定位引导加载程序应尝试在此地址加载。

不可重定位内核将无条件地移动自身并在该地址运行。可重定位内核会将自身移动到此地址,如果它加载到低于此地址的位置。

字段名称

init_size

类型

读取

偏移量/大小

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

类型

读取

偏移量/大小

0x264/4

此字段是从内核镜像的开头到 EFI 切换协议入口点的偏移量。使用 EFI 切换协议启动内核的引导加载程序应跳转到此偏移量。

有关更多详细信息,请参阅下面的 EFI 切换协议。

字段名称

kernel_info_offset

类型

读取

偏移量/大小

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 stub 负责创建 boot_params,因此 BIOS 的加载程序无法使用它(但是可以使用 setup_data)。

由于 2 字节跳转字段的范围(该字段兼作结构的长度字段)与 struct boot_params 中“孔”的大小相结合,保护模式加载程序或 BIOS stub 必须将其复制到该孔中,因此 setup_header 永久限制为 144 字节。目前它的长度是 119 字节,这给我们留下了 25 个非常宝贵的字节。如果不完全修改引导协议,破坏向后兼容性,就无法解决这个问题。

boot_params 本身限制为 4096 字节,但可以通过添加 setup_data 条目来任意扩展。它不能用于传达内核镜像的属性,因为它是 .bss 并且没有镜像提供的内容。

kernel_info 通过为内核镜像信息提供可扩展的位置来解决这个问题。它是只读的,因为内核无法依赖引导加载程序将其内容复制到任何地方,但这没关系;如果需要,它仍然可以包含已启用的引导加载程序应复制到 setup_data 块中的数据项。

所有 kernel_info 数据都应是此结构的一部分。固定大小的数据必须放在 kernel_info_var_len_data 标签之前。可变大小的数据必须放在 kernel_info_var_len_data 标签之后。每个可变大小的数据块必须以标头/魔术和它的大小作为前缀,例如

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 字段的详细信息

字段名称

标头

偏移量/大小

0x0000/4

包含魔数“LToP”(0x506f544c)。

字段名称

大小

偏移量/大小

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 给出(参见上文。)此地址可以位于 setup 堆的末尾和 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 或更高版本,命令行不必与实模式 setup 代码位于同一 64K 段中;因此允许为堆栈/堆提供完整的 64K 段,并将命令行定位在其上方。

内核命令行不应位于实模式代码下方,也不应位于高位内存中。

1.8. 示例引导配置

作为一个示例配置,假设实模式段的以下布局。

在 0x90000 以下加载时,使用整个段

0x0000-0x7fff

实模式内核

0x8000-0xdfff

堆栈和堆

0xe000-0xffff

内核命令行

在 0x90000 加载时 OR 协议版本为 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=<mode>

这里的 <mode> 是一个整数(以 C 表示法,可以是十进制、八进制或十六进制)或字符串“normal”(表示 0xFFFF)、“ext”(表示 0xFFFE)或“ask”(表示 0xFFFD)之一。此值应输入到 vid_mode 字段中,因为它在解析命令行之前被内核使用。

mem=<size>

<size> 是一个以 C 表示法的整数,可以选择后跟(不区分大小写)K、M、G、T、P 或 E(表示 << 10、<< 20、<< 30、<< 40、<< 50 或 << 60)。这指定了内核的内存结束位置。这会影响 initrd 的可能位置,因为 initrd 应放置在靠近内存结束位置。请注意,这是内核和引导加载程序的选项!

initrd=<file>

应加载 initrd。<file> 的含义显然取决于引导加载程序,并且一些引导加载程序(例如 LILO)没有这样的命令。

此外,一些引导加载程序将以下选项添加到用户指定的命令行

BOOT_IMAGE=<file>

已加载的引导镜像。同样,<file> 的含义显然取决于引导加载程序。

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. 高级引导加载程序钩子

如果引导加载程序在特别恶劣的环境中运行(例如 LOADLIN,它在 DOS 下运行),则可能无法遵循标准的内存位置要求。这样的引导加载程序可以使用以下钩子,如果设置了这些钩子,则内核会在适当的时间调用它们。这些钩子的使用可能应该被认为是绝对的最后手段!

重要提示:所有钩子都需要在调用过程中保留 %esp、%ebp、%esi 和 %edi。

realmode_swtch

在进入保护模式之前立即调用的 16 位实模式远子例程。默认例程禁用 NMI,因此您的例程可能也应该这样做。

code32_start

在转换为保护模式后,但在内核解压缩之前立即跳转到的 32 位扁平模式例程。除 CS 外,不保证设置任何段(当前内核会这样做,但较旧的内核不会);您应该自己将它们设置为 BOOT_DS (0x18)。

完成钩子后,您应该跳转到您的引导加载程序覆盖它之前此字段中的地址(如果合适,则重新定位)。

1.13. 32 位引导协议

对于具有传统 BIOS 以外的一些新 BIOS 的机器,例如 EFI、LinuxBIOS 等以及 kexec,无法使用基于传统 BIOS 的内核中的 16 位实模式 setup 代码,因此需要定义 32 位引导协议。

在 32 位引导协议中,加载 Linux 内核的第一步应该是设置引导参数(struct boot_params,传统上称为“零页面”)。应分配 struct boot_params 的内存并将其初始化为全零。然后,应将内核镜像偏移量 0x01f1 处的 setup 标头加载到 struct boot_params 中并进行检查。setup 标头的结尾可以计算如下

0x0202 + byte value at offset 0x0201

除了像 16 位引导协议那样读取/修改/写入 struct boot_params 的 setup 标头之外,引导加载程序还应填写 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 处的 setup 标头加载到 struct boot_params 中并进行检查。setup 标头的结尾可以计算如下

0x0202 + byte value at offset 0x0201

除了像 16 位引导协议那样读取/修改/写入 struct boot_params 的 setup 标头之外,引导加载程序还应填写 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 引导 stub。引导加载程序需要从引导介质加载内核/initrd 并跳转到 EFI 切换协议入口点,该入口点是从 startup_{32,64} 开头偏移 hdr->handover_offset 个字节的位置。

在涉及节对齐、可执行镜像在文件本身大小之外的内存占用以及可能影响镜像作为 EFI 固件提供的执行上下文中的 PE/COFF 二进制文件正确操作的 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 入口点,以及基于 LINUX_EFI_INITRD_MEDIA_GUID 的 initrd 加载协议(有关这方面的引导加载程序示例,请参阅 [0]),它消除了 EFI 引导加载程序了解 boot_params 的内部表示或对命令行和 ramdisk 在内存中的位置或内核镜像本身的位置的任何要求/限制的需要。

[0] https://github.com/u-boot/u-boot/commit/ec80b4735a593961fe701cc3a5d717d4739b0fd0