x86-64 计算机的启动流程——基于 coreboot+SeaBIOS+GRUB2+GNU/Linux+systemd 🔗
概述 🔗
x86-64 计算机加电后,CPU 首先从 ROM 中寻找并执行 BIOS 程序,初始化硬件,然后从启动设备(硬盘等)中读取分区表/引导信息,并启动引导加载器(bootloader),引导操作系统启动。 操作系统会进一步初始化硬件,然后启动各种系统服务,最终展现用户界面(CLI 或 GUI),供用户使用。
coreboot - 初始化硬件 🔗
coreboot 是一种自由、开放源代码的系统启动固件,在 x86 平台上结合相应的 payload 可以替代 Legacy BIOS 或 UEFI BIOS。
coreboot 在 x86-64 架构下执行时分为 bootblock、romstage、postcar、ramstage、payload 五个阶段,而其中的 payload 可以直接选择 GRUB2 等引导加载器加载操作系统内核,也可以用 SeaBIOS 模拟 Legacy BIOS,或者采用 Tianocore EDK2 执行 UEFI BIOS。
bootblock 阶段 🔗
计算机加电后,主板向 CPU 发出重置(reset)信号,并保持直到系统供电稳定为止。CPU 重置后,从重置向量(reset vector)开始执行代码。80386 之后的 x86 处理器的重置向量位于物理地址 FFFFFFF0h(CS:IP FFFF:FFF0h),映射到主板上一块支持片内执行的 ROM 芯片(即 BIOS 芯片)上。这个位置是一条 JMP 跳转指令,跳转到 bootblock 的主要代码区域来执行(_start16bit:
,src/cpu/x86/entry16.S)。
bootblock 的工作有:
- 将高速缓存“当作 RAM”(Cache-As-RAM)用于堆和栈
- 设置栈指针
- 将 .bss 段清零
- 解压并加载下一个启动阶段
- 下一个启动阶段一般是 romstage,但也可以是可选的 verstage,用于验证固件完整性。 在 x86 平台上,bootblock 还会:
- 加载 Microcode 更新
- 初始化计时器
- 从16位实模式跳转到32位保护模式
Cache-As-RAM 模式(CAR) 🔗
也称为“Non-Eviction mode”。此模式可以将 CPU 高速缓存当作普通的 SRAM 内存来使用,从而允许在启动阶段 DRAM 尚未初始化之时使用 C 语言编程。
ramstage 之前的阶段都在 Cache-As-RAM 下运行。
romstage 阶段 🔗
此阶段会初始化 DRAM 内存控制器并“训练”(train)内存,得出合适的时序数据。此阶段也进行其他设备的早期初始化。
postcar 阶段 🔗
postcar 阶段关闭 CAR 模式并加载 ramstage。
ramstage 🔗
ramstage 是 BIOS 中最主要的用于初始化硬件设备的阶段,其工作有:
- 初始化 PCI 设备
- 初始化片上(on-chip)设备
- 初始化 TPM(如果没有 verstage)
- 初始化显卡(可选,也可以由 SeaBIOS 执行 VGA ROM 来初始化)
- 初始化 CPU ,例如设置系统管理模式(SMM) 初始化之后,ramstage 会写入一些表格,让 payload 或操作系统了解硬件是否存在及硬件状态,这些表格包括:
- ACPI 表(x86 特定)
- SMBIOS 表(x86 特定)
- coreboot 表 ramstage 阶段还会锁定一些硬件和固件:
- 启动媒体的写保护
- 锁定安全相关的寄存器
- 锁定 SMM 模式(x86 特定)
payload 🔗
Payload 是 coreboot 文件系统(CBFS)中嵌入的软件,可以是引导加载器(GRUB2、FILO 等)、其他固件的模拟器(SeaBIOS、Tianocore 等),也可以是受支持的操作系统内核。
每份 coreboot 构建只能有一个 ELF 可执行文件作为 payload 加载。coreboot 自身不允许在运行时选择 payload,只能由被加载的 payload 执行链式加载来执行 CBFS 中的其他 ELF 文件。
SeaBIOS - 模拟传统 BIOS,执行引导器 🔗
SeaBIOS 是一种自由、开放源代码的16位 x86 BIOS 实现。为了兼容性,它支持标准的 BIOS 功能和由典型的专有 x86 BIOS 实现的调用接口。
SeaBIOS 本身可以在模拟器上直接作为 BIOS 执行,也可以嵌入到 Tianocore UEFI 中作为 CSM 执行,当然也可以嵌入到 coreboot 中作为 payload 执行。
POST 阶段 🔗
在 coreboot 环境下,SeaBIOS 从32位保护模式的 romlayout.S:entry_elf()
开始执行,然后会调用 post.c:handle_post()
。POST 阶段包括以下列出的几个子阶段:
- “preinit” 子阶段:在代码重定位前执行的代码。主要是检测内存并“预初始化”(preinit) SeaBIOS 内部的 malloc 实现。
- “init” 子阶段:初始化内部变量和接口。主要包括正式初始化 malloc,初始化 CBFS,初始化中断向量表、BIOS 数据区域 BDA、POST内存管理(PMM)、即插即用(PNP)、键盘和鼠标。
- “setup” 子阶段:设置硬件和驱动。首先初始化平台硬件,然后执行各种 Option ROM,包括 VGA ROM 并设定显卡,然后显示选择启动设备的界面。
- “prepboot” 子阶段:结束接口,并为 boot 阶段做准备。主要是将 BIOS 内存区域设置为只读,然后回到16位实模式,并调用 0x19 中断,进入 BOOT 阶段。
BOOT 阶段 🔗
BOOT 阶段首先从处理 0x19 中断开始运行,然后按照选择的启动设备调用相应函数。
对于从硬盘启动,需要调用 0x13 中断读取主引导记录(MBR)的扇区
- 功能描述:读扇区
- 入口参数:
- AH=02H
- AL=扇区数
- CH=柱面
- CL=扇区
- DH=磁头
- DL=驱动器,00H~7FH:软盘;80H~0FFH:硬盘
- ES:BX=缓冲区的地址
- 出口参数:
- CF=0——操作成功,AH=00H,AL=传输的扇区数
- 否则,AH=状态代码
主引导记录,是位于硬盘0磁头,0柱面,1扇区为起始位置,扇区数为1(512字节)的一段数据。SeaBIOS 将其读出并复制到 0x7c00,然后检查 signature(最后的两个字节)是不是小端序的 0xAA55,如果是就跳转到 CS:IP 0x0000:0x7c00 执行。
GRUB2 🔗
GRUB2 是一种模块化的启动加载器,支持 multiboot 启动协议和一些操作系统专有的启动协议。
boot.img 🔗
boot.img 是 GRUB2 最首先启动的组件,也就是安装到主引导记录前 446 字节的 bootloader,相当于 GRUB Legacy 的 stage 1。446 字节的 boot.img 太小,无法容纳读取文件系统的指令。因此,boot.img 会从硬盘中加载 core.img 的第一个扇区,并由 core.img 来读取文件系统和加载模块。
core.img 🔗
主引导记录 512 字节之后到第一个主分区的位置之间,有 31 KiB 的空间。这个位置过去可以存放 GRUB Legacy 的 stage 1.5,但对于 GRUB2 的 core.img 来说还是太小了。所幸,现代分区工具会按照 1 MiB 对齐的原则来创建分区,在第一个主分区之前保留 1~2 MiB 的空间,这正好可以让 GRUB2 存放 core.img。 core.img 的第一个扇区被加载后,会将自身的其他内容都加载到内存然后继续执行。 core.img 是用 grub-mkimage 创建的,其中包括了足够多的模块用以读取 GRUB 安装位置(通常是某个 GNU/Linux 操作系统的 /boot/grub 目录)——这些模块可能有 part_msdos.mod、ext2.mod 等。core.img 会根据配置文件从 GRUB 安装位置中加载更多模块,然后加载相应的操作系统,或者向用户展示启动菜单。
Linux kernel 🔗
引导 🔗
header.S - 启动代码和 C 语言环境设置 🔗
内核从 _start
启动后跳转到 start_of_setup
执行。这段代码首先设置 cs、ds、es 段寄存器,然后设置 ss 寄存器和栈,最后清空 .bss 段并跳转到 main.c
执行 C 语言代码。
main.c - 主函数 🔗
- 复制启动参数
- 初始化控制台
- 初始化堆
- 检查 CPU 类型
- 内存分布侦测
- 初始化键盘
- 查询系统参数
main.c - 显示模式初始化和进入保护模式 🔗
- 显示模式的初始化
- 在进入保护模式之前的准备工作
- 禁止外部中断
- 禁止 NMI 中断
- 使能 A20 地址线
- 设置中断描述符表(IDT)
- 设置全局描述符表(GDT)
- 正式进入保护模式
startup_32 并进入 64 位长模式 🔗
- 建立栈
- 确认 CPU 是否支持长模式和 SSE
- 计算内核解压缩之后的重定位地址(-fPIC)
- 进入长模式前的准备工作
- 更新全局描述符表
- 启用 PAE
- 初期页表初始化
- 切换到长模式
startup_64 和内核解压缩 🔗
- 重置寄存器
- 保存 rsi 寄存器的值(指向 boot_params)
- 内核解压缩
- 再次初始化控制台
- 调用压缩内核镜像的解压函数,使其原地解压
- 移动内核镜像到正确的位置
- 进入内核
内核初始化 🔗
页式内存管理 🔗
x86-64 下 Linux 系统既采用了内存分段管理,也采用了分页管理;但和其他段页式内存管理系统不同,Linux 的内存分段只用于权限控制,不用于寻址——每个段的大小都等于整个地址空间。
- 设置初期页表
- 设置初期中断和异常处理
- 初始化内存页
内核入口 - start_kernel 🔗
start_kernel 函数的主要功能包括:
- 初始化锁验证器
- 激活第一个 CPU
- 打印内核版本号和编译环境
- 调用 setup_arch,进行体系结构相关初始化
- 配置 ioremap - 将外部设备映射到内存空间虚拟地址上
- 初始化设备树
- 初始化内存描述符
- 设置 NX 位
- 加载 PCI 设备
- 检测系统可用内存
- 从桌面管理接口(DMI)收集系统信息
- 配置 SMP(对称多处理)
- 为 DMA 分配空间
- 初始化稀疏内存
- 进行 vsyscall 系统调用映射
- 初始化调度器
- 配置 NUMA
- 初始化 PID 散列表
- 根据编译选项初始化调度器(默认为 CFS 完全公平调度器)
- 初始化 RCU - Read Copy Update(读复制更新),一种适合少写多读情况的并发同步机制。采用发布-订阅机制实现
- ACPI 早期初始化
- 为 init 进程分配缓存
- 初始化缓存
- 创建 proc 文件系统
- 启动 RCU 调度器
- 启动 SMP 线程
- 执行 init 进程
systemd 🔗
systemd 是一个 Linux 系统基础组件的集合,提供了一个系统和服务管理器,运行为 PID 1 并负责启动其它程序。功能包括:支持并行化任务;同时采用 socket 式与 D-Bus 总线式激活服务;按需启动守护进程(daemon);利用 Linux 的 cgroups 监视进程;支持快照和系统恢复;维护挂载点和自动挂载点;各服务间基于依赖关系进行精密控制。systemd 支持 SysV 和 LSB 初始脚本,可以替代 sysvinit。除此之外,功能还包括日志进程、控制基础系统配置,维护登陆用户列表以及系统账户、运行时目录和设置,可以运行容器和虚拟机,可以简单的管理网络配置、网络时间同步、日志转发和名称解析等。——systemd 项目主页 https://freedesktop.org/wiki/Software/systemd
initramfs 🔗
GRUB 加载 Linux 内核时,通常会一并加载一个 initramfs,其中包括挂载真实根文件系统以前使用的文件系统。这是因为用户使用的文件系统驱动往往不会全都编译进内核,initramfs 可以提供加载驱动模块的程序。initramfs 在 GNU/Linux 发行版的包管理器安装内核时生成,典型用到的工具有 Fedora 的 dracut 或 Arch Linux 的 mkinitcpio 等。 对于现代 GNU/Linux 系统来说,initramfs 包括:
- init 进程对应的可执行文件,/sbin/init。这是一个指向 /lib/systemd/systemd 的软链接
- 根文件系统所需内核驱动模块
- 挂载根分区所需的其他用户空间可执行文件 initramfs 阶段,systemd 的启动目标顺序为:
- sysinit.target,初始化用户空间运行环境
- basic.target,执行早期开机自启动任务,如监听一些 socket,运行一些定时器等
- default.target,在 initramfs 中是一个指向 initrd.target 的软链接。initrd.target 挂载根文件系统到 /sysroot
- initrd-switch-root.target,切换到真正的根文件系统,并用实际根文件系统上的 systemd 替换现有的 init 进程
切换根文件系统之后的 systemd 🔗
由于切换根文件系统之后是将 systemd 进程替换为文件系统上的版本,所以 PID 仍然为 1。 此时的启动目标顺序为:
- sysinit.target
- basic.target
- default.target,此时的 default.target 不再指向 initrd.target,而是指向 multi-user.target(命令行)或 graphical.target(图形界面)。在此阶段,systemd 会启动各种系统服务,并最终执行 getty.service 和/或相应的 Display Manager(如果是图形界面),显示登录界面。
总结——操作系统在计算机启动过程中的作用 🔗
操作系统是资源的管理者,向用户提供各种服务,也是对硬件机器的扩展。
操作系统在计算机启动过程中,会配置内部存储器,初始化和管理计算机的各种外部设备(配置中断、映射内存空间等),启用进程和线程的管理功能(状态、控制、同步/互斥、通信、调度等),提供文件系统读写功能,并最终显示用户接口供用户使用。
参考文献 🔗
- https://doc.coreboot.org/getting_started/architecture.html
- https://www.cnblogs.com/gnuemacs/p/14287120.html
- https://www.cnblogs.com/shao-ye/p/11202680.html
- https://blog.csdn.net/Pedroa/article/details/53455355
- https://stackoverflow.com/questions/23422594/reset-vector-in-386-processors
- https://zhuanlan.zhihu.com/p/107898009
- https://www.seabios.org/Execution_and_code_flow
- https://opensource.com/article/17/2/linux-boot-and-startup
- https://www.gnu.org/software/grub/manual/grub/grub.html
- https://wiki.archlinux.org/index.php/GRUB_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)
- https://xinqiu.gitbooks.io/linux-inside-zh/content/
- https://blog.csdn.net/jinking01/article/details/107098437
- https://zhuanlan.zhihu.com/p/67520807
- https://www.junmajinlong.com/linux/systemd/systemd_bootup/
- https://wiki.archlinux.org/index.php/Systemd_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)
- https://blog.csdn.net/sinat_22055459/article/details/51055410