# Linux启动过程：从电源按钮到内核

> 完整解析Linux系统从按下电源按钮到内核初始化的全过程，涵盖BIOS/UEFI引导流程、Bootloader（GRUB）工作原理、内核解压与初始化、根文件系统挂载以及systemd启动第一个用户空间进程的技术细节，深入理解操作系统启动的每一个关键环节

- Canonical: https://www.zeroxin.xin/2025/11/09/linux-boot-process/
- Published: 2025-11-09T00:00:00+08:00
- Updated: 2026-06-18T17:19:20+08:00
- Categories: 系统技术
- Tags: Linux, 系统, 启动过程, 操作系统, 内核

> 原文地址：[The Linux Boot Process: From Power Button to Kernel](https://www.0xkato.xyz/linux-boot/)
> 
> 作者：0xkato  
> 本文为中文翻译版本，旨在分享这篇优秀的Linux启动过程技术文章。

## 引言

你按下电源按钮。一秒钟后，一墙文本滚过，或是一个logo淡入，最终Linux出现了。其间发生的事情并非魔法。这是tiny程序和一个非常 literal 的CPU之间的精心握手。这部分将追踪这个握手过程，直到Linux内核内部的第一行C代码运行。

## 第一部分 — 从电源按钮到内核的第一次呼吸

### 最早的指令

当电源稳定时，CPU将自己重置为一个称为实模式的小型、老式的模式。实模式可以追溯到最初的8086芯片。规则设计得很简单。内存地址由CPU保存在称为寄存器的特殊快速存储中的两个值构建。你这样组合段和偏移量：

```
physical_address = (segment << 4) + offset
```

如果你看到像`0xFFFFFFF0`这样的数字，那是十六进制。Hex是16进制。我们写`0x`在前面来明确这一点。`0x10`是日常计数中的16。`0x100000`是1兆字节。十六进制与硬件如何存储位很好地对齐，这就是为什么你在低级代码中到处看到它的原因。

复位后，CPU跳转到称为复位向量的特殊地址`0xFFFFFFF0`。把它想象成一个永久书签，说"从这里开始"。在那个地址几乎没有空间，所以制造商在那里放了一个远（长）跳转，将控制权传递给主板上的固件。

小解释：寄存器  
寄存器是CPU内部的一个小槽。它保存CPU正在使用的数字。像CS和IP这样的名称是寄存器名称。CS意思是"代码段"，标记指令的当前邻域。IP意思是"指令指针"，标记接下来要执行哪条指令。

### BIOS和UEFI

固件是烘焙在你主板上的一个小启动程序。

BIOS代表基本输入输出系统。它是较旧的样式。BIOS执行一个称为POST的快速健康检查，查看启动顺序，并尝试每个设备。如果它找到其第一个512字节扇区以标记字节`0x55`和`0xAA`结尾的磁盘，它将该设备视为可启动。BIOS将该扇区复制到内存中`0x7C00`并跳转到那里。那个扇区很小，所以它通常只知道如何加载下一个更大的片段。

UEFI是现代替代品。它仍然启动机器，但直接理解文件系统，并可以加载更大的引导程序，而不需要旧的"第一扇区"舞蹈。UEFI还向操作系统传递更丰富的信息。不同的路径，相同的目标：将控制权交给能够加载Linux的引导程序。

### 认识引导加载程序

引导加载程序是让操作系统就位的引导员。GRUB是PC上的流行选择。它读取其配置，如果你安装了一个会显示菜单，并将Linux内核加载到内存中。Linux内核文件实际上包含两样东西：

• 一个仍然在实模式下运行的小设置程序
• 稍后将被解压缩的较大的压缩内核

GRUB还填充了一个称为设置头的小结构，包含有用的事实：
它将内核放在哪里，命令行在哪里，如果有initrd的话它在哪里。然后它跳转到设置程序。

### 设置程序创建一个安全的空间

在任何Linux可以做任何有趣的事情之前，设置代码创建一个可预测的工作空间。

它对齐段寄存器，使得内存复制每次都以相同的方式进行。你在这里会看到的名称是CS表示代码，DS表示数据，SS表示栈。它还清除一个称为"方向标志"的单个CPU位，以便复制指令向前通过内存移动。

它创建一个栈。栈是一个后进先出的工作台，函数在其中存储临时值。SS表示栈使用哪个段。SP是栈当前顶部的指针。

它清除一个称为BSS的区域。BSS是必须以零开始的全局变量所在的地方。C代码假设BSS为零。设置程序将零写入整个跨度以保持这个承诺。

如果你在内核命令行上传入了`earlyprintk`，设置代码还会对串行端口进行编程，这样它就可以打印非常早期的消息。这在图形尚未准备就绪时很有用。

最后，设置程序询问固件"我们实际上有多少可用RAM以及空洞在哪里。"在旧BIOS上，这是一个人们经常昵称为e820的调用，它返回一个简单的可用和保留范围列表。内核将使用该列表避免踩到固界的脚趾。

完成后，设置代码调用其第一个C函数 literally 名为`main`。我们仍然在小的旧实模式中。下一步工作是离开它。

小解释：中断  
中断是硬件或软件的"打扰一下"，暂停CPU正在做的事情并为紧急事情运行一个小处理程序。定时器tick是一个中断。键击是一个中断。这里有两种风格。可屏蔽中断遵循你的规则，可以被临时阻止，以便它们在关键时刻不触发。非屏蔽中断，通常称为NMI，总是切入，因为它们通常报告严重的硬件问题。我们将在切换模式时控制两者，这样就不会有东西在半路上让我们惊讶。

## 第二部分 — 离开实模式，穿越32位土地，到达64位

现代Linux在PC上以长模式运行，即x86_64的64位模式。你不能从实模式直接跳转到那里。路径是实模式到保护模式，然后保护模式到长模式。这部分涵盖那条路径并解释沿途的词汇。

### 保护模式，没有术语迷雾

保护模式是1980年代引入的32位世界，用于超越当时的限制。它添加了两个核心思想。

全局描述符表或GDT是一个短描述符列表。一个描述说"这个段从这里开始，覆盖这么多，并被允许做这些事情。"Linux保持这个简单。它使用平面模型，这意味着基址为零，大小覆盖整个32位空间。当一切都平坦时，地址看起来像普通数字一样。

中断描述符表或IDT是"紧急呼叫电话号码"的目录。如果中断到达，CPU在IDT中查找条目并跳转到列出的处理程序。在切换期间，我们加载一个小的占位符IDT，因为我们即将阻止中断。全功能IDT稍后会到达，一旦真正的内核负责。

### 小心的切换

设置代码首先关闭嘈杂的部分。它用一条指令禁用可屏蔽中断。它使旧的PIC芯片安静下来，这样硬件中断在瞬间被完全阻塞。它打开A20行。这是一个历史遗留问题。早期PC使得地址在1兆字节处环绕。打开A20移除那个环绕，所以更高的地址按你期望的方式工作。它重置数学协处理器，使浮点状态干净。

然后它加载一个只有我们现在需要的小GDT和一个小IDT。最后它在称为CR0的控制寄存器中设置一个称为PE的单个位并执行远跳转。该跳转从GDT重新加载代码段并锁定保护模式。它重新加载数据段和栈段，并修复栈指针以匹配新的平面世界。

我们现在处于32位保护模式。

小解释：控制寄存器  
CPU有一些特殊的开/关开关寄存器。CR0打开保护模式。CR3保存页表顶部的地址，我们马上就会需要。CR4启用更大的页表条目等扩展功能集。

### 为什么我们仍然没有完成

Linux想要64位。那是长模式。需要两样东西。

分页必须打开。分页是虚拟地址和物理地址之间的转换器。程序使用虚拟地址。硬件读取和写入物理内存。页表以称为页的固定大小块将一个映射到另一个。在PC上，正常页面是4KB。还有更大的页面。启动早期，内核使用2兆字节页面快速描述低内存。

在称为EFER的特殊寄存器中必须设置一个称为LME的单个位以允许长模式。EFER是型号特定寄存器，这是一种说"用于某些CPU功能的寄存器"的优雅方式。

### 构建足够的分页

32位序言构建一个小的页表集，说"对于这个区域，虚拟等于物理。"这称为身份映射。足以安全地打开分页。

为了使这工作，代码在CR4中启用PAE以使用更大的条目。它构建覆盖低内存2兆字节块的最小表集。它将顶层表的地址写入CR3。分页现在已准备就绪。

最后它在EFER中设置LME并执行远返回到一个写为64位代码的标签。长模式现在处于活动状态。段仍然是"平坦的"，但地址和寄存器是64位宽。

为什么所有额外的小心  
在运行系统切换模式就像在滚动时更换轮胎一样。代码阻止中断，准备所需的最小表，翻转位，然后才邀请中断返回。慢而稳阻止了奇怪的半切换状态。

## 第三部分 — 解压真实内核，修复地址，以及为什么Linux有时会移动自己

我们有一个64位CPU，分页打开，内存中有一个压缩内核。现在小的64位存根做实际工作：如果需要就离开，解压内核，如果内核不在其默认位置就修复地址，然后跳转。

### 清理路径并设置安全网

存根首先弄清楚它实际在哪里运行。早期代码被链接为好像它住在地址零，然后在运行时计算其真实基址。如果解压缩内核的计划目标位置会与存根重叠，它将自己复制到安全地方。

它清除自己的BSS，以便全局状态从干净开始。

它用两个处理程序加载一个最小IDT。一个用于页面错误，一个用于NMI。当CPU无法找到它刚刚尝试使用的虚拟地址的映射时，会发生页面错误。在我们的早期身份映射世界中，小页面错误处理程序可以动态添加缺失的映射并继续。NMI处理程序在那里，这样当我们在启动时，NMI不会使机器崩溃。

它还为其下一步将触及的区域构建身份映射。这包括内核的未来家园、引导加载程序填充的小引导参数页面和命令行缓冲区。

### 解压Linux…

通常名为`extract_kernel`的C函数接管。它为临时缓冲区设置一个小堆，打印经典行，并使用内核构建时使用的任何算法解压缩内核。gzip、xz、zstd、lzo和其他都插入到同一个包装器中。

当字节出来后，解压器读取内核的ELF头。ELF是Executable and Linkable Format的缩写，既是文件格式也是地图。它说哪些块是代码，哪些是数据，以及每个块确切地想要住在哪里。解压器将每个块复制到它所属的地方。

如果内核被加载到与它构建的地址不同的地址，解压器应用重定位。重定位是一个小的修复，调整包含地址的指针或指令。解压器遍历这些的列表并修补每个地方，以便它指向我们在地址空间中实际使用的正确位置。

当一切都到位时，解压器返回真正内核的入口点并跳转，将指向引导参数的指针传递过去。从那一刻起你就处于完整内核中了。你遇到的第一个函数是`start_kernel`，大的初始化开始了。

### 为什么内核有时会故意移动自己

你可能在内核日志中看到kASLR提到。那是内核地址空间布局随机化。想法很简单。如果攻击者不知道内核在内存中住在哪里，某些攻击就变得困难得多。

在启动早期，如果kASLR启用，解压器随机选择两个"基址"：

• 物理基址，即字节在RAM中住的地方
• 虚拟基址，即一旦完整分页设置好内核将使用的起始虚拟地址

它如何在不破坏任何东西的情况下选择

它构建一个不要触摸列表。这包括解压器本身、压缩图像、initial ramdisk、引导参数页面和命令行缓冲区。它还可以包括你用命令行上的`memmap=`选项保留的范围。

它从固件扫描内存映射以找到大空闲区域。对于每个空闲区域，它计算有多少对齐的"槽"适合正确的大小。它使用它拥有的最佳早期熵源绘制随机数。在现代CPU上可能是硬件随机指令。它将数字减少到槽的总数并选择匹配的槽。那成为物理基址。虚拟基址以相同方式选择，但在内核的虚拟地址窗口内。

如果不存在合适的东西，代码回退到默认地址并打印小警告。如果你在命令行上传入`nokaslr`，设计会跳过随机化步骤。

## 快速词汇表

**十六进制**：用`0x`书写的16进制数字。`0x10`是16。`0x100000`是1兆字节。十六进制与位映射清洁，这就是低级代码使用它的原因。

**寄存器**：CPU内部保存当前数字的小槽。例子：CS、DS、SS、IP、SP。

**段和偏移**：用于构建实模式地址的两部分。物理地址等于段乘以16加偏移。

**BIOS**：启动机器、检查硬件并将第一个引导扇区加载到内存的较旧固件。

**UEFI**：理解文件系统并直接加载更大引导程序的现代固件。

**引导加载程序**：将内核放入内存并将系统事实传递给它的引导员。GRUB是常见的一个。

**栈**：函数的后进先出工作台。SS选择其段。SP指向当前顶部。

**BSS**：必须以零开始的全局变量所在的内核区域。设置代码在C运行之前清除它。

**中断**：来自硬件或软件的快速"打扰一下"。CPU暂停，运行小处理程序，然后恢复。可屏蔽中断可以被阻止片刻。NMI不能。

**GDT**：全局描述符表。段描述符的短列表。Linux将其设置为简单平面模型。

**IDT**：中断描述符表。处理程序的中断目录。早期引导使用最小的一个。真正的内核稍后安装真正的。

**A20行**：必须打开以在旧PC上正确寻址1兆字节以上地址的历史开关。

**保护模式**：引入GDT和IDT并允许分页的32位模式。

**长模式**：x86_64上的64位模式。需要分页和在EFER寄存器中设置名为LME的位。

**分页**：从虚拟地址到物理内存的转换器。用页表实现。

**页表**：将虚拟页面映射到物理页面的数据结构。早期引导使用身份映射。正常页面是4KB。早期引导经常使用2MB页面快速覆盖区域。

**CR0、CR3、CR4**：控制寄存器。CR0打开保护模式。CR3指向页表顶部。CR4启用PAE等扩展功能。

**EFER**：保存长模式启用以及其他位的型号特定寄存器。

**ELF**：内核的磁盘格式，带有内置的什么属于哪里的地图。

**重定位**：当代码加载到与其构建的基址不同的基址时调整地址的修复。

**kASLR**：在启动时随机化内核基址地址以使开发更加困难。

---

## 相关阅读

- [Linux进程内存友好之旅](https://www.0xkato.xyz/linux-process-memory/)

---

*本文为技术翻译文章，原始文章作者为0xkato，发表在0xkato.xyz上。翻译旨在学习和分享，如需查看原文请访问上述链接。*

