从 Zircon 内核到用户空间引导 (userboot)

Zircon 采用微内核设计风格。微内核设计的一个复杂性在于如何引导初始用户空间进程。通常,这可通过让内核仅出于引导目的实现文件系统读取和程序加载的最小版本来实现,即使这些内核设施在启动后从未使用过也是如此。Zircon 采用了不同的方法。

引导加载程序和内核启动

引导加载程序会将内核加载到内存中,并将控制权转移到内核的启动代码。本文未介绍引导加载程序协议的详细信息。与 Zircon 搭配使用的引导加载程序会同时加载内核映像和 Zircon 引导映像格式的数据 blob。ZBI 格式是一种简单的容器格式,用于嵌入引导加载程序传递的项,包括硬件专用信息、提供启动选项的内核“命令行”,以及 RAM 磁盘映像(通常是压缩的)。内核会在启动阶段的早期提取一些基本信息供其自行使用。

BOOTFS

Zircon 启动映像中嵌入的一个项目是初始 RAM 磁盘文件系统映像。图片通常使用 zstd 格式进行压缩。解压缩后,映像将采用 BOOTFS 格式。这是一种简单的只读文件系统格式,只会列出文件名,以及每个文件在 BOOTFS 映像中的偏移量和大小(这两个值都必须与页面对齐,并且都限制为 32 位)。

主要的 BOOTFS 映像包含用户空间系统运行所需的所有内容:可执行文件、共享库和数据文件。这些包括设备驱动程序的实现和更高级的文件系统,这些实现使您能够从存储设备或网络设备读取更多代码和数据。

系统自行引导后,主要 BOOTFS 中的文件会成为根位于 /boot 的只读文件系统树(由组件管理器提供)。

内核加载 userboot

内核不包含用于解压缩 zstd 格式的任何代码,也不包含用于解读 BOOTFS 格式的任何代码。而是由第一个用户空间进程(称为 userboot)完成所有这些工作。

userboot 是一个常规的用户空间进程。它只能像任何其他进程一样通过 vDSO 进行标准系统调用,并且受完整的 vDSO 强制执行机制的约束。userboot 的特别之处在于其加载方式。

userboot 构建为 ELF 动态共享对象,使用与 vDSO 相同的 RODSO 布局。与 vDSO 一样,userboot ELF 映像会在编译时嵌入到内核中。其布局简单,这意味着加载它不需要内核在启动时解析 ELF 头文件。内核只需知道三件事:只读段的大小、可执行段的大小和 userboot 入口点的地址。在编译时,这些值会从 userboot ELF 映像中提取,并用作内核代码中的常量。

与任何其他进程一样,userboot 必须从已映射到其地址空间的 vDSO 开始,才能进行系统调用。内核会将 userboot 和 vDSO 映射到第一个用户进程,然后在 userboot 入口点启动该进程。

内核发送 processargs 消息

在正常程序加载期间,系统会向每个新进程发送一个引导加载程序消息。进程的第一个线程会在寄存器中接收一个通道句柄。然后,它就可以读取其创建者发送的数据和句柄。

内核使用完全相同的协议启动 userboot。内核命令行会拆分为字词,这些字词会成为引导消息中的环境字符串。此消息中包含 userboot 本身需要的所有句柄,以及系统其余部分需要访问内核设施的所有句柄。句柄信息条目遵循常规格式,用于说明每个句柄的用途。其中包括 PA_VMO_VDSO 手柄

userboot 在 vDSO 中查找系统调用

用于告知新进程其 vDSO 映射的标准惯例要求该进程解析 vDSO 的 ELF 头文件和符号表,以定位系统调用入口点。为避免这种复杂性,userboot 会以其他方式在 vDSO 中查找入口点。

当内核将 userboot 映射到第一个用户进程时,它会选择内存中的随机位置,就像正常的程序加载一样。不过,在映射 vDSO 时,它不会像平常一样选择其他随机位置。而是将 vDSO 映像放置在内存中的 userboot 映像之后。这样,vDSO 代码始终位于 userboot 代码的固定偏移处。

在编译时,系统会从 vDSO ELF 映像中提取所有系统调用入口点的符号表条目。然后,这些符号会转换为链接器脚本符号定义,这些定义会使用每个符号在 vDSO 映像中的固定偏移量,以便在相对于链接器提供的 _end 符号的固定偏移量处定义该符号。这样,userboot 代码就可以在内存中 userboot 映像本身之后的确切位置直接调用每个 vDSO 入口点。

userboot 解压缩 BOOTFS

userboot 首先要做的是读取内核发送的引导消息。在从内核获取的句柄中,有一个句柄的句柄信息条目PA_HND(PA_VMO_BOOTDATA, 0)。这是一个包含引导加载程序中的 ZBI 的 VMOuserboot 会从此 VMO 读取 ZBI 标头,以查找类型为 ZBI_TYPE_STORAGE_BOOTFS 的第一个项。该文件包含 BOOTFS 映像。项的 ZBI 头文件指示其是否已压缩(通常是已压缩)。userboot 会映射到 VMO 的这一部分。userboot 包含 zstd 格式支持代码,用于将项解压缩为新的 VMO。

userboot 从 BOOTFS 加载第一个“真实”用户进程

接下来,userboot 会检查从内核收到的环境字符串,这些字符串代表内核命令行。如果存在字符串 userboot.next=file+optional_arg1+optional_arg2=foo+...,则 file 将作为第一个真实用户进程加载,并将通过“+”分隔的参数传递给它。如果不存在此类选项,则默认文件bin/component_manager+--boot。这些文件位于 BOOTFS 映像中。

为了加载文件,userboot 实现了一个功能齐全的 ELF 程序加载器。通常,要加载的文件是具有 PT_INTERP 程序头的动态链接可执行文件。在这种情况下,userboot 会搜索 PT_INTERP 中命名的文件,并改为加载该文件。

然后,userboot 会在随机地址处加载 vDSO。它会按照标准惯例启动新进程,并向其传递通道句柄和 vDSO 基地址。在该通道上,userboot 会发送标准的 processargs 消息。它会传递从内核收到的所有重要句柄(将特定句柄,例如进程-自身句柄和线程-自身句柄,替换为新进程(而非 userboot 本身)的句柄)。

userboot 加载程序服务

根据标准程序加载协议,当 userboot 通过 PT_INTERP 加载程序时,它会在主消息之前发送一条额外的 processargs 消息,以供动态链接器使用。此消息包含一个频道的 PA_LDSVC_LOADER 句柄,userboot 会在该句柄上提供标准加载器服务的最小实现。

userboot 只有一个线程,该线程会一直循环处理加载器服务请求,直到通道关闭。收到 LOADER_SVC_OP_LOAD_OBJECT 请求后,它会在 BOOTFS 中将前缀为 lib/ 的对象名称作为文件进行查找,并返回其内容的 VMO。因此,第一个“真实”用户进程可以是(通常是)需要各种共享库的动态链接可执行文件。动态链接器、可执行文件和共享库都从同一 BOOTFS 页面加载,这些页面稍后会显示为 /boot 中的文件。

将由 userboot 加载的可执行文件(即 component manager)通常应在完成启动后关闭其加载器服务通道。这样,userboot 就会知道不再需要它了。

userboot 驶向夕阳

当加载器服务通道关闭(或者如果可执行文件没有 PT_INTERP,因此不需要加载器服务,那么在进程启动后立即关闭),userboot 就不再有任何事情可做。

如果在内核命令行中指定了 userboot.shutdown 选项,则 userboot 会等待其启动的进程退出,然后关闭系统(就像通过 power shutdown 命令关闭系统一样)。这对于运行单个测试程序,然后关闭机器(或模拟器)非常有用。例如,命令行 userboot.next=bin/core-tests userboot.shutdown 会运行 Zircon 核心测试,然后关闭。

否则,userboot 不会等待进程退出。userboot 会立即退出,让第一个“真实”用户进程负责启动和关闭系统的其余部分。