> For the complete documentation index, see [llms.txt](https://freebsd-journal-cn.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://freebsd-journal-cn.bsdcn.org/2023-1112-freebsd-14.0/linuxboot-cong-linux-qi-dong-freebsd.md).

# LinuxBoot：从 Linux 启动 FreeBSD

* 原文：[LinuxBoot: Booting FreeBSD from Linux](https://freebsdfoundation.org/our-work/journal/browser-based-edition/freebsd-14-0/linuxboot-booting-freebsd-from-linux/)
* 作者：**Warner Losh**

## 演进历程

三大主题推动了 LinuxBoot 日益普及：最初的简洁、不受控的膨胀，以及回归简单的愿望。这三个主题共同造就了 x86 和嵌入式系统上错综复杂的引导生态系统。LinuxBoot 试图在一定程度上简化这些生态系统，不过把整个 Linux 内核搬进来，通常不会让人立刻联想到 “简洁”。

在 IBM PC 问世之前，大多数系统要么需要手动输入引导程序，要么自动引导 ROM 简单到只加载一个引导扇区，再由该扇区加载系统的其余部分。这种用简单的加载器逐级加载更复杂加载器的过程，称为系统 “自举”（bootstrapping），源自谚语 “拉着靴带把自己提起来”。后来这个词被简称为 “引导”（booting）。

1982 年，IBM 推出 IBM PC，在 ROM 中同时提供引导代码和其他基本 I/O 服务，比先前的系统略有改进。IBM 借用前代 CP/M 系统的术语，将这些服务称为 BIOS。这正是 “BIOS” 一词的由来，也解释了围绕该词的困惑——它究竟指 “用于引导系统的固件”，还是仅指 PC 平台上 UEFI 之前的引导方式。两派支持者都笃信自己正确，但鲜有人知这段历史背后的来龙去脉。因此，本文用 “CSM” 或 “CSM 引导” 来指代这种引导方式。UEFI 标准使用 “CSM” 描述传统引导，含义明确无歧义。

随着时间推移，CSM 引导不断增加新功能：磁盘分区表、处理器配置用的 MP Table、电源管理用的 APM、系统元数据用的 SMBIOS、PCI 运行时服务、PXE 网络引导，以及统一上述功能的 ACPI。这些服务的接口都与 x86 特有的机制绑定。整个系统演变成一个极其复杂的生态系统，充斥着权宜之计、特殊情况和微妙不同的解释。

1990 年代后期，Intel 设计 IA-64 CPU 架构时，很快发现这一革命性架构无法使用 CSM 生态系统中的绝大多数技术。整个引导生态系统必须被替换。这就是统一可扩展固件接口（UEFI）引导的起源。起初，它仅用于 Intel x86（改称 IA-32）和 IA-64 架构。2000 年代初期，UEFI 固件逐步取代了 Intel x86 系统上的传统固件，通常能够同时支持旧的 CSM 引导和新的 UEFI 引导。到 2006 年，Intel 通过 TianoCore 项目发布了 EDK2 开源开发套件，用于创建 UEFI 固件，促使更多 OEM 厂商采纳 UEFI。

与此同时，在 1990 年代和 2000 年代，嵌入式系统上也在演化另一个引导生态系统。起初，嵌入式领域有数十种不同的引导加载器，它们与后续引导阶段的接口各不相同。Magnus Damm 和 Wolfgang Denk 于 1999 年创建了 Das U-Boot，最初面向 PowerPC，后来扩展到 ARM、MIPS 等架构。U-Boot 起步时小巧简洁，比竞争对手更灵活，并且因为是开源的，相对容易扩展。凭借简洁、良好的支持和丰富的功能集，它迅速成为通用引导加载器。它树立了引导的标准，并推动了 Linux 的许多功能，包括扁平设备树（FDT）支持。这套引导系统与 CSM 或 UEFI 毫无共同之处。它极其易用，所有竞争对手都逐渐销声匿迹。redboot、eCos、CFE、yaboot、YAMON 如今在哪里？充其量不过是维基百科页面上的脚注。

2011 年，ARM 推出了 aarch64——ARM 平台的 64 位版本。U-Boot 和 EDK2 争夺系统引导的主导权，同时 FDT 和 ACPI 争夺系统设备枚举的主导权。低端嵌入式系统倾向于使用 U-Boot 配合 FDT，而高端服务器级系统则使用 UEFI 和 ACPI。最终，UEFI 引导开始胜出，尤其是当 U-Boot 开始提供足以通过 UEFI 引导 Linux 的最小 UEFI 实现之后。ACPI 和 FDT 融合（现在可以用 FDT 属性指定 ACPI 节点）。在此过程中，EDK2/UEFI 变得越来越复杂，以支持 SecureBoot、iSCSI、更多网卡、RAM disk 支持、initramfs 支持等太多无法一一列举的功能。

这还没有算上那些使用精简版 Linux 内核的引导加载器，比如 coreboot、slim boot、LinuxBIOS 等，其中一些将在下文介绍。也无法描述商业 BIOS 之间的差异。到 2017 年，Google 决定改变这种状况，启动了 NERF 项目来简化这一团乱麻并加强安全性。NERF 全称是 Non-Extensible Reduced Firmware（不可扩展精简固件），与 UEFI 的 Unified Extensible Firmware Interface（统一可扩展固件接口）相对。它也是电子游戏中的俚语，指削弱某个游戏元素的威力或影响力，以实现更好的平衡或提升游戏体验。这些努力后来演变成了 LinuxBoot。

## Linux 引导

用 Linux 引导 Linux 的历史非常悠久，但篇幅有限，只能简要概述。1990 年代中期，**kexec(2)** 系统调用家族被加入 Linux，用于提高服务器和嵌入式系统的正常运行时间和可靠性。Ron Minnich 和 Eric Biederman 于 1990 年代在洛斯阿拉莫斯启动了 LinuxBIOS 项目，旨在用固件中的 Linux 内核来引导系统。这个项目演变成了 coreboot，被 Chromebook 和多款开放平台笔记本电脑使用。在此过程中，coreboot 变得模块化，允许二进制 blob 与开源组件并存，因为 CPU 厂商拒绝开放早期处理器初始化代码，只向开源和闭源固件开发者提供二进制 blob。EDK2、U-Boot 和闭源固件也发展出了模块化系统，允许这些二进制 blob 与其他组件共存。

## NERF 项目成为 LinuxBoot

Google 的 NERF 项目由 Ron Minnich 领导，演变成了 LinuxBoot 项目：一系列脚本，用于创建以 Linux 内核引导最终操作系统的固件镜像。然而，该项目背后有更大的愿景。Google 想要创建一个每个组件都自由可用的开源固件。他们希望通过用经过加固的 Linux 内核替换 UEFI 引导环境来简化它——他们认为 UEFI 引导环境已经变得过于复杂，存在太多潜在的安全漏洞。他们想要创建一个使用广泛部署和经过审查的代码的通用框架；最小化不可避免的、仅含二进制的、无源码的引导加载器部分；尽可能统一 ARM 和其他嵌入式系统的引导；消除冗余代码以加快引导速度；以及创建比传统固件甚至 EDK2 提供的更模块化、更可定制的引导体验。他们还希望实现可重现构建，确保任何人都能运行完全相同的二进制文件，无论是下载的还是自行构建的。

结果是一个支持多种引导加载器的模块化系统。初始化 CPU 的最早阶段由 CPU 特定和引导加载器特定的代码处理。LinuxBoot 定义了哪些系统部件在此阶段初始化，哪些延迟到 Linux 内核处理。这种设置允许 CPU 厂商继续提供仅含二进制的 blob 来初始化底层时钟、内存控制器、辅助核心等现代 CPU 运行所需的部件。EDK2、coreboot、U-Boot 和 slim boot 都支持这些协议，因此 Linux 内核可以在所有这些平台上引导，而不需要任何特殊代码。LinuxBoot 还提供了 u-root——一个用 Go 编写的 ramfs 构建器，用于查找和加载最终操作系统，以及一些操作固件镜像的工具。这些工具的使用方法将在本文第二部分介绍。

来源：<https://www.linuxboot.org>

虽然未能完全用 Linux 替换整个引导加载器，但 LinuxBoot 将保留的部分降到了最低。例如，对于 UEFI，只保留了初始化处理器、缓存和 RAM 的 Pre-EFI Initialization（PEI）阶段以及 UEFI 运行时服务。

LinuxBoot 消除了所有测试不充分的 UEFI DEX 驱动。Linux 内核在内存和基本硬件初始化完成后接管，但不初始化传统固件可能初始化的其他内容，比如 PCI 设备的资源。

除了更好的安全性和对固件的更多控制外，LinuxBoot 还使用经过充分测试和高度审查的 Linux 驱动程序，这些驱动是 Linux 在平台上运行所必需的。借助 LinuxBoot，SOC 厂商和系统集成商可以只为 Linux 编写驱动程序来优化上市时间。完全不需要创建 UEFI DEX 驱动。具备 Linux 驱动开发技能的程序员比能写 UEFI DEX 驱动的程序员容易找得多。Linux 内核已经过数千名研究人员的审计，而研究过 EDK2 UEFI 代码库的人相对较少。然而，这些优势要求其他支持 UEFI 的操作系统做出适配。它们的 UEFI 引导加载器无法与仅存的那一小部分 UEFI 协同工作。这意味着，要在这些系统上引导，操作系统必须创建新的加载器来支持 LinuxBoot。

一些较简单的操作系统使用 Linux `kexec-tools` 包提供的基本 ELF 加载功能来通过 LinuxBoot 引导。非常旧版本的 BSD 和 Plan9 就是这样引导的。FreeBSD/powerpc 运行在来自更简单时代的处理器上，具有定义明确的 OpenFirmware 接口，也以这种方式加载。然而，Windows 无法以这种方式引导，LinuxBoot 社区正在研究绕过的方法。FreeBSD/amd64 和 FreeBSD/aarch64 也无法以这种方式引导。

amd64 和 aarch64 的 FreeBSD 内核需要只有引导加载器才能获取的元数据。在 amd64 上，引导加载器在捕获有关系统内存布局和其他数据的信息后，将系统置入长模式，这些信息只能在进入长模式之前获取。内核依赖这些数据，没有它们就无法运行。在 amd64 和 aarch64 上，引导加载器必须向内核告知 UEFI 系统表和其他系统数据的地址。引导加载器通过设置 `tuneables` 来调整内核。加载器预加载动态内核模块，并将初始熵、UUID 等信息传入内核。`kexec-tools` 不具备这些专门知识，它只能加载 ELF 二进制文件并跳转到其起始地址。

## FreeBSD 与 LinuxBoot

FreeBSD 通过 Linux 引导的历史可以追溯到十多年前。2010 年，FreeBSD PS/3 移植版使用 PS/3 的 “另一个操作系统” 选项来启动。FreeBSD 开发者 Nathan Whitehorn 在 FreeBSD 引导加载器中添加了必要的胶水代码，为 FreeBSD 内核设置内存。他创建了一个类似 Ubuntu PS/3 支持包中 kboot 的小型 Linux 二进制文件。这个 Linux 二进制文件是静态链接的，包含从 PS/3 磁盘读取 FreeBSD 内核所需的少量系统调用。它包含一个小型 libc（类似 musl 或 glibc）和命令行解析支持。然而，其源码结构仅针对 PowerPC。

在 Netflix 工作期间，我于 2020 年开始试验从 Linux 引导 FreeBSD 有多困难。Netflix 在全球部署了大量服务器。经过多年持续改进，Netflix 创建了一套非常健壮的系统，能够自动纠正常见问题。即便经过这些改进，引导问题仍导致了太多昂贵的 RMA。使用 UEFI 脚本改善引导时可靠性的实验仅带来了边际改善。由于脚本存储在闪存驱动器上，只有少数简单情况得到改善。闪存驱动器可能以只读方式故障，导致 UEFI 固件和脚本都做出错误行为。只要脚本还留在驱动器上，就无法取得进展。

LinuxBoot 为 UEFI 提供了有吸引力的替代方案，因为它驻留在主板固件中，消除了最容易故障的组件。Netflix 希望我创建一个故障安全环境，能够回传机器状态信息，使用幸存的 NVMe 驱动器重新配置机器，并提供灵活的平台来支持远程调试、诊断镜像等。

从 Linux 引导 FreeBSD，我有以下几个目标：

1. 必须在 FreeBSD 构建系统内构建。
2. 必须提供对主机资源的完全访问。
3. 必须使用原生内核引导（如果可能）。
4. 必须使用 UEFI 引导接口（不支持 i386 CMS 引导和 ARM U-Boot 二进制引导）。
5. 必须作为 init/PID 1 运行。
6. 还要在从 shell 脚本调用时运行良好，以支持引导不一定基于 FreeBSD 的不同类型镜像。

要在 amd64 和 aarch64 等现代架构上从 Linux 引导 FreeBSD，需要对相对简陋的 PS/3 kboot 基础做几项改动。加载器需要四类改动：按照 MI/MD 模型重构现有 kboot，扩展对主机资源访问的支持，重构 UEFI 引导代码使其同时供 `loader.efi` 和 LinuxBoot `loader.kboot` 使用，以及偿还引导加载器中的技术债务。

## MI/MD 改动

几个领域需要经典的 MI/MD 拆分：公共 MI 代码与实现公共 API 的每架构 MD 代码接口。Linux 各架构之间系统调用的差异比 FreeBSD 大得多。程序启动在各架构之间需要略有不同的汇编胶水代码。需要不同的链接器脚本。加载器元数据虽然大体相似，但存在架构差异。最后，从 Linux kexec 重启向量到内核的交接也不同。后两者将在下文 “重构 UEFI 引导” 部分介绍。

前三项改动是为了创建 Linux 二进制文件。为创建静态二进制文件，我编写了 C 运行时支持代码，提供 Linux 内核交接与传统 main 函数之间的胶水。我编写了一些每架构汇编代码，配合一个调用 main 的标准启动例程。为此，一个标准的系统调用 C 接口使 FreeBSD 的 Linux 小型 libc 的 MD 部分保持小巧。我为系统调用创建了一些每架构汇编代码。我添加了一个框架来处理 Linux 每架构 ABI 差异，最大的差异在 termios 接口。这反映了 Linux 二进制兼容性的复杂历史。每架构链接器脚本生成 Linux ELF 二进制文件。这些元素组合在一起构成了 `loader.kboot`——一个 Linux ELF 二进制文件。新的 libsa 驱动程序（见下文）与此 libc 接口。

## 访问主机资源

原始的 `loader.kboot` 代码可以访问一些主机资源，但不完整。我希望能够从裸设备引导，或者通过驻留在主机系统文件系统中的内核或加载器引导。引导加载器一直支持多种指定文件来源的方式，但在重构之前，添加新方式很困难。通过对现有代码进行一些重构，我添加了通过 Linux 设备名访问任何块设备的能力。例如，名称 **/dev/sda4:/boot/loader** 读取 sda 磁盘第四个分区内的 **/boot/loader** 文件。此外，`lsdev` 现在会列出所有可用的 Linux 块设备。引导加载器会发现 zpool。例如，**zfs:zroot/kboot-example/boot/kernel** 指定要引导的内核。最后，将内核和/或引导加载器直接放在 Linux initrd 中可能更方便。引导加载器本身使用此功能从 **/sys** 和 **/proc** 文件系统获取必要数据。任何已挂载的文件系统都可以通过 `host:‹path-to-file›` 访问。因此，你可以从 `host:/freebsd/boot/kernel` 引导，或者用 `more host:/proc/iomem` 读取 Linux 内存使用情况。加载器还支持将 **/sys/** 或 **/proc/** 前缀映射到主机的 **/sys** 和 **/proc** 文件系统，无论当前活动设备是什么。

`loader.kboot` 可以替换 Linux initrd 中的 **/sbin/init**。init 是第一个运行的程序，必须执行额外步骤来准备系统。`loader.kboot` 在作为 init 运行时会注意到这一点，并在启动前执行这些额外步骤：挂载所有初始文件系统（**/dev**、**/sys**、**/proc**、**/tmp**、**/var**），创建若干预期的符号链接，以及打开 stdin、stdout 和 stderr。加载器要么在此环境中运行，要么作为从标准 Linux 启动脚本启动的进程运行。目前，`loader.kboot` 无法 fork 和执行 Linux 命令。

## 重构 UEFI 引导

引导的悠久历史可见一斑：FreeBSD 的引导过程在过去 30 年（amd64）和约 20 年（aarch64）中与内核共同演化。当然，这两种架构并非一直存在，但 amd64 继承了 i386 的许多特殊行为，而 aarch64 的引导虽然简洁得多，却是 20 年嵌入式 FreeBSD 系统的产物。要在这个复杂环境中成功引导，`loader.kboot` 需要重现所有这些特殊行为。它遵循 UEFI 协议，创建与 UEFI 引导加载器 `loader.efi` 相同的元数据结构。

这些工作从 amd64 开始，因为它更容易用于实验。我选择模拟 UEFI + ACPI 引导环境。UEFI 是较新、更灵活的接口，似乎特殊情况更少。虽然理论上 FreeBSD 内核可以从 UEFI 或 CSM 引导而不知道自己是从哪个引导的，但实际情况并非如此。内核期望以某些方式获取 UEFI 派生的数据，而以略有不同的方式获取 BIOS 派生的数据。很早就发现，尝试同时支持两者会限制进展，因为通常需要编写和调试两条不同的路径。由于 UEFI 将长期存在（即使有 LinuxBoot，也只有一小部分 UEFI 保留下来），而 CSM 可能不会，我决定在 amd64 上仅支持 UEFI。

尽管做了这一简化，进展仍然太慢，因为我必须通过试错来发现 amd64 内核依赖引导加载器的所有特殊行为。FreeBSD 开发者 Mark Johnston 建议 aarch64 可能更容易实现，因为它的接口集更简单。事实证明确实如此。在完成了从 UEFI 数据结构到 FreeBSD 加载器元数据的基本转换后，我在 aarch64 上取得了更大进展。只有几个 bug，稍后讨论。既然 aarch64 已经工作，我计划接下来让 amd64 也工作起来。

加载器需要几百行代码来从 UEFI 设置元数据。这段代码不出所料地期望在 UEFI 运行时中执行。它使用 UEFI API 分配内存，从 UEFI 获取内存信息，并以 UEFI 特有的方式获取 ACPI 表。我需要重构这段代码，使其能够从 Linux 上的 **/sys** 和 **/proc** 文件系统创建正确的元数据结构。此外，Linux 通过 FDT 和 ACPI 同时提供设备数据，但设备描述仅在 ACPI 中。这欺骗了 FreeBSD，让它以为没有设备存在，因为当两者同时存在时，FreeBSD 优先使用 FDT 进行设备枚举。Linux 通过 FDT 仅提供执行另一次 kexec 所需的数据，而不提供设备数据。

除了正常的 UEFI 数据结构和引导外，kexec 之后，Linux 将硬件留在一种与固件冷启动或热启动都微妙不同的状态。因此系统并未完全处于重置状态。通常这无所谓——我们可以从该状态引导并将所有硬件置于正确状态。但存在一些问题。

UEFI Boot Services 是我遇到的第一个问题。当 Linux 退出 UEFI 的 “boot services” 时，它创建的内存映射中虚拟地址（VA）与物理地址（PA）不匹配。FreeBSD 的 `loader.efi` 总是创建 1:1 映射，即 PA 等于 VA（即所谓的 PA = VA）。由于内存映射只能设置一次，FreeBSD 内核必须使用 Linux 创建的映射。内核会 panic，因为映射不是 PA = VA。幸运的是，这些 panic 是由于调试 `loader.efi` 时遗留的过于严格的断言。移除这些断言后暴露了一个使用 PA 而非 VA 的 bug，但修复该 bug 后内核就成功引导了。

我遇到的第二个问题是 “gicv3” 问题——“几乎重置” 加上设备勘误带来了大麻烦。gicv3 中断路由器存在设计缺陷：一旦启动（Linux 引导时会完全启动它），除非完全重置并初始化系统（kexec 做不到），否则无法停止。为解决此问题，FreeBSD 内核必须重用此内存。Linux 通过 UEFI 系统表结构传递 gicv3 状态数据。该表包含为 gicv3 保留的物理地址列表。FreeBSD 解析此表，确认其与 gicv3 正在使用的内容匹配，并将它们标记为保留，以便 FreeBSD 的内存分配代码不会分配它们。在 QEMU 中一切正常；然而，当我们在 aarch64 机器上运行时，我非常惊讶地发现了这个问题。幸运的是，Linux 社区此前已发现此问题，并有一组补丁，我可以用类似方式修复 FreeBSD。

## 偿还技术债务

在这个项目中我发现一个不太意外的现象：引导加载器中有大量复制粘贴的代码来实现路径和设备名解析。这些代码的副本之间存在不明显的差异。有时改动是 bug 修复，但软件考古学显示其他副本保留了 bug。有时副本中引入了新 bug。引导加载器有这样的命运是可以理解的。移植到新平台时，从可工作的加载器复制代码并为新环境稍作调整很容易。很少有人考虑缺乏重构的长期影响。既然加载器能引导内核了，为什么还要在加载器上花更多时间？事实证明这种策略和态度是有害的。例如，文件名解析代码被从一个环境复制到另一个环境，到我开始时已有约 10 个副本。它们都需要一个通用例程——除了一个例外，它有合理的理由与众不同，因为其设备规范与别处使用的 `diskXpY:` 不同。解析中的 bug 促使我将所有这些代码重构到一个位置（这样我可以一次性修复 bug）。这使得 `/dev/XXXX` 的新用法在访问裸设备时可以作为 “设备名” 使用。这也让 `host` 前缀无需单元号即可工作。加载器的文件名解析器比我之前了解的要灵活得多。

## 结论

将 FreeBSD 引导加载器和内核适配到这个新环境中引导，过程相当顺利。Linux 主机集成的大量小任务，加上 FreeBSD 未记录的引导加载器到内核的交接，是该项目这一阶段最大的挑战。意外的硬件勘误增加了挑战性，并导致内核需要最大的改动。随着 FreeBSD/aarch64 成功在真实硬件上引导，我们可以推进项目的下一阶段：创建自己的固件并修复剩余的 FreeBSD/amd64 bug。即使存在这些限制，我们已使用 `loader.kboot` 下载安装程序 ramdisk 来配置系统并重启结果。它还被纳入了去年夏天的加载器持续集成 GSoC 项目。

## 下期预告

下一篇文章中，我们将创建一个引导 FreeBSD 的 LinuxBoot 固件镜像。我将解释固件镜像如何打包、用于创建和操作镜像的工具，以及如果你足够勇敢，如何刷新固件镜像。我会帮你从 Linux 提供的众多 initrd 创建工具中选择合适的工具，并给出一个示例脚本来查找和引导你的 FreeBSD 系统。运气好的话，最终你将拥有更简洁、更快速、更安全的固件。

***

**Warner Losh** 参与 FreeBSD 项目贡献已有多年。他为引导加载器贡献了许多功能和修复。他对 Unix 历史的兴趣延伸到自举如何随 Unix 及其众多衍生系统演化。他与妻子 Lindy（热爱画猫画狗）和女儿（演奏多种铜管乐器）居住在科罗拉多州。常常可以看到他在遛腊肠犬。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://freebsd-journal-cn.bsdcn.org/2023-1112-freebsd-14.0/linuxboot-cong-linux-qi-dong-freebsd.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
