注:本文为“计算机启动、 Linux 启动”相关文章合辑。
替换引文部分不清晰的图。
探索计算机的启动过程
Aleksandr Goncharov
2023/04/21
很多人对计算机的启动方式很感兴趣。只要设备开启,这就是魔法开始和持续的地方。在本文中,我们将概述 启动 过程,包括其各个阶段、涉及的关键组件以及过程中面临的挑战。
虽然我们主要关注x86 架构(使用最广泛的架构),但其他架构在其引导过程中会有许多相似之处。我希望这篇文章对于任何希望加深他们在该领域的知识的人来说都是宝贵的资源。开始了!
开机 ROM
位于主板上并存储负责启动计算机的固件代码的 集成电路(芯片)称为 BOOT ROM。这个名字是不规范的,所以其他开发者常称它为FLASH ROM、BIOS FLASH、BOOT FLASH、SPI FLASH等(这些名字是因为技术、接口和用途名称而给它们起的)。不用担心,这些术语可以互换。BOOT ROM中的固件代码在计算机开机时首先执行。它执行基本测试、初始化硬件,然后将操作系统加载程序从可启动设备(例如硬盘驱动器或 USB 驱动器)加载到内存中。该芯片由非易失性存储器 (NVM) 制成。
非易失性存储器
非易失性存储器是一种计算机存储器,即使在电源关闭时也能保留其内容。它使这种类型的内存非常适合存储即使在计算机断电时也需要保留的重要数据。此外,讨论将仅集中在保存固件代码的存储器上。我们不会谈论硬盘驱动器 (HDD)、固态驱动器 (SSD)、软盘等存储。
基本上,我们可以将这种类型的内存分为以下几类。
一次性可编程
-
Masked ROM:内容在制造时确定,之后无法更改。
-
可编程 ROM (PROM):与屏蔽 ROM 不同,这种类型的存储器可以在制造后进行编程。但还是只有一次。
现场可编程
-
可擦可编程 ROM (EPROM):可以多次编程,但可以使用 紫外线 擦除和重新编程其内容。
-
电可擦除可编程 (EEPROM):可以使用 电信号 多次重新编程。
-
NOR 闪存:在结构上按块排列,其中数据在 块级别 擦除,并且可以在 字节级别 读取或写入。 NOR 存储器可使用字节并行、I2C 或 SPI 等标准接口直接访问。
在行业中,有一种惯例将术语 EEPROM 保留为字节方式可擦除存储器,而不是块方式可擦除闪存。
可编程存储器有一条规则 ——先擦后写。在这样的内存中,写入新数据更加复杂,因为数据以电荷的形式存储在浮栅上(原因主要在于存储单元的物理特性)。栅极上的电荷量决定了单元存储 “0” 还是 “1”。
擦除闪存芯片时,会将存储在其上的所有数据位设置为已知(默认)状态,通常为逻辑 “1”。这允许您从一个干净的平台开始,可以这么说,并将新数据编程到芯片上,而不会在其中存储任何旧数据的残余。当新数据写入芯片时,各个位的状态从 “1” 变为 “0” 以表示新数据。
如果不先擦除芯片而直接向芯片写入新数据,新数据将与旧数据结合,导致不可预知的结果。例如,考虑一个具有 8 位内存存储值 “0110 0010” 的闪存芯片。如果您将新数据 “1100 1001” 写入芯片而不先将其擦除,则芯片的最终状态将是 “0100 0000”,这可能不是您想要的。
主要的混淆与 ROM 这个词有关,它代表 只读存储器 。术语 “只读存储器” 历来用于指代永久且用户无法更改的存储器。然而,随着技术的进步,ROM 的定义也发生了变化,现在它通常被用来指代在工厂预编程且最终用户无法 轻易 更改的存储器。但如果用户拥有所需的技能和专用设备(例如编程器),则可以对芯片进行重新编程。尽管定义已更改,但 ROM 这个名称仍然保留,作为对存储器原始用途的历史参考。
通过应用写保护,某些类型的可重新编程 ROM 可能会暂时变为只读存储器。
这些不是所有现有类型的非易失性存储器,而是您可能偶然听说的大多数流行类型。如今,在大多数主板上,这些芯片都是使用 NOR Flash 技术制成的。
就地执行 (XIP)
Execute in Place (XIP) 是一种允许处理器直接从闪存中执行代码而无需先将其复制到易失性存储器(如 RAM )中的方法。这是通过将闪存映射到处理器的地址空间来实现的,因此可以直接从闪存执行代码。因此,系统能够尽快开始执行代码,而不必先等待 RAM 初始化。
等等…CPU 可以通过 SPI/Parallel/etc 协议与 BOOT ROM 通信吗?当然不是,它只是从系统内存中获取指令,对这个内存区域的请求被重定向到 Intel Direct Media Interface (DMI ) 或 AMD Infinity Fabric (IF) / Unified Media Interface (UMI)(前身)。它是主板上 CPU 和芯片组之间的链接。此时,通过位于芯片组中的解码器对地址进行解码,并将来自芯片的数据返回给处理器。
当芯片由支持随机访问读取但不支持随机访问写入的 NOR 闪存制成时,问题来了。只要可写内存不可用,所有计算都必须在处理器寄存器内执行。此时,代码只能用汇编语言编写,并倾向于为高级语言(通常为 C 语言)搭建环境。原因是内存初始化变得如此复杂,纯汇编很难写。由于此类语言至少需要堆和栈,因此我们需要可写内存。一些处理器在芯片本身中嵌入了 SRAM,但更现代的方法是使用板载高速缓存作为 RAM (CAR)。
缓存作为 Ram (CAR)
CPU 高速缓存是一种高速存储器,用于存储来自主存储器的常用数据和指令的副本。高速缓存位于更靠近处理器的位置,并分为多个级别(L1、L2、L3 ……),每个级别都比前一个更大、更慢。
如果数据在缓存中,CPU 可以从缓存中检索请求的数据(称为缓存命中)。当 CPU 缓存无法找到所需的数据时,就会导致缓存未命中。发生这种情况的原因可能是数据从未存储在缓存中,或者因为数据以前存储过但已从缓存中逐出。无论如何,处理器必须一路走到主存才能访问数据并将其复制到缓存中。
缓存逐出是从缓存中删除数据以为新数据释放空间的过程。数据的逐出可以由缓存系统发起(通常当缓存已满并且需要存储新数据时,或者当数据的生存时间策略已过期时)或通过显式请求发起。
但是,如果我们想将 CPU Cache 用作 RAM,我们需要将缓存设置为在Non-Eviction Mode下运行,也称为No-Fill Mode。此技术可防止因 高速缓存未命中而被逐出。相反,缓存被视为常规 SRAM,所有访问(读 / 写)都将访问缓存,而不会访问主内存。可以使用供应商特定的 CPU 指令激活该模式。
布局和内存映射
实际上,BOOT ROM 包含多种类型的固件。一旦一堆固件存储在 BOOT ROM 中,就需要以某种方式对其进行组织以区分它们。让我们看看它是如何完成的。
非描述符模式
最初,芯片组将整个 BOOT ROM 内容直接映射到内存(从 4GB 到 4GB - 16MB)。通常,如果 BOOT ROM 小于 16 MB,则内容会被重复映射。 CPU 和固件可以不受任何限制地读 / 写闪存。
新芯片组不再支持非描述符模式。
Intel 闪存描述符 / 描述符模式
最终,在 ICH8 中,Intel 引入了一种特殊的 BOOT ROM 布局。闪存分为以下区域:
- 闪存描述符 (FD) - 此数据结构必须位于设备的开头,偏移量为
0x10
。它由十一部分组成,如下图所示:
Descriptor MAP具有指向其他区域的指针以及每个区域的大小。
组件部分包含有关系统中闪存的信息(组件数量、每个组件的密度、无效指令等)。
Masters 部分定义了区域的读 / 写权限。就读 / 写而言,权限必须设置为只读,该区域存储的信息只能在制造过程中写入。
- BIOS - 只有这个区域被映射到内存中。
- Intel Converged Security and Management Engine (CSME / ME) - 支持不同英特尔技术和 ME 的固件。
- 千兆以太网 (GbE) - 只能由千兆以太网控制器直接访问。
- 平台数据
- 嵌入式控制器 (EC)
闪存描述符和Intel ME是唯一需要的区域。
英特尔固件接口表 (FIT)
FIT 是 BIOS 区域内的数据结构,包含描述平台配置的各种条目。表中的每个条目大小为 16 个字节。第一个称为 FIT 标头,另一个称为 FIT 条目。它位于物理地址 0xFFFFFFC0
(4GB - 0x40) 的 FIT 指针处。
在执行来自 * 复位向量的 * 第一条 CPU 指令之前,必须处理这些组件。条目包括 CPU 微代码更新、启动 ACM、平台引导 / TPM/BIOS/TXT 策略和其他内容。但至少 FIT 应该包括 FIT 标头和微码更新条目。因此,FIT 的常见用法是在执行 * 重置向量 * 之前更新微码。
这是内存映射的样子:
AMD 嵌入式固件结构
不幸的是,信息少得多,我找不到任何泄露的 AMD 芯片组文档,其中包含有关其布局的详细信息。所以我不能告诉你比 coreboot 文档 更好的了。它是根据仅在 NDA 下可用的 AMD 文档编写的。
实际上,只要知道闪存描述符的 AMD 模拟是嵌入式固件结构就足够了,它包含指向 PSP 目录表、BIOS 目录表和其他固件的指针。
硅初始化
如果你想看看现代内存和 CPU 是如何初始化的,那么我不得不打扰你。英特尔和 AMD 并不急于向社区发布硅初始化代码。就此类信息不公开而言,他们提供必要的硅初始化代码的二进制分发。这被认为是固件开发人员的库,包含用于初始化内存控制器、芯片组、CPU 和系统其他不同部分的二进制代码。
英特尔固件支持包 (FSP)
该二进制文件可以分为 4 个部分:
- FSP-T:设置可以执行 C 代码的早期执行环境(“临时 RAM”)。实际上,这个二进制文件设置了 CAR,但也做了一些早期的硬件初始化,比如设置 PCIe 内存映射配置空间。
- FSP-M:初始化永久存储器(如 DRAM)。
- FSP-S:完成芯片初始化,包括 CPU 和 IO 控制器初始化。
- FSP-O:提供 OEM 设备初始化的可选组件。
这是英特尔发布的 英特尔 FSP 二进制文件存储库,您可以在他们的 GitHub 上找到。 FSP Specification v2.1 可从 Intel 网站获取。
AMD 通用封装软件架构 (AGESA)
早于 Family 17h 的产品的 AGESA 称为 v5 或 Arch2008。当时,AGESA 是开源的,代码在 coreboot 存储库 中可用(4.18 版后已弃用)。 Arch2008 的规范 可以在 AMD 网站上找到。
随着 Family 17h (Zen 微架构)产品的推出,AMD 并未发布 AGESA 源代码,仅发布了预构建的二进制解决方案。这样的继任者称为 AGESA v9,支持 Family 17h 及更高版本。
开放 SIL
没有可用的详细信息,只有 新闻。
自治子系统
现代 x86 启动过程的一个组成部分,没有它 x86 内核将永远不会被激活。因此不可能完全禁用它们。这些技术负责硬件初始化、验证系统完整性、电源管理和 CPU 启动。这些子系统的固件在主处理器开始执行它自己的固件之前被加载和执行。此类系统上的代码独立于平台的 CPU 内核运行。
只要许多硬件公司通过隐匿性来纳入安全原则,这些子系统的源代码和文档就都无法获得。幸运的是,我们知道它如何影响引导过程 - 请参阅 [硬件电源序列](https://hackernoon.com/lang/zh/ 固件的隐藏世界探索您的计算机启动过程 #power_sequence)。
我们不会详细介绍,因为互联网上已经有来自世界各地的研究人员的大量文章。但我只会给你一个简短的描述。
英特尔管理引擎 (ME)
Intel ME 是一个独立的 i486/80486 微处理器,自 2008 年起集成到 Intel 芯片组 (PCH) 中。它有自己的 RAM、内置 ROM、连接芯片组内所有总线的总线桥(因此,它可以访问网络甚至 CPU 上的主 RAM),等等。运行基于 MINIX 的自定义操作系统。
AMD 平台安全处理器 (PSP)
AMD PSP 是一个依赖于 Trustzone 扩展的 ARM 内核,作为协处理器被插入到 CPU die 中。自 2013 年以来,该芯片已集成到大多数 AMD 平台中。运行未记录的专有操作系统。
硬件电源序列
此过程也称为上电顺序或电源排序,以平台所需的特定顺序提供多个派生电压电平和 / 或 * 电源 * 轨。更简单地说,它按特定顺序为多个平台组件供电。该过程因系统或平台设计而异,但通常标准 PC 包括以下步骤:
-
您按下电源按钮。但是等等…… 这个按钮位于计算机机箱上,它不是计算机的必要部件。通常,电源按钮是一根电缆。我们在一侧有一个按钮,在另一侧的主板上的两个金属插脚上有一个开关。当我们按下按钮时,这些插脚连接在一起,因此电流可以通过它们。如果您感兴趣,请观看下面的视频,了解如何在没有电源按钮的情况下打开计算机。
https://www.youtube.com/embed/G4FOBL1c3pA
-
主板向电源单元 (PSU) 发送信号。
-
电源接收信号,提供适量的电量,并将信号传回主板。
-
一旦主板收到电源良好信号,它就会为核心、时钟、芯片组、内存、不同控制器等平台组件加电。
-
各种子系统,包括自主子系统(上面讨论过),可以在主处理器之前启动。
-
基于 AMD 的系统(适用于 Family 17h 及更高版本)
-
PSP 执行片上BOOT ROM。
-
PSP 在片外 BOOT ROM 中找到嵌入式固件表并执行 PSP 固件。
-
PSP 解析 PSP 目录表以查找 ABL 阶段并执行它们。
-
ABL 阶段初始化主内存,在 BOOT ROM 中找到 BIOS 映像,并将其加载到 DRAM 中(如果映像被压缩,则解压缩)。
该平台没有理由使用 CAR,因为 DRAM 已经可用并且 PSP 将固件映像加载到其中。
-
基于英特尔的系统
-
芯片组 (ICH/PCH) 在 BOOT ROM 中找到Intel 闪存描述符。
-
芯片组将 CSME 固件复制到内部存储器中,Intel ME 可以访问它,最后一个开始执行它。
-
芯片组将 BIOS 区域映射到内存。
-
位于固件接口表中的微代码更新被加载到 CPU 中。它们必须在每次系统启动时应用。
-
(可选)如果找到经过身份验证的代码模块 (ACM),则执行该条目。
-
一直以来,CPU 复位信号都有效,以防止 CPU 在系统其他部分准备就绪之前启动。当平台准备就绪时,CPU 复位线被取消断言。在多处理器或多核系统中,一个 CPU 被动态选择为运行所有固件初始化代码的引导处理器 (BSP)。其余的处理器,此时称为应用程序处理器 (AP),将保持暂停状态,直到稍后它们被固件 / 内核明确激活。
-
CPU 首次上电后,以 实模式 运行。大多数寄存器都有明确定义的值,包括指令指针 (IP)、代码段 (CS) 和描述符缓存,它是处理器中每个段描述符的副本,以允许快速访问段内存。
段描述符.是全局描述符表(GDT).中的一个条目,包含基地址、段限制和访问信息(这部分被忽略,因为实模式没有像保护模式那样的访问控制)。不是为每次内存访问访问 GDT(位于内存中),而是将信息存储在描述符缓存中。
但是,GDT 不涉及实模式,因此处理器在内部生成条目。用于访问段描述符的 CS 选择器寄存器加载了 0xF000
。 CS 基地址初始化为 0xFFFF_0000
。 IP 初始化为 0xFFF0
。
因此,处理器开始从位于物理地址 0xFFFF_FFF0
( 0xFFFF_0000
+ 0x0000_FFF0
) 的内存中获取指令。在该地址执行的第一条指令称为 复位向量。
注意:此技巧可让您访问高地址空间,但是,您无法访问 0xFFFF_0000
地址以下的代码。 CS 基地址保持此初始值,直到 CS 选择器寄存器被固件加载。可以通过执行远跳来完成。
此时,最好的决定是切换到具有 4 GB 寻址能力的保护模式。如果固件不这样做,那么为了使实模式工作,芯片组必须能够将低于 1 MB 的内存范围别名化为略低于 4 GB 的等效范围。某些芯片组没有这种别名,可能需要在执行第一次跳远之前切换到另一种操作模式。
-
该地址位于一段非易失性存储器中,因此 CPU 使用就地执行 (XIP) 方法。尽管如果它是基于 AMD 的系统,您可能正在从主内存中读取。
-
CPU 执行固件代码。
建议观看下面关于开机顺序的视频,该视频以华硕 P9X79 主板为例说明了该过程。尽管它是俄语,但如果您打开自动生成的英文字幕,您将能够理解所有内容。
- Процесс запуска материнской платы. Power on Sequence - YouTube
motherboard startup process. Power on Sequence
https://www.youtube.com/embed/lAH7eSjR1d4
从 BIOS/UEFI 到 OS
Aleksandr Goncharov
2024/08/22
按下计算机电源按钮的那一刻会发生什么?
在屏幕亮起之前,在那短暂的停顿背后,发生了一系列复杂的过程。
本文将深入迷人的固件世界,探索不同组件在启动过程中如何交互。
通过了解这些联系,您将更清楚地了解使您的系统充满活力的基础元素。我们的主要重点是英特尔 x86 架构,但许多原则也适用于其他架构。
现在,让我们揭开固件背后的秘密。
定义
-固件:一种嵌入在硬件中的专用软件,提供低级控制并使硬件能够正常运行并与其他系统组件交互。
-
基本输入 / 输出系统 (BIOS):一种传统固件(最初为 IBM PC 创建),负责平台开机后的硬件初始化。如今,它通常被含糊地称为整套固件。
-
引导加载程序:负责启动计算机的固件的统称。它被用作BIOS的现代概念,通常提供带有引导代码的 框架 来初始化处理器和芯片组,以及第三方(例如主板开发人员)执行平台特定初始化的接口。
-
Payload:在 bootloader 退出时执行的软件。可以是第二阶段的 bootloader、操作系统、BIOS/UEFI 应用程序等。它通常根据固件设计负责引导流程。
-
BIOS和bootloader这两个术语的使用可能会令人困惑,因为它们的含义取决于上下文。然而,当有人提到固件、BIOS或bootloader时,他们通常指的是在操作系统和硬件之间运行的整套固件。
-
EFI与UEFI: 可扩展固件接口 (EFI) 是英特尔开发的原始规范。统一可扩展固件接口 (UEFI)是EFI的后继者,由 UEFI 论坛 创建,旨在标准化和扩展原始规范。在大多数情况下,EFI和UEFI可互换使用。
整体固件架构
为了理解固件组件如何交互,我们将探索整个架构及其所有连接部分。下图所示的执行流程从 复位向量 开始,它是第一阶段引导加载程序的一部分。从那里开始,它经历了各种固件阶段:
固件或 BIOS 通常可以分为两个主要部分,它们之间通常具有最小的接口:
1.硬件初始化:负责初始化系统的硬件组件。
2.操作系统和用户界面:向操作系统和用户提供必要的接口。
平台固件的设计可以是单片的,结合硬件初始化和启动功能,也可以遵循模块化和分阶段的启动流程。设计选择取决于系统要求,可能对某些设备更有利。
下图说明了不同的固件组件如何交互以及如何一起使用来支持启动过程(箭头表示执行顺序):
如果这些图表现在看起来很复杂,请不要担心。阅读本文后再回顾一下,它们会更清晰。
第一阶段引导加载程序 (FSBL)
此固件旨在初始化计算机和嵌入式系统,重点是最少的硬件初始化:只执行绝对需要的操作,然后将控制权交给第二阶段引导加载程序以启动操作系统。FSBL不会从闪存芯片以外的存储介质加载操作系统。 由于它只初始化底层硬件,而不处理硬盘、SSD 或 USB 闪存驱动器等启动介质,因此需要另一个软件来实际启动操作系统。
FSBL 的主要职责:
1.CPU:从 16 位实模式.切换到 32 位保护模式.(注意:或者在 BIOS 的情况下处于虚拟 8086 模式.)。
2.缓存利用率:调用 FSP-T 为 C 环境 配置 Cache-As-RAM 。
3.调试端口:通过调用特定于板的初始化方法来初始化配置的调试端口。
4.内存初始化:调用 FSP-M 初始化系统主内存并保存关键的系统内存信息。
5.GPIO:配置通用输入 / 输出(GPIO)引脚用于与外部设备连接。
6.硅:执行早期平台初始化并使用 FSP-S 完成芯片组、CPU 和 IO 控制器的初始化。
7.PCI 枚举:枚举 PCI 设备并分配内存地址和 IRQ 等资源。
8.有效载荷准备:设置 SMBIOS 和 ACPI 表,包括需要传递给有效载荷的准备信息(coreboot 表、HOB)。
9.装载和交接:装载并将控制权转交给有效载荷。
BIOS(POST 阶段)
在计算机发展的早期,开源软件并不流行,大多数 BIOS 实现都是专有的。只有少数可用的开放解决方案提供 BIOS POST 源代码,例如 Super PC/Turbo XT BIOS 和 GLaBIOS 。这些项目旨在在 IBM 5150/5155/5160 系统和大多数 XT 克隆系统上运行。
然而,更知名的开源 BIOS 实现(如 OpenBIOS 和 SeaBIOS )不执行硬件初始化,因为它们不打算在裸机上运行。但它们被广泛用作第二阶段引导加载程序,并在 QEMU 和 Bochs 等虚拟环境中本地运行。
无论如何,您几乎不需要直接使用这些早期的 BIOS 或深入研究它们的细节。但如果您有兴趣探索,上述存储库是一个很好的起点。
就目前的发展趋势而言,似乎没有专有 BIOS 解决方案的持续开发,而且面对现代替代方案,此类项目已经过时。
UEFI 平台初始化 (PI)
启动过程遵循分阶段的流程,在下图中从左侧开始,然后移至右侧。平台启动过程的时间线分为以下几个阶段,如黄色框所示:
- 安全(SEC):复位向量 之后的第一个阶段,其主要功能是设置临时 RAM(CPU Cache-As-RAM 或 SRAM)。
- 预 EFI 初始化 (PEI):此阶段调度称为预 EFI 初始化模块 (PEIM) 的专用驱动程序。这些模块处理必要的硬件初始化,例如配置 CPU 和芯片组以及设置主内存 (DRAM)。
- 驱动程序执行环境 (DXE):在此阶段,执行系统初始化的其余部分。DXE 阶段提供 UEFI 服务并支持系统运行所需的各种协议和驱动程序。
- 启动设备选择 (BDS):此阶段实施平台启动策略,确定启动顺序并选择适当的启动设备 / 加载程序。
- 瞬态系统加载 (TSL):在此阶段,系统使用 UEFI 服务运行应用程序来准备操作系统。它包括从 UEFI 环境到操作系统的转换,最后以
ExitBootServices ()
调用结束。 - 运行时(RT):在此阶段,操作系统完全运行,管理其控制下的系统。
- 使用寿命结束后 (AL):此阶段处理硬件或操作系统崩溃 / 关闭 / 重启的情况。固件可能会尝试恢复操作,但是,UEFI PI 规范并未为此阶段定义特定要求或行为。
这个过程及其执行阶段在UEFI 平台初始化 (PI) 规范中有所介绍。不过,还有UEFI 接口(图中用粗蓝线表示),它不属于前一个文档,而是在UEFI 规范中有所描述。虽然 * UEFI * 的名称和频繁使用可能会造成混淆,但这两个文档的重点有所不同:
-UEFI PI 规范:专注于低级固件组件之间的接口,并详细说明这些模块如何交互以初始化平台。
-UEFI 规范:定义操作系统 (OS) 和固件之间交互的接口。这将在第二阶段引导加载程序的背景下进一步讨论。请注意,UEFI 规范依赖于 PI 规范。
本质上,这两个规范都是关于接口的,只是层次不同。有关详细信息,您可以在 UEFI 论坛网站 上访问这两个规范。
UEFI PI 最初设计为统一的固件解决方案,没有考虑第一阶段和第二阶段引导程序之间的区别。但是,当我们将 UEFI 称为**[第一阶段引导程序](https://hackernoon.com/lang/zh/ 从 - biosuefi - 到 - os - 的固件秘密之旅 #fsbl)** 时,它包括 SEC、PEI 和早期 DXE 阶段。我们将 DXE 分为早期和晚期阶段的原因是由于它们在初始化过程中扮演的角色不同。
在早期 DXE 阶段,驱动程序通常执行必要的 CPU/PCH/ 主板初始化,并生成**DXE 架构协议 (AP),这有助于将 DXE 阶段与平台特定的硬件隔离开来。AP 封装了特定于平台的细节,使 后期 DXE 阶段能够独立于硬件细节运行。
核心启动
关于 Coreboot 工作原理的详细文章即将发布。关注我的社交媒体 - 它们很快就会发布!
其他解决方案
- Intel Slim Bootloader (SBL). :纯粹的第一阶段引导程序,仅提供核心硬件组件初始化,然后加载有效负载。但是,它仅适用于 Intel x86 平台,不支持 AMD x86 或其他架构。
- Das U-Boot. :既是第一阶段的引导程序,也是第二阶段的引导程序。但是,与其他固件相比,直接从平台的 x86 重置向量(称为裸模式)引导的支持有限。它在嵌入式系统和基于 ARM 的设备上更受欢迎。作为第二阶段的引导程序,U-Boot 实现了 UEFI 的一个子集,但专注于嵌入式系统。
第二阶段引导加载程序 (SSBL)
初始硬件设置完成后,进入第二阶段。其主要作用是在操作系统和平台固件之间设置软件接口,确保操作系统可以管理系统资源并与硬件组件交互。
SSBL 旨在尽可能隐藏硬件差异,通过处理大部分硬件级接口来简化操作系统和应用程序开发。这种抽象使开发人员可以专注于更高级别的功能,而不必担心底层硬件差异。
SSBL 的主要职责:
1.平台信息检索:从第一阶段引导加载程序获取平台特定信息,包括内存映射、SMBIOS、ACPI 表、SPI 闪存等。
2.运行平台独立驱动程序:包括 SMM、SPI、PCI、SCSI/ATA/IDE/DISK、USB、ACPI、网络接口等驱动程序。
3.服务实现(又名接口):提供一组促进操作系统和硬件组件之间通信的服务。
4.设置菜单:提供系统配置的设置菜单,允许用户调整与启动顺序、硬件首选项和其他系统参数相关的设置。
5.启动逻辑:从可用的启动媒体定位并加载有效负载(可能是操作系统)的机制。
BIOS
BIOS 中的接口称为BIOS 服务 / 功能 / 中断调用。这些功能提供了一组用于硬件访问的例程,但它们在系统特定硬件上执行的具体细节对用户是隐藏的。
在 16 位 实模式.中,可以通过 INT x86 汇编语言指令调用软件中断轻松访问它们。在 32 位保护模式.中,由于段值的处理方式不同,几乎所有 BIOS 服务都不可用。
我们以 磁盘服务( INT 13h
) 为例,它使用 柱面 - 磁头 - 扇区 (CHS) 寻址提供基于扇区的硬盘和软盘读写服务,作为如何使用此接口的示例。假设我们要读取 2 个扇区 (1024 字节) 并将它们加载到内存地址 * 0x9020* ,则可以执行以下代码:
mov $0x02, %ah # Set BIOS read sector routine
mov $0x00, %ch # Select cylinder 0
mov $0x00, %dh # Select head 0 [has a base of 0]
mov $0x02, %cl # Select sector 2 (next after the boot sector) [has a base of 1]
mov $0x02, %al # Read 2 sectors
mov $0x00, %bx # Set BX general register to 0
mov %bx, %es # Set ES segment register to 0
mov $0x9020, %bx # Load sectors to ES:BX (0:0x9020)
int $0x13 # Start reading from drive
jmp $0x9020 # Jump to loaded code
如果您对 SeaBios 中这项服务的编写方式感兴趣,请查看 src/disk.c 。
引导阶段
-
通过读取设备的第一个 512 字节扇区(扇区零)开始搜索可启动设备(可以是硬盘、CD-ROM、软盘等)。
-
BIOS 中的引导序列将它找到的第一个有效的 主引导记录 (MBR) 加载到计算机物理内存的物理地址 0x7C00 处(提示: 0x0000:0x7c00 和 0x7c0:0x0000 指的是同一个物理地址)。
-
BIOS 将控制权移交给有效负载的前 512 个字节。这个已加载的扇区太小,无法容纳整个有效负载代码,其目的是从可启动设备加载剩余的有效负载。此时,有效负载可以使用 BIOS 公开的接口。
值得注意的是,早期并不存在 BIOS 规范。BIOS 是事实上的标准- 它的工作方式与 20 世纪 80 年代实际的 IBM PC 上的工作方式相同。其余制造商只是进行逆向工程并制作与 IBM 兼容的 BIOS。因此,没有法规阻止 BIOS 制造商发明新的 BIOS 功能或具有重叠的功能。
统一可扩展固件接口 (UEFI)
如前所述,UEFI 本身只是一个规范,有很多实现。最广泛使用的就是 TianoCore EDK II ,它是 UEFI 和 PI 规范的开源 参考实现。虽然仅靠 EDKII 不足以创建功能齐全的启动固件,但它为大多数商业解决方案提供了坚实的基础。
为了支持不同的第一阶段引导加载程序并提供 UEFI 接口,我们使用了 UEFI Payload项目。它依赖于完成的初始设置和引导固件提供的平台信息来为 UEFI 环境准备系统。
UEFI Payload 使用 DXE 和 BDS 阶段,这两个阶段的设计与平台无关。它提供可适应不同平台的通用负载。在大多数情况下,它不需要任何自定义或针对特定平台的调整,并且可以通过使用第一阶段引导加载程序中的平台信息按原样使用。
UEFI Payload 的变体:
1.旧版 UEFI 有效负载:需要解析库来提取必要的特定于实现的平台信息。如果引导加载程序更新其 API,则有效负载也必须更新。
2.通用 UEFI 负载:遵循 通用可扩展固件 (USF) 规范,使用 可执行和可链接格式 (ELF) 或 平面映像树 (FIT) 作为通用映像格式。它不需要自己解析它们,而是希望在负载入口处接收切换块 (HOB)。
虽然 Legacy UEFI Payload运行良好,但 EDK2 社区正试图将行业转向 Universal UEFI Payload。有效载荷之间的选择取决于您的固件组件。例如,如果没有 我的补丁, 就无法在 Slim Bootloader 上运行支持 SMM 的 Legacy Payload。另一方面,将Universal Payload与 coreboot 一起使用需要一个垫片层来将 coreboot 表转换为 HOB,这是 StarLabs EDK2 fork 中才有的功能。
界面
每个符合 UEFI 标准的系统都提供了一个系统表,该表会传递给在 UEFI 环境中运行的每个代码(驱动程序、应用程序、操作系统加载器)。此数据结构允许 UEFI 可执行文件访问**系统配置表,**例如 ACPI、SMBIOS和一组 UEFI 服务。
Only available prior to OS runtime | Available before and during OS runtime |
---|
表结构在 MdePkg/Include/Uefi/UefiSpec.h 中描述:
typedef struct {
EFI_TABLE_HEADER Hdr;
CHAR16 *FirmwareVendor;
UINT32 FirmwareRevision;
EFI_HANDLE ConsoleInHandle;
EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn;
EFI_HANDLE ConsoleOutHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
EFI_HANDLE StandardErrorHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr;
// A pointer to the EFI Runtime Services Table.
// EFI_RUNTIME_SERVICES *RuntimeServices;
// A pointer to the EFI Boot Services Table.
// EFI_BOOT_SERVICES *BootServices;
UINTN NumberOfTableEntries;
EFI_CONFIGURATION_TABLE *ConfigurationTable;
} EFI_SYSTEM_TABLE;
服务包括以下类型:引导服务、运行时服务和通过协议提供的服务。
UEFI 通过设置 UEFI 协议来抽象对设备的访问。这些协议是包含函数指针的数据结构,由**全局唯一标识符 (GUID)**标识,允许其他模块找到并使用它们。它们可以通过引导服务发现。
UEFI 驱动程序生成这些协议,实际函数(不是指针!)包含在驱动程序本身中。此机制允许 UEFI 环境中的不同组件相互通信,并确保操作系统在加载其自己的驱动程序之前可以与设备交互。
虽然某些协议是在 UEFI 规范中预定义和描述的,但固件供应商也可以 * 创建自己的自定义协议 * 来扩展平台的功能。
引导服务
提供只能在启动时使用的函数。这些服务保持可用,直到调用 EFI_BOOT_SERVICES.ExitBootServices ()
函数( MdeModulePkg/Core/Dxe/DxeMain/DxeMain.c )。
指向所有启动服务的指针都存储在启动服务表( MdePkg/Include/Uefi/UefiSpec.h ) 中:
typedef struct {
EFI_TABLE_HEADER Hdr;
...
EFI_GET_MEMORY_MAP GetMemoryMap;
EFI_ALLOCATE_POOL AllocatePool;
EFI_FREE_POOL FreePool;
...
EFI_HANDLE_PROTOCOL HandleProtocol;
...
EFI_EXIT_BOOT_SERVICES ExitBootServices;
...
} EFI_BOOT_SERVICES;
运行时服务
操作系统运行时,仍有一组最少的服务可供访问。与引导服务不同,在任何有效负载(例如,操作系统引导加载程序)通过调用 EFI_BOOT_SERVICES.ExitBootServices()
控制平台后,这些服务仍然有效。
指向所有运行时服务的指针都存储在 运行时服务表 (MdePkg/Include/Uefi/UefiSpec.h) 中:
typedef struct {
EFI_TABLE_HEADER Hdr;
...
EFI_GET_TIME GetTime;
EFI_SET_TIME SetTime;
...
EFI_GET_VARIABLE GetVariable;
EFI_GET_NEXT_VARIABLE_NAME GetNextVariableName;
EFI_SET_VARIABLE SetVariable;
...
EFI_GET_NEXT_HIGH_MONO_COUNT GetNextHighMonotonicCount;
EFI_RESET_SYSTEM ResetSystem;
...
} EFI_RUNTIME_SERVICES;
下图显示了启动和运行时服务的时间线,因此您可以准确地看到每个服务何时处于活动状态。
启动设备选择 (BDS) 阶段
UEFI 规范定义了一个称为 UEFI 启动管理器的启动策略引擎。它将尝试按特定顺序加载 UEFI 应用程序。可以通过修改全局 NVRAM(非易失性随机存取存储器)变量来配置此顺序和其他设置。让我们讨论其中最重要的一些:
Boot####
(####
由一个唯一的十六进制值替换)—— 启动 / 加载选项。BootCurrent
— 用于启动当前运行系统的启动选项。BootNext
— 仅用于下次启动的启动选项。它仅用于一次BootOrder
,并在首次使用后被启动管理器删除。这允许您更改下次启动行为而不更改BootOrder
。BootOrder
— 有序的启动选项加载列表。启动管理器尝试启动此列表中的第一个活动选项。如果不成功,则尝试下一个选项,依此类推。BootOptionSupport
— 启动管理器支持的启动选项类型。Timeout
— 固件的启动管理器在自动从BootNext
或BootOrder
中选择启动值之前的超时时间(以秒为单位)。
使用 efibootmgr (8) 可以轻松从 Linux 获取这些变量:
[root@localhost ~]# efibootmgr
BootCurrent: 0000
Timeout: 5 seconds
BootOrder: 0000,0001,2001,2002,2003
Boot0000* ARCHLINUX HD (5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File (\EFI\ARCHLINUX\grubx64.efi)
Boot0001* Windows Boot Manager HD (1,GPT,6f185443-09fc-4f15-afdf-01c523565e52,0x800,0x32000)/File (\EFI\Microsoft\Boot\bootmgfw.efi) 57a94e544f5753000100000088900100780000004200430044039f0a42004a004500430054003d007b00390064006500610038003600320063002d1139006300640064002d0034006500370030102d0061006300630031002d006600330032006200330034003400640034003700390035007d00000033000300000710000000040000007fff0400
Boot0002* ARCHLINUX HD (5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)
Boot2001* EFI USB Device RC
Boot2002* EFI DVD/CDROM RC
Boot2003* EFI Network RC
让我们根据上面的代码片段来看一下启动过程。UEFI 将开始迭代 BootOrder
列表。对于列表中的每个条目,它会查找相应的 Boot####
变量 — Boot0000
表示 0000, Boot2003
表示 2003,依此类推。如果变量不存在,它将继续查找下一个条目。如果变量存在,它会读取变量的内容。每个启动选项变量都包含一个 EFI_LOAD_OPTION
描述符,它是可变长度字段的字节打包缓冲区(它只是数据结构)。
数据结构描述在 [MdePkg/Include/Uefi/UefiSpec.h][ https://github.com/tianocore/edk2/blob/edk2-stable202405/MdePkg/Include/Uefi/UefiSpec.h#L2122 )
typedef struct _EFI_LOAD_OPTION {
/// The attributes for this load option entry.
UINT32 Attributes;
/// Length in bytes of the FilePathList.
UINT16 FilePathListLength;
/// The user readable description for the load option.
/// Example: 'ARCHLINUX' / 'Windows Boot Manager' / `EFI USB Device`
CHAR16 Description [];
/// A packed array of UEFI device paths.
/// Example: 'HD (5,GPT,d03ca3cf-1511-d94e-8400-c7a125866442,0x40164000,0x100000)/File (\EFI\ARCHLINUX\grubx64.efi)'
EFI_DEVICE_PATH_PROTOCOL FilePathList [];
/// The remaining bytes in the load option descriptor are a binary data buffer that is passed to the loaded image.
/// Example: '57a9...0400' in Boot0001 variable
UINT8 OptionalData [];
} EFI_LOAD_OPTION;
此时,固件将检查设备路径( EFI_DEVICE_PATH_PROTOCOL )。在大多数情况下,我们的计算机是从存储设备(硬盘 / SSD/NVMe/ 等)启动的。因此,设备路径将包含 HD (Partition Number, Type, Signature, Start sector, Size in sectors)
节点。
-类型— 使用关键字 MBR(1) 或 GPT(2) 表示分区方案使用的格式。
-签名— 如果类型为 MBR,则为 4 字节 MBR 签名;如果类型为 GPT,则为 16 字节UUID。
- 注意:如果您对如何转换其他路径感兴趣,请阅读 UEFI 规范 v2.10,10.6.1.6 文本设备节点参考。
UEFI 将检查磁盘,看它是否有与节点匹配的分区。如果存在,则应使用特定的全局唯一标识符 (GUID) 对其进行标记,将其标记为 EFI 系统分区 (ESP) 。该分区使用文件系统格式化,其规范基于特定版本的 FAT 文件系统,名为 EFI 文件系统;实际上,它只是一个普通的FAT12/16/32. 。
- 本机启动:如果设备路径包含文件
File (\Path\To\The\File.efi)
的明确路径,则 UEFI 将查找该特定文件。例如,Boot0000
选项包含File (\EFI\ARCHLINUX\grubx64.efi)
。
-后备启动:如果设备路径只是指向一个磁盘,那么在这种情况下,固件将使用基于架构的后备启动路径—\EFI\BOOT\BOOT {arch}.EFI
(对于 amd64 为BOOTx64.EFI
或对于 i386 / IA32 为BOOTia32.EFI
)。此机制允许 * 可启动的可移动媒体 *(例如 USB 驱动器)在 UEFI 中工作;它们只是使用后备启动路径。例如,Boot0002
选项将使用此机制。
- 注意:上面提到的所有
Boot####
选项均指 * efibootmgr * 示例输出中显示的启动选项。
在这两种情况下,UEFI 启动管理器都会将 UEFI 应用程序(可能是 [OS 引导程序](https://hackernoon.com/lang/zh/ 从 - biosuefi - 到 - os - 的固件秘密之旅 #osbl)、UEFI Shell、实用程序软件、系统设置等)加载到内存中。此时,控制权将转移到 UEFI 应用程序的入口点。与BIOS不同,UEFI 应用程序可以将控制权返回给固件(除了应用程序接管系统控制的情况)。如果发生这种情况或出现任何问题,启动管理器将转到下一个 Boot####
条目,并遵循完全相同的过程。
规范中提到,启动管理器可以自动维护数据库变量。这包括删除未引用或无法解析的加载选项变量。此外,它可以重写任何有序列表以删除任何没有对应加载选项变量的加载选项。
以上内容介绍了UEFI 启动。此外,UEFI 固件还可以在模拟 BIOS 的 兼容支持模块 (CSM) 模式下运行。
操作系统引导加载程序
由固件(通常是第二阶段引导程序)启动并使用其接口加载操作系统内核的软件。它可以像操作系统一样复杂,提供以下功能:
- 从各种文件系统读取(HFS+、ext4、XFS 等)
- 通过网络进行交互(例如 TFTP、HTTP)
- 启动兼容多重.引导的内核
- 链式加载.
- 加载初始 ramdisk ( initrd )
- 还有更多!
这些程序的共同设计超出了本文的讨论范围。有关流行操作系统引导加载程序的详细比较,您可以参考 ArchLinux wiki 和 Wikipedia 文章。
Windows 系统使用其专有的操作系统引导加载程序,称为 Windows 启动管理器 (BOOTMGR) 。
固件不再是 一小段复杂的 代码。它已经变成了 大量复杂的代码,而当前的趋势只会助长这一点。我们可以在固件上运行 Doom 、 Twitter 和许多其他有趣的应用程序。
了解整体架构有助于在脑海中组织这些组件。通过检查现有固件的设计,您可以深入了解每次启动计算机时展开的迷人过程。这种自上而下的视角不仅阐明了每个部分的作用,还突出了现代固件系统的复杂和不断发展的性质。
About Author
Aleksandr Goncharov@tristejoursoir
This article has provided a lot of theoretical information related to how booting works. However, to truly understand this process, we need to take a closer look at the source code and architecture of existing firmware.
I respect & support ideas of open-source. Interested in low-level solutions (firmware/kernel) and application software.
计算机是如何启动的?
作者: 阮一峰
日期: 2013 年 2 月 16 日
从打开电源到开始操作,计算机的启动是一个非常复杂的过程。
这个过程只看见屏幕快速滚动各种提示… 到底是怎么回事?
查了一些资料,下面就是我整理的笔记。
零、boot 的含义
“启动” 用英语怎么说?
是 boot。可是,boot 原意是靴子,“启动” 与靴子有什么关系呢? 原来,这里的 boot 是 bootstrap(鞋带)的缩写,它来自一句谚语:
“pull oneself up by one’s bootstraps”.
字面意思是 “拽着鞋带把自己拉起来”,这当然是不可能的事情。最早的时候,工程师们用它来比喻,计算机启动是一个很矛盾的过程:必须先运行程序,然后计算机才能启动,但是计算机不启动就无法运行程序!
早期真的是这样,必须想尽各种办法,把一小段程序装进内存,然后计算机才能正常运行。所以,工程师们把这个过程叫做 “拉鞋带”,久而久之就简称为 boot 了。
计算机的整个启动过程分成四个阶段。
一、第一阶段:BIOS
上个世纪 70 年代初,“只读内存”(read-only memory,缩写为 ROM)发明,开机程序被刷入 ROM 芯片,计算机通电后,第一件事就是读取它。
这块芯片里的程序叫做 “基本輸出輸入系統”(Basic Input/Output System),简称为 BIOS。
1.1 硬件自检
BIOS 程序首先检查,计算机硬件能否满足运行的基本条件,这叫做 “硬件自检”(Power-On Self-Test),缩写为 POST。
如果硬件出现问题,主板会发出不同含义的 蜂鸣,启动中止。如果没有问题,屏幕就会显示出 CPU、内存、硬盘等信息。
1.2 启动顺序
硬件自检完成后,BIOS 把控制权转交给下一阶段的启动程序。
这时,BIOS 需要知道,“下一阶段的启动程序” 具体存放在哪一个设备。也就是说,BIOS 需要有一个外部储存设备的排序,排在前面的设备就是优先转交控制权的设备。这种排序叫做 “启动顺序”(Boot Sequence)。
打开 BIOS 的操作界面,里面有一项就是 “设定启动顺序”。
二、第二阶段:主引导记录
BIOS 按照 “启动顺序”,把控制权转交给排在第一位的储存设备。
这时,计算机读取该设备的第一个扇区,也就是读取最前面的 512 个字节。如果这 512 个字节的最后两个字节是 0x55 和 0xAA,表明这个设备可以用于启动;如果不是,表明设备不能用于启动,控制权于是被转交给 “启动顺序” 中的下一个设备。
这最前面的 512 个字节,就叫做 “主引导记录”(Master boot record,缩写为 MBR)。
2.1 主引导记录的结构
“主引导记录” 只有 512 个字节,放不了太多东西。它的主要作用是,告诉计算机到硬盘的哪一个位置去找操作系统。
主引导记录由三个部分组成:
(1) 第 1-446 字节:调用操作系统的机器码。
(2) 第 447-510 字节:分区表(Partition table)。
(3) 第 511-512 字节:主引导记录签名(0x55 和 0xAA)。
其中,第二部分 “分区表” 的作用,是将硬盘分成若干个区。
2.2 分区表
硬盘分区有很多 好处。考虑到每个区可以安装不同的操作系统,“主引导记录” 因此必须知道将控制权转交给哪个区。
分区表的长度只有 64 个字节,里面又分成四项,每项 16 个字节。所以,一个硬盘最多只能分四个一级分区,又叫做 “主分区”。
每个主分区的 16 个字节,由 6 个部分组成:
(1) 第 1 个字节:如果为 0x80,就表示该主分区是激活分区,控制权要转交给这个分区。四个主分区里面只能有一个是激活的。
(2) 第 2-4 个字节:主分区第一个扇区的物理位置(柱面、磁头、扇区号等等)。
(3) 第 5 个字节:主分区类型。
(4) 第 6-8 个字节:主分区最后一个扇区的物理位置。
(5) 第 9-12 字节:该主分区第一个扇区的逻辑地址。
(6) 第 13-16 字节:主分区的扇区总数。
最后的四个字节(“主分区的扇区总数”),决定了这个主分区的长度。也就是说,一个主分区的扇区总数最多不超过 2 的 32 次方。
如果每个扇区为 512 个字节,就意味着单个分区最大不超过 2TB。再考虑到扇区的逻辑地址也是 32 位,所以单个硬盘可利用的空间最大也不超过 2TB。如果想使用更大的硬盘,只有 2 个方法:一是提高每个扇区的字节数,二是 增加扇区总数。
三、第三阶段:硬盘启动
这时,计算机的控制权就要转交给硬盘的某个分区了,这里又分成三种情况。
3.1 情况 A:卷引导记录
上一节提到,四个主分区里面,只有一个是激活的。计算机会读取激活分区的第一个扇区,叫做 "卷引导记录"(Volume boot record,缩写为 VBR)。
“卷引导记录” 的主要作用是,告诉计算机,操作系统在这个分区里的位置。然后,计算机就会加载操作系统了。
3.2 情况 B:扩展分区和逻辑分区
随着硬盘越来越大,四个主分区已经不够了,需要更多的分区。但是,分区表只有四项,因此规定有且仅有一个区可以被定义成 “扩展分区”(Extended partition)。
所谓 “扩展分区”,就是指这个区里面又分成多个区。这种分区里面的分区,就叫做 “逻辑分区”(logical partition)。
计算机先读取扩展分区的第一个扇区,叫做 “扩展引导记录”(Extended boot record,缩写为 EBR)。它里面也包含一张 64 字节的分区表,但是最多只有两项(也就是两个逻辑分区)。
计算机接着读取第二个逻辑分区的第一个扇区,再从里面的分区表中找到第三个逻辑分区的位置,以此类推,直到某个逻辑分区的分区表只包含它自身为止(即只有一个分区项)。因此,扩展分区可以包含无数个逻辑分区。
但是,似乎很少通过这种方式启动操作系统。如果操作系统确实安装在扩展分区,一般采用下一种方式启动。
3.3 情况 C:启动管理器
在这种情况下,计算机读取 “主引导记录” 前面 446 字节的机器码之后,不再把控制权转交给某一个分区,而是运行事先安装的 “启动管理器”(boot loader),由用户选择启动哪一个操作系统。
Linux 环境中,目前最流行的启动管理器是 Grub。
四、第四阶段:操作系统
控制权转交给操作系统后,操作系统的内核首先被载入内存。
以 Linux 系统为例,先载入 /boot 目录下面的 kernel。内核加载成功后,第一个运行的程序是 /sbin/init。它根据配置文件(Debian 系统是 /etc/initab)产生 init 进程。这是 Linux 启动后的第一个进程,pid 进程编号为 1,其他进程都是它的后代。
然后,init 线程加载系统的各个模块,比如窗口程序和网络程序,直至执行 /bin/login 程序,跳出登录界面,等待用户输入用户名和密码。
至此,全部启动过程完成。
(完)
Linux 的启动流程
作者: 阮一峰
日期: 2013 年 8 月 17 日
上文《计算机是如何启动的?》探讨 BIOS 和主引导记录的作用,但不涉及操作系统,只与主板的板载程序有关。
这里接着往下写,探讨操作系统接管硬件以后发生的事情,也就是操作系统的启动流程。
这个部分比较有意思。因为在 BIOS 阶段,计算机的行为基本上被写死了,程序员可以做的事情并不多;但是,一旦进入操作系统,程序员几乎可以定制所有方面。所以,这个部分与程序员的关系更密切。
我主要关心的是 Linux 操作系统,它是目前服务器端的主流操作系统。下面的内容针对的是 Debian 发行版,因为我对其他发行版不够熟悉。
第一步、加载内核
操作系统接管硬件以后,首先读入 /boot
目录下的内核文件。
以我的电脑为例,/boot 目录下面大概是这样一些文件:
$ ls /boot config-3.2.0-3-amd64 config-3.2.0-4-amd64 grub initrd.img-3.2.0-3-amd64 initrd.img-3.2.0-4-amd64 System.map-3.2.0-3-amd64 System.map-3.2.0-4-amd64 vmlinuz-3.2.0-3-amd64 vmlinuz-3.2.0-4-amd64
第二步、启动初始化进程
内核文件加载以后,就开始运行第一个程序 /sbin/init,它的作用是初始化系统环境。
由于 init 是第一个运行的程序,它的进程编号(pid)就是 1。其他所有进程都从它衍生,都是它的子进程。
第三步、确定运行级别
许多程序需要开机启动。它们在 Windows 叫做 “服务”(service),在 Linux 就叫做 “[守护进程](https://zh.wikipedia.org/wiki/ 守护进程)”(daemon)。
init 进程的一大任务,就是去运行这些开机启动的程序。但是,不同的场合需要启动不同的程序,比如用作服务器时,需要启动 Apache,用作桌面就不需要。Linux 允许为不同的场合,分配不同的开机启动程序,这就叫做 “[运行级别](https://zh.wikipedia.org/wiki/ 运行级别)”(runlevel)。也就是说,启动时根据 “运行级别”,确定要运行哪些程序。
Linux 预置七种运行级别(0-6)。一般来说,0 是关机,1 是单用户模式(也就是维护模式),6 是重启。运行级别 2-5,各个发行版不太一样,对于 Debian 来说,都是同样的多用户模式(也就是正常模式)。
init 进程首先读取文件 /etc/inittab,它是运行级别的设置文件。如果你打开它,可以看到第一行是这样的:
id:2:initdefault:
initdefault 的值是 2,表明系统启动时的运行级别为 2。如果需要指定其他级别,可以手动修改这个值。
那么,运行级别 2 有些什么程序呢,系统怎么知道每个级别应该加载哪些程序呢?… 回答是每个运行级别在 /etc 目录下面,都有一个对应的子目录,指定要加载的程序。
/etc/rc0.d /etc/rc1.d /etc/rc2.d /etc/rc3.d /etc/rc4.d /etc/rc5.d /etc/rc6.d
上面目录名中的 “rc”,表示 run command(运行程序),最后的 d 表示 directory(目录)。下面让我们看看 /etc/rc2.d 目录中到底指定了哪些程序。
$ ls /etc/rc2.d README S01motd S13rpcbind S14nfs-common S16binfmt-support S16rsyslog S16sudo S17apache2 S18acpid ...
可以看到,除了第一个文件 README 以外,其他文件名都是 “字母 S + 两位数字 + 程序名” 的形式。字母 S 表示 Start,也就是启动的意思(启动脚本的运行参数为 start),如果这个位置是字母 K,就代表 Kill(关闭),即如果从其他运行级别切换过来,需要关闭的程序(启动脚本的运行参数为 stop)。后面的两位数字表示处理顺序,数字越小越早处理,所以第一个启动的程序是 motd,然后是 rpcbing、nfs… 数字相同时,则按照程序名的字母顺序启动,所以 rsyslog 会先于 sudo 启动。
这个目录里的所有文件(除了 README),就是启动时要加载的程序。如果想增加或删除某些程序,不建议手动修改 /etc/rcN.d 目录,最好是用一些专门命令进行管理。
参考这里:
-
Manage Linux init or startup scripts | Debian Admin Posted on December 7, 2006 by ruchi
https://www.debianadmin.com/manage-linux-init-or-startup-scripts.html -
Remove Unwanted Startup Files or Services | Debian Admin Posted on September 12, 2006 by ruchi
https://www.debianadmin.com/remove-unwanted-startup-files-or-services-in-debian.html
第四步、加载开机启动程序
前面提到,七种预设的 “运行级别” 各自有一个目录,存放需要开机启动的程序。不难想到,如果多个 “运行级别” 需要启动同一个程序,那么这个程序的启动脚本,就会在每一个目录里都有一个拷贝。这样会造成管理上的困扰:如果要修改启动脚本,岂不是每个目录都要改一遍?
Linux 的解决办法,就是七个 /etc/rcN.d 目录里列出的程序,都设为链接文件,指向另外一个目录 /etc/init.d ,真正的启动脚本都统一放在这个目录中。init 进程逐一加载开机启动程序,其实就是运行这个目录里的启动脚本。
下面就是链接文件真正的指向。
$ ls -l /etc/rc2.d README S01motd -> ../init.d/motd S13rpcbind -> ../init.d/rpcbind S14nfs-common -> ../init.d/nfs-common S16binfmt-support -> ../init.d/binfmt-support S16rsyslog -> ../init.d/rsyslog S16sudo -> ../init.d/sudo S17apache2 -> ../init.d/apache2 S18acpid -> ../init.d/acpid ...
这样做的另一个好处,就是如果你要手动关闭或重启某个进程,直接到目录 /etc/init.d 中寻找启动脚本即可。比如,我要重启 Apache 服务器,就运行下面的命令:
$ sudo /etc/init.d/apache2 restart
/etc/init.d 这个目录名最后一个字母 d,是 directory 的意思,表示这是一个目录,用来与程序 /etc/init 区分。
第五步、用户登录
开机启动程序加载完毕以后,就要让用户登录了。
一般来说,用户的登录方式有三种:
(1)命令行登录
(2)ssh 登录
(3)图形界面登录
这三种情况,都有自己的方式对用户进行认证。
(1)命令行登录:init 进程调用 getty 程序(意为 get teletype),让用户输入用户名和密码。输入完成后,再调用 login 程序,核对密码(Debian 还会再多运行一个身份核对程序 /etc/pam.d/login
)。如果密码正确,就从文件 /etc/passwd 读取该用户指定的 shell,然后启动这个 shell。
(2)ssh 登录:这时系统调用 sshd 程序(Debian 还会再运行 /etc/pam.d/ssh ),取代 getty 和 login,然后启动 shell。
(3)图形界面登录:init 进程调用显示管理器,Gnome 图形界面对应的显示管理器为 gdm(GNOME Display Manager),然后用户输入用户名和密码。如果密码正确,就读取 /etc/gdm3/Xsession,启动用户的会话。
第六步、进入 login shell
所谓 shell,简单说就是命令行界面,让用户可以直接与操作系统对话。用户登录时打开的 shell,就叫做 login shell。
Debian 默认的 shell 是 Bash,它会读入一系列的配置文件。上一步的三种情况,在这一步的处理,也存在差异。
(1)命令行登录:首先读入 /etc/profile,这是对所有用户都有效的配置;然后依次寻找下面三个文件,这是针对当前用户的配置。
~/.bash_profile ~/.bash_login ~/.profile
需要注意的是,这三个文件只要有一个存在,就不再读入后面的文件了。比如,要是~/.bash_profile 存在,就不会再读入后面两个文件了。
(2)ssh 登录:与第一种情况完全相同。
(3)图形界面登录:只加载 /etc/profile 和~/.profile。也就是说,~/.bash_profile 不管有没有,都不会运行。
第七步,打开 non-login shell
老实说,上一步完成以后,Linux 的启动过程就算结束了,用户已经可以看到命令行提示符或者图形界面了。但是,为了内容的完整,必须再介绍一下这一步。
用户进入操作系统以后,常常会再手动开启一个 shell。这个 shell 就叫做 non-login shell,意思是它不同于登录时出现的那个 shell,不读取 /etc/profile 和.profile 等配置文件。
non-login shell 的重要性,不仅在于它是用户最常接触的那个 shell,还在于它会读入用户自己的 bash 配置文件~/.bashrc。大多数时候,我们对于 bash 的定制,都是写在这个文件里面的。
你也许会问,要是不进入 non-login shell,岂不是.bashrc 就不会运行了,因此 bash 也就不能完成定制了?事实上,Debian 已经考虑到这个问题了,请打开文件~/.profile,可以看到下面的代码:
if [ -n "$BASH_VERSION" ]; then if [ -f "$HOME/.bashrc" ]; then . "$HOME/.bashrc" fi fi
上面代码先判断变量 $BASH_VERSION 是否有值,然后判断主目录下是否存在 .bashrc 文件,如果存在就运行该文件。第三行开头的那个点,是 source 命令的简写形式,表示运行某个文件,写成 “source ~/.bashrc” 也是可以的。
因此,只要运行~/.profile 文件,~/.bashrc 文件就会连带运行。但是上一节的第一种情况提到过,如果存在~/.bash_profile 文件,那么有可能不会运行~/.profile 文件。解决这个问题很简单,把下面代码写入.bash_profile 就行了。
if [ -f ~/.profile ]; then . ~/.profile fi
这样一来,不管是哪种情况,.bashrc 都会执行,用户的设置可以放心地都写入这个文件了。
Bash 的设置之所以如此繁琐,是由于历史原因造成的。早期的时候,计算机运行速度很慢,载入配置文件需要很长时间,Bash 的作者只好把配置文件分成了几个部分,阶段性载入。系统的通用设置放在 /etc/profile,用户个人的、需要被所有子进程继承的设置放在.profile,不需要被继承的设置放在.bashrc。
顺便提一下,除了 Linux 以外, Mac OS X 使用的 shell 也是 Bash。但是,它只加载.bash_profile,然后在.bash_profile 里面调用.bashrc。而且,不管是 ssh 登录,还是在图形界面里启动 shell 窗口,都是如此。
参考链接
[1] Debian Wiki, Environment Variables
[2] Debian Wiki, Dot Files
[3] Debian Administration, An introduction to run-levels
[4] Debian Admin,Debian and Ubuntu Linux Run Levels
[5] Linux Information Project (LINFO), Runlevel Definition
[6] LinuxQuestions.org, What are run levels?
[7] Dalton Hubble, Bash Configurations Demystified
(完)
via:
-
固件的隐藏世界:探索计算机的启动过程 Aleksandr Goncharov , 2023/04/21
https://hackernoon.com/lang/zh/固件的隐藏世界探索您的计算机启动过程 -
固件秘密之旅:从 BIOS/UEFI 到 OS Aleksandr Goncharov ,2024/08/22
https://hackernoon.com/lang/zh/从-biosuefi-到-os-的固件秘密之旅 -
计算机是如何启动的? - 阮一峰的网络日志 作者: 阮一峰 日期: 2013 年 2 月 16 日
https://www.ruanyifeng.com/blog/2013/02/booting.html -
Linux 的启动流程 - 阮一峰的网络日志 作者: 阮一峰 日期: 2013 年 8 月 17 日
https://www.ruanyifeng.com/blog/2013/08/linux_boot_process.html