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

Zircon 采用微内核设计。如何引导初始用户空间进程是微内核设计的复杂性。通常,这是通过使内核实现最低版本的文件系统读取和程序加载(仅用于引导,即使这些内核设施在启动后从未使用过)来实现的。Zircon 则另辟蹊径。

引导加载程序和内核启动

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

BOOTFS

Zircon 启动映像中嵌入的项之一是初始 RAM 磁盘文件系统映像。图像通常使用 LZ4 格式压缩。解压缩后,映像为 BOOTFS 格式。这是一种简单的只读文件系统格式,它只列出文件名,以及每个文件的 BOOTFS 映像中的偏移量和大小(这两个值都必须在两个字段中进行页面对齐,且不得超过 32 位)。

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

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

内核加载 userboot

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

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

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

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

内核发送 processargs 消息

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

内核使用完全相同的协议来启动 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 包含 LZ4 格式的支持代码,用于将内容解压缩到新的 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 会等待其开始退出的进程,然后关闭系统(就像通过 dm shutdown 命令一样)。这种做法在以下情况下可能很有用:运行单个测试计划,然后关闭机器(或模拟器)。 例如,命令行 userboot.next=bin/core-tests userboot.shutdown 会运行 Zircon 核心测试,然后关闭。

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