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)增加 bzImage 和 initrd 支持;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_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 支持;增加 payload_offset 和 payload_length 字段以方便定位 payload。 |
2.09 | (内核 2.6.31)在 kernel_alignment 之外增加宽松对齐协议;增加 init_size 和 pref_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_info 和 kernel_info.setup_type_max 字段。 |
- 协议版本号只在setup header发生变化时增加。如果 boot_params 或者 kernel_info 没有变化,没有必要更新版本号。另外,推荐使用 xloadflags 和 kernel_info 来和 bootloader 交互内核支持的特性信息。以为在最初的 setup header 中,空间十分受限,从协议2.15开始,最主要的和 bootloader 通信的方式就是通过 kernel_info。
内存布局
传统加载 Image 和 bzImage 的内核
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 偏移地址 |
- 为了向后兼容,如果 setup_sects 字段是0, 真实的值是4
- 在2.04之前的引导协议,syssize 的前两个字节没有用,这也意味着 bzImage 内核的大小 无法确定。
- 对于协议版本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 之后,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 的基地址。