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 的 VMO。userboot
从此 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 将作为第一个真实用户进程加载,并向其传递以“+”号分隔的参数。如果不存在此选项,则默认 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
会立即退出,让第一个“实际”用户进程负责启动和关闭系统的其余部分。