从 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 代码就可以直接调用每个 vDSO 入口点出现在内存中 userboot 映像本身之后的准确位置。

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 将作为第一个真实用户进程加载,并向其传递以“+”号分隔的参数。如果不存在此选项,则默认 filebin/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 会等待其开始退出的进程,然后关闭系统(就像通过 dm shutdown 命令执行的那样)。这对于运行单个测试程序然后关闭计算机(或模拟器)非常有用。例如,命令行 userboot.next=bin/core-tests userboot.shutdown 会运行 Zircon 核心测试,然后关闭。

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