Linux内核引导

以下笔记来自阅读:https://www.kernel.org/doc/html/latest/x86/boot.html


Linux/x86 引导协议

    在x86平台上,Linux使用一个很复杂的引导协议。形成这种状况的原因有很多,包括:各种历史原因、早期期望内核本身就是一个可以启动的 image、复杂的PC内存模型、以及实模式消亡后PC工业界的变化等等。

    目前,存在如下版本的 Linux/x86 引导协议:

版本 说明
老内核 只支持 zImage/Imgge。有些早期内核甚至不支持命令行。
2.00 (内核 1.3.73)增加 bzImageinitrd 支持;bootloader 可以和内核通过形式化的方式通信;setup.S 可以重定向,原有的 setup 区域依然支持写入。
2.01 (内核 1.3.76)增加堆溢出警告。
2.02 (内核 2.4.0-tet3-pre3)新的命令行协议;降低引导程序可以使用的内存上限;不覆盖传统 setup 区域,因此可以支持使用 SMM EBDA 或者使用32比特BIOS入口。
2.03 (内核 2.4.18-pre1)initrd 可以访问的最高地址对 bootloader 显式可见。
2.04 (内核 2.6.14)拓展 syssize 字段到4字节。
2.05 (内核 2.6.20)保护模式内核支持重定位;添加 relocatable_kernelkernel_alignment 字段。
2.06 (内核 2.6.22)添加包含启动命令行大小的字段。
2.07 (内核 2.6.24)增加半虚拟化引导协议。在 load_flags 增加 hardware_subarchhardware_subarch_dataKEEP_SEGMENTS 三个标志。
2.08 (内核 2.6.26)增加 crc32 校验码和 ELF 格式 payload 支持;增加 payload_offsetpayload_length 字段以方便定位 payload
2.09 (内核 2.6.31)在 kernel_alignment 之外增加宽松对齐协议;增加 init_sizepref_address 字段。增加扩展的 bootloader ID。
2.11 (内核 3.6)增加描述EFI handover 协议 入口偏移地址的字段
2.12 (内核 3.8)boot_params 结构增加 xloadflags 和其他扩展字段,用以支持在64位系统4G以上地址加载 bzImage 或者 ramdisk
2.13 (内核 3.14)为支持从32位 EFI启动64位内核,支持在 xloadflags 中设置32位和64位标志。
2.15 (内核 5.5)增加 kernel_infokernel_info.setup_type_max 字段。
  • 协议版本号只在setup header发生变化时增加。如果 boot_params 或者 kernel_info 没有变化,没有必要更新版本号。另外,推荐使用 xloadflagskernel_info 来和 bootloader 交互内核支持的特性信息。以为在最初的 setup header 中,空间十分受限,从协议2.15开始,最主要的和 bootloader 通信的方式就是通过 kernel_info

内存布局

    传统加载 ImagebzImage 的内核 loader,其内存布局如下:

    当使用bzImage时,保护模式内核将被重定向到 0x100000 (高端内存),而内核实模式部分(引导扇区,、配置、堆栈等)则可以重定向到 0x10000 到低端内存结束间的任何地址。不幸的是,在引导协议2.00和2.01版本中,内核内部仍使用0x90000+ 内存空间,直到引导协议版本2.02修复了这个问题。

    实际中, memory ceiling (在低端内存中 bootloader 可以使用的最高内存位置)越低越好,因为一些新的BIOS开始在低端内存的头部分配大量的内存(扩展BIOS数据区域EBDA)。bootloder 应该使用 INT 12h BIOS调用来确认还有多少低端内存可以使用。

    不幸的是,如果 INT 12h 报告的内存太少,通常 bootloader 除了向用户报错其他什么也做不了。因此 bootloader 应该设计成占用尽量少的低端内存。对于 zImage 或者老的 bzImage 内核而言,它们需要写到 0x90000。因此,bootloader 应该保证不要使用超过 0x9A000的内存,如果超过,很多BIOS将崩溃。

    对于现在使用 bzImage、引导协议大于等于2.02的内核,建议如下的内存布局:


实模式内核头部信息

    在后面的文本以及内核启动的流程中,“一个扇区”指代512字节,这独立于物理介质的实际扇区大小。

    加载Linux内核的第一步是加载实模式的代码(引导扇区和 setup 代码),之后检查如下在偏移 0x01f1 位置的头部信息。实模式代码最大可以占到32K,然而 bootloader 可能只选择加载前两个扇区(1K),之后检查需要的启动扇区大小。

    头部信息如下:

偏移/大小 协议 名称 含义
01F1/1 ALL(1) setup_sects 以扇区数衡量的 setup 区域大小
01F2/2 ALL root_flags 如果设置,root 将以只读挂载
01F4/4 2.04+(2) syssize 保护模式(32bit)代码的大小,单位为16个字节
01F8/2 ALL ram_size 不要使用
01FA/2 ALL vid_mode 视频模式控制
01FC/2 ALL root_dev 默认的根设备号
01FE/2 ALL boot_flag 魔数 (0xAA55)
0200/2 2.00+ jump 跳转指令
0202/4 2.00+ header 魔数(HdrS 0x53726448)
0206/2 2.00+ version 支持的引导协议版本
0208/4 2.00+ realmode_swtch bootloader hook(见后续描述)
020C/2 2.00+ start_sys_seg 已废弃
020E/2 2.00+ kernel_version 指向内核版本号字符串的指针
0210/1 2.00+ type_of_loader bootloader 标识
0211/1 2.00+ loadflags 引导协议的可选标志
0212/2 2.00+ setup_move_size 移动到高端内存的大小
0214/4 2.00+ code32_start bootloader hook
0218/4 2.00+ ramdisk_image initrd 的加载地址(由 bootloader 设置)
021C/4 2.00+ ramdisk_size initrd 的大小(由 bootloader 设置)
0220/4 2.00+ bootsect_kludge 不要使用
0224/2 2.00+ heap_end_ptr setup 结束之后的空余内存
0226/1 2.02+(3) ext_loader_ver 扩展的 bootloader 版本号
0227/1 2.02+(3) ext_loader_type 扩展的 bootloader 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 最小对齐(1 << min_alignment)
0236/2 2.12+ xloadflags 引导协议可选标志
0238/4 2.06+ cmdline_size 内核命令行的最大大小
023C/4 2.07+ hardware_subarch 硬件子架构(半虚拟化使用,比如 0x00000002 代表Xen)
0240/8 2.07+ hardware_subarch_data 子架构数据
0248/4 2.08+ payload_offset 内核 payload 偏移
024C/4 2.08+ payload_length 内核 payload 大小
0250/8 2.09+ setup_data 64比特指针,指向 struct setup_data 链表
0258/8 2.10+ pref_address 首选加载位置
0260/4 2.10+ init_size 初始化时需要的连续内存大小
0264/4 2.11+ handover_offset handover 入口偏移地址
0268/4 2.15+ kernel_info_offset kernel_info 偏移地址
  1. 为了向后兼容,如果 setup_sects 字段是0, 真实的值是4
  2. 在2.04之前的引导协议,syssize 的前两个字节没有用,这也意味着 bzImage 内核的大小 无法确定。
  3. 对于协议版本2.02-2.09,设置是安全的,但是会被忽略。

    如果 HdrS(0x53726448)在 0x202 没有找到,引导协议就是老版本的,加载旧版本内核,并且假设如下参数:Image类型为zImage;不支持initrd;实模式内核必须加载到地址 0x90000。其他情况,version 字段将包含引导协议版本,例如:使用引导协议2.01,这个值会是0x0201。当设置头部信息的字段时,必须保证只设置当前协议支持的字段。


加载内核的余下部分

    32比特(非实模式)内核开始于内核文件(setup_sects+1) x 512 的位置,对于Image/zImage它将被加载在 0x10000 地址,对于 bzImage 则是 0x100000

    如果引导协议大于2.00、loadflags 中标记了 0x01(LOAD_HIGH) ,就可以判定是 bzImage 内核。

    需要注意,Image/zImage 内核最大能到512K,因此使用 0x10000 - 0x90000 整个地址范围的内存空间,因而也要求实模式部分在 0x90000 加载。bzImage 内核要相对灵活。


运行内核

    内核通过 jump 到内核入口启动,内核入口在内核实模式代码的段偏移 0x20 处。也就是说,如果你加载实模式代码到 0x90000, 内核入口在 9020:0000

这里插入一个小贴士: 实模式下的物理地址 = 段基址(16位) << 4 + 偏移(16位)

    在内核入口,ds=es=ss 指向实模式内核的开始地址(如果实模式内核加载于 0x90000,值应为0x9000),sp 需要正确设置,一般指向堆的顶部,并且屏蔽中断。另外,为了避免内核中的bug,推荐 bootloader 设置 fs=gs=ds=es=ss

以下是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 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 */


32位引导协议

    对于有新BIOS(比如EFI、LinuxBIOS、kexec等等)的机器,基于老BIOS的内核16比特实模式代码就不能使用了。因此需要定义基于32比特的引导协议。

    在32比特引导协议中,加载内核的第一步是配置启动参数(struct boot_params,也就是一般指的 Zero Page)。struct boot_params 使用的内存空间需要分配并初始化为0。之后内核镜像位于 0x1F1 位置的头部信息将加载到 struct boot_params 并进行检查。头部信息的结束位置可以通过如下计算得到:

1
0x0202 + byte value at offset 0x0201
    除了要读写、修改 struct boot_params 中16比特实模式使用的头部信息,还需要填写更多的字段,这些字段参考:Zero Page

    在设置好struct boot_params 之后,bootloader 可以像16比特实模式协议那样,加载32/64比特内核。

    在32比特引导协议中,内核通过跳转到32比特入口地址开始执行。

    在内核入口,CPU必须配置为32位保护模式,并且关闭分页;必须加载正确的GDT,GDT中包含__BOOT_CS(0x10)和__BOOT_DS(0x18)选择子的描述符;这两个描述符都必须是4G平坦分段;__BOOT_CS(0x10)必须有读和执行的权限,__BOOT_DS(0x18)必须有读写权限;CS必须是__BOOT_CS, 而DS、ES、SS必须是__BOOT_DS; 中断需要禁用; %esi 必须存储 struct boot_params 的基地址; %ebp、%edi、%ebi 必须为0。


64位引导协议

    配置 struct boot_params 及加载头部信息的过程与32比特协议一致。

    配置 struct boot_params 后,bootloader可以像16比特实模式协议那样加载64比特内核,但是内核将加载至4G以上地址。

    在64位引导协议中,内核通过跳转到64比特入口地址执行,这个地址是64比特的跳转地址加上 0x200

    在内核入口,CPU必须在64位模式下,并且开启分页;zero page、命令行缓存、以及从内核加载地址到 setup_header.init_size 都映射到同一个内存区域;必须加载正确的GDT,GDT中包含__BOOT_CS(0x10)和__BOOT_DS(0x18)选择子的描述符;这两个描述符都必须是4G平坦分段;__BOOT_CS(0x10)必须有读和执行的权限,__BOOT_DS(0x18)必须有读写权限;CS必须是__BOOT_CS, 而DS、ES、SS必须是__BOOT_DS; 中断需要禁用;%rsi 必须存储 struct boot_params 的基地址。