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) 添加了一个 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) 为在 64 位中加载 4G 以上的 bzImage 和 ramdisk 添加了 xloadflags 字段和 struct boot_params 的扩展字段。

协议 2.13

(内核 3.14) 支持在 xloadflags 中设置 32 位和 64 位标志,以支持从 32 位 EFI 引导 64 位内核

协议 2.14

由于不正确的 COMMIT ae7e1238e68f2a472a125673ab506d49158c1889 (“x86/boot: 将 ACPI RSDP 地址添加到 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+

jump

跳转指令

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

指向 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 字段的较高两个字节不可用,这意味着无法确定 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 内核最接近魔数的东西。

字段名称

jump

类型

读取

偏移/大小

0x200/2

协议

2.00+

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

字段名称

header

类型

读取

偏移/大小

0x202/4

协议

2.00+

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

字段名称

version

类型

读取

偏移/大小

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+

如果设置为非零值,则包含指向以 NULL 结尾的人类可读内核版本号字符串的指针,减去 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

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+

将此字段设置为设置堆栈/堆结尾的偏移量(从实模式代码的开头开始),减去 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 为真)。在与此字段中的值不兼容的对齐方式加载的可重定位内核将在内核初始化期间进行重新对齐。

从协议版本 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,则内核/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

Moorestown MID

0x00000004

CE4100 电视平台

字段名称

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+

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

struct setup_data {
        u64 next;
        u32 type;
        u32 len;
        u8  data[0];
};

其中,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 {
  __u64 next = 0 or <addr_of_next_setup_data_struct>;
  __u32 type = SETUP_INDIRECT;
  __u32 len = sizeof(setup_indirect);
  __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) {
        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 存根负责创建 `boot_params`,因此基于 BIOS 的加载程序无法使用它(但可以使用 `setup_data`)。

`setup_header` 永久限制为 144 字节,这是由于 2 字节跳转字段的范围(该字段兼作结构的长度字段),以及保护模式加载程序或 BIOS 存根必须将其复制到的 `struct boot_params` 中的“空洞”大小共同决定的。它目前长 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` 字段的详细信息

字段名称

header

偏移/大小

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` 的开头。

字段名称

总大小

偏移/大小

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. 映像校验和

从引导协议 2.08 版本开始,使用特征多项式 0x04C11DB7 和初始余数 0xffffffff 对整个文件计算 CRC-32。校验和附加到文件的末尾;因此,直到标头 `syssize` 字段中指定的限制的文件 CRC 始终为 0。

1.7. 内核命令行

内核命令行已成为引导加载程序与内核通信的重要方式。它的一些选项也与引导加载程序本身相关,请参阅下面的“特殊命令行选项”。

内核命令行是一个以 null 结尾的字符串。最大长度可以从字段 `cmdline_size` 中检索。在协议版本 2.06 之前,最大长度为 255 个字符。太长的字符串将被内核自动截断。

如果引导协议版本为 2.02 或更高版本,则内核命令行的地址由标头字段 `cmd_line_ptr` 给出(参见上文)。此地址可以在设置堆的末尾和 0xA0000 之间的任何位置。

如果协议版本不是 2.02 或更高版本,则使用以下协议输入内核命令行:

  • 在偏移量 0x0020 (word) 处,即 “`cmd_line_magic`”,输入魔术数字 0xA33F。

  • 在偏移量 0x0022 (word) 处,即 “`cmd_line_offset`”,输入内核命令行的偏移量(相对于实模式内核的开头)。

  • 内核命令行必须在 `setup_move_size` 覆盖的内存区域内,因此你可能需要调整此字段。

1.8. 实模式代码的内存布局

实模式代码需要设置堆栈/堆,以及为内核命令行分配的内存。这需要在底部兆字节的实模式可访问内存中完成。

应该注意的是,现代机器通常具有相当大的扩展 BIOS 数据区 (EBDA)。因此,建议尽可能少地使用低兆字节。

不幸的是,在以下情况下,必须使用 0x90000 内存段:

  • 加载 `zImage` 内核时 (`(loadflags & 0x01) == 0`)。

  • 加载 2.01 或更早版本的引导协议内核时。

注意

对于 2.00 和 2.01 引导协议,实模式代码可以加载到另一个地址,但它会在内部重新定位到 0x90000。对于“旧”协议,实模式代码必须加载到 0x90000。

在 0x90000 加载时,请避免使用 0x9a000 以上的内存。

对于 2.02 或更高版本的引导协议,命令行不必与实模式设置代码位于相同的 64K 段中;因此,允许为堆栈/堆提供完整的 64K 段,并将命令行放置在其上方。

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

1.9. 示例引导配置

作为示例配置,假设实模式段的布局如下:

在 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.10. 加载内核的其余部分

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.11. 特殊命令行选项

如果引导加载程序提供的命令行是由用户输入的,则用户可能希望以下命令行选项起作用。它们通常不应从内核命令行中删除,即使并非所有选项对内核都真正有意义。引导加载程序的作者如果需要引导加载程序本身的其他命令行选项,应在 内核的命令行参数 中注册它们,以确保它们现在或将来不会与实际的内核选项冲突。

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

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

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

realmode_swtch

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

code32_start

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

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

1.14. 32 位引导协议

对于具有一些非传统 BIOS 的新 BIOS 的机器,例如 EFI、LinuxBIOS 等,以及 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.15. 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.16. EFI 切换协议(已弃用)

此协议允许引导加载程序将初始化推迟到 EFI 引导存根。引导加载程序需要从引导介质加载内核/initrd,并跳转到 EFI 切换协议入口点,该入口点位于从 startup_{32,64} 的开头偏移 hdr->handover_offset 字节的位置。

引导加载程序必须在节对齐、可执行映像在文件大小之外的内存占用,以及 PE/COFF 标头中可能影响映像作为 EFI 固件提供的执行上下文中 PE/COFF 二进制文件的正确操作的任何其他方面,都要遵守内核的 PE/COFF 元数据。

切换入口点的函数原型如下所示:

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