x86-64 计算机的启动流程——基于 coreboot+SeaBIOS+GRUB2+GNU/Linux+systemd

· 616字 · 3分钟

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 的启动目标顺序为:
  1. sysinit.target,初始化用户空间运行环境
  2. basic.target,执行早期开机自启动任务,如监听一些 socket,运行一些定时器等
  3. default.target,在 initramfs 中是一个指向 initrd.target 的软链接。initrd.target 挂载根文件系统到 /sysroot
  4. initrd-switch-root.target,切换到真正的根文件系统,并用实际根文件系统上的 systemd 替换现有的 init 进程

切换根文件系统之后的 systemd 🔗

由于切换根文件系统之后是将 systemd 进程替换为文件系统上的版本,所以 PID 仍然为 1。 此时的启动目标顺序为:

  1. sysinit.target
  2. basic.target
  3. default.target,此时的 default.target 不再指向 initrd.target,而是指向 multi-user.target(命令行)或 graphical.target(图形界面)。在此阶段,systemd 会启动各种系统服务,并最终执行 getty.service 和/或相应的 Display Manager(如果是图形界面),显示登录界面。

总结——操作系统在计算机启动过程中的作用 🔗

操作系统是资源的管理者,向用户提供各种服务,也是对硬件机器的扩展。

操作系统在计算机启动过程中,会配置内部存储器,初始化和管理计算机的各种外部设备(配置中断、映射内存空间等),启用进程和线程的管理功能(状态、控制、同步/互斥、通信、调度等),提供文件系统读写功能,并最终显示用户接口供用户使用。

参考文献 🔗

  1. https://doc.coreboot.org/getting_started/architecture.html
  2. https://www.cnblogs.com/gnuemacs/p/14287120.html
  3. https://www.cnblogs.com/shao-ye/p/11202680.html
  4. https://blog.csdn.net/Pedroa/article/details/53455355
  5. https://stackoverflow.com/questions/23422594/reset-vector-in-386-processors
  6. https://zhuanlan.zhihu.com/p/107898009
  7. https://www.seabios.org/Execution_and_code_flow
  8. https://opensource.com/article/17/2/linux-boot-and-startup
  9. https://www.gnu.org/software/grub/manual/grub/grub.html
  10. https://wiki.archlinux.org/index.php/GRUB_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)
  11. https://xinqiu.gitbooks.io/linux-inside-zh/content/
  12. https://blog.csdn.net/jinking01/article/details/107098437
  13. https://zhuanlan.zhihu.com/p/67520807
  14. https://www.junmajinlong.com/linux/systemd/systemd_bootup/
  15. https://wiki.archlinux.org/index.php/Systemd_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)
  16. https://blog.csdn.net/sinat_22055459/article/details/51055410
comments powered by Disqus