| RFC-0264:在 Fuchsia 上运行未修改的 AArch32 Linux 程序 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 通过受限模式和 starnix 启用未修改的不可信 AArch32 访客代码。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2024-11-05 |
| 审核日期(年-月-日) | 2025-01-21 |
问题陈述
安全高效的 外部 ABI 支持使 AArch64 Linux 程序能够以未修改的状态运行。随着我们不断扩大希望在 Fuchsia 上运行的软件范围,我们遇到了需要能够运行而无需重新编译的 32 位 ARM (AArch32 ISA) Linux 程序,或者这些程序在针对 AArch32 Linux ABI 运行时会获得足够的好处。
此问题不会扩展到在 Fuchsia 程序中启用任何其他 ABI 支持。
摘要
本文档建议添加 AArch32 外部 ABI 支持,以运行未经修改的 32 位 ARM ISA Linux 程序。基于 Zircon 的高效用户空间内核模拟,来自 32 位 AArch32 用户空间的异常将重定向到 starnix。Starnix 将负责实现所有必需的 AArch32/Linux 外部 ABI 功能。
利益相关方
此提案会影响项目的多个方面,这体现在审核者身上:
- Zircon:由于内核必须安全且可持续地支持 AArch32
异常。 - Starnix:因为它负责大部分外部 ABI 兼容性。
- 工具链:由于这会增加针对
AArch32 目标平台进行编译的新需求(但需求有限)。 - 基础架构和测试:因为这会添加新的硬件和/或模拟
置换。
辅导员:
- hjfreyer@
审核者:
- maniscalco@
- lindkvist@
- mcgrathr@
- jamesr@
- mvanotti@
已咨询:
- travisg@
- phosek@
- abarth@
- tkilbourn@
- olivernewman@
- ajkelly@
共同化:
我们首先开发了此提案的技术原型,以验证其技术可行性并进行初步的成本评估。该原型已与受影响组件的代表分享,同时还分享了本文档的草稿。通过原型实现,可以更准确地评估潜在的短期和长期影响,而设计审核则促使我们更深入地讨论设计选择,并发现了一些现已解决的问题。
要求
潜在的指导性要求有很多,但选择问题陈述后,以下要求会应用于后续设计:
Starnix 中的 AArch32 支持必须:
- 能够解析和加载现代 armv7-linux 二进制文件
- 能够管理单独的 AArch32 系统调用路径
- 合理包含,不会影响整个 Starnix 中的代码
- 能够停用(例如,通过 build-time 标志)
- 尽可能减少对现有 AArch64 代码路径的影响
- 能够在同一 starnix 实例上执行 AArch64 和 AArch32 程序
值得注意的是,虽然 AArch64 和 AArch32 软件可能在同一 starnix 内核实例上并行执行,但目前的目标并不是保证这两个环境之间的互动行为。
Zircon 中的 AArch32 支持必须:
- 受限模式线程的唯一函数
- 允许共享进程托管 AArch32 或 AArch64 受限模式代码
- 不会造成过度的长期维护负担
- 尽可能减少 API Surface 变更
- 易于移除和/或停用
- 不会影响 Zirecon 下运行的每个任务
- 不需要对 Zircon 进行大量更改
虽然未来可能需要其他受 Fuchsia 支持的架构,但我们目前希望运行的程序面向 AArch32 环境,例如 Debian 或 Android。我们的目标不是提供 armv7-linux 的完美模拟,而是基于 starnix 的 API 层来支持公开的处理器 AArch32 功能。
设计
Starnix 提供与 Linux 内核的兼容性。Linux 内核已支持所谓的“兼容”模式。启用后,CONFIG_COMPAT 允许 64 位 Linux 内核运行主机 CPU 架构支持的 32 位或 64 位 ISA 目标二进制文件。对于 Arm 处理器,这是 AArch64 和 AArch32。
一般来说,CONFIG_COMPAT 可让 32 位架构得以运行。此设计旨在使 Starnix 上的 Android 能够运行 32 位 (AArch32) 或 64 位 (AArch64) ARM 二进制文件,从而在 Starnix 上实现相同的行为。在 Fuchsia 上实现此目标与在 Linux 上实现此目标有所不同,并且需要对 Zircon 和 Starnix 进行更改。本文档将从硬件开始,逐步向上介绍。
Zircon
Zircon 是 Fuchsia 的内核,可通过受限模式启用 Starnix。
为实现 AArch32 支持,我们采取的方法是:(1) 确定处理器的功能和特性;(2) 确保受限模式可以在入口处启用 AArch32 执行;(3) 确保所有异常(错误或其他异常)都路由到受限模式的监督程序 (starnix),同时尽可能减少对 Zircon 的任何其他影响。
处理器支持
如前所述,ARM 架构提供了一种执行状态,称为 AArch32,可在较新的架构版本(ARMv8 及更高版本)的处理器上提供与之前的 32 位 ARM ISA 的向后兼容性。
ARM 架构为实现 AArch32 支持提供了一条相对清晰的途径,既不会影响现代内核,又能在最后一个 32 位架构版本 ARMv7 的基础上实现一小部分额外功能。以下各部分将介绍启用 AArch32 执行所需的处理器功能。
本部分的其余内容主要引用并改写了 ARM 技术指南和参考手册中与 Zircon 和 Starnix 的后续变更相关的部分。
检测 AArch32 支持
最近的 ARM 架构修订版本公开了一个处理器功能寄存器 ID_PFR0_EL1,用于支持 AArch32。此寄存器中的值决定了支持哪些 AArch32 功能(如果有)。许多面向消费类设备的 64 位 ARM 芯片都支持 A32(状态 0)和 T32(状态 1)指令集(作为 ARMv7 支持)。为了支持 Linux 的 32 位兼容性用户空间,需要这两个指令集。请务必注意,许多面向服务器的 64 位 ARM 芯片不支持任何 AArch32 功能,但如果支持,这两组指令集都将可用。
核心寄存器映射
| AArch32 | AArch64 |
|---|---|
| R0-R12 | X0-X12 |
| Banked SP 和 LR | X13-X23 |
| 已保存的 FIQ | X24-X30 |
在大多数情况下,寄存器的架构映射是无缝的。AArch32 有 16 个预声明的核心寄存器,而 AArch64 有 32 个预声明的核心寄存器。AArch32 寄存器映射到 AArch64 通用寄存器组的前 16 个寄存器:x0-15 是 r0-15。根据 ARMv8 的 64 位架构概览,从 AArch32 访问寄存器时,系统会忽略或将高 32 位清零:
- 源寄存器的高 32 位会被忽略。
- 目标寄存器的高 32 位设置为零。
- 由指令设置的条件标志根据低 32 位计算得出。
当有意从 AArch64 将这些寄存器作为 AArch32 寄存器访问时,通常使用 W 而不是 X 来避免过时的值。
请务必注意,程序计数器 (PC) 映射到寄存器 R15,堆栈指针 (SP) 映射到 R13,链接寄存器 (LR) 映射到 R14。
除了核心寄存器之外,当前程序状态寄存器 (CPSR) 映射在 AArch32 和 AArch64 交互中也发挥着至关重要的作用。下一部分将介绍其映射。
当前程序状态寄存器 (CPSR)
在 ARMv7 及更早版本中,有一个单一寄存器,即当前程序状态寄存器 (CPSR),其中包含:
- APSR 标志 - 即
- N、Z、C 和 V 标志。
- Q(饱和度)标志。
- GE(大于或等于)标志。
- 当前处理器模式。
- 中断停用标志。
- 当前处理器状态,即 ARM、Thumb、ThumbEE 或 Jazelle。
- 字节序。
- IT 块的执行状态位。
在 AArch64 中,此信息会被拆分并以不同的方式访问(不过 PSTATE 本质上是 CPSR 的重命名)。不过,当出现异常时,信息会显示在已保存的程序状态寄存器 (SPSR) 中,该寄存器会存储:
- N、Z、C 和 V 标志。
- D、A、I 和 F 中断禁用位。
- 寄存器宽度。
- 执行模式。
- IL 和 SS 位。
从架构上讲,AArch32 CPSR 在发生异常时映射到 AArch64 的 SPSR。这样一来,AArch32 CPSR 的仅限 AArch32 的位将存储在 SPSR 的预留区域中:
| 31 27 | 26 25 | 24 | 23 20 | 19 16 | 15 10 | 9 | 8 | 7 | 6 | 5 | 4 0 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| N Z C V Q | IT | J | 预留 | GE | IT | E | A | I | F | T | M |
| [1:0] | [3:0] | [7:2] | [4:0] |
在 AArch64 状态下,IT、Q 和 GE 标志无法读取或写入。在 AArch32 中,GE 标志由并行加法和减法指令设置,Q 设置为指示溢出或饱和。IT 位供 Thumb 指令用于有条件执行,在 A32 模式下应为 0,在 T32 模式下应在异常情况下保持不变。
执行模式和处理器状态
在 ARMv8 和 ARMv9 中,执行模式(即上述 SPSR/CPSR 图中的 M[4])可设置为指示程序是否处于 AArch32 模式。只能在重置或异常级别发生变化时更改执行模式。
在 AArch64 执行模式下,只能使用 A64 指令集。在 AArch32 中,仅支持 A32 和 T32 指令。启用的指令集称为处理器状态,可以使用 T 位(即 Thumb 执行状态位)切换处理器状态。 在用户空间中,此更改通过分支 (BX/BLX) 执行。任何内核软件都应确保 PC 对齐与当前指令集对齐相匹配。
浮点寄存器和高级 SIMD 寄存器
除了核心寄存器之外,高级 SIMD 和浮点指令共享同一寄存器组。它包含 32 个 64 位寄存器,较小的寄存器会打包到较大的寄存器中,就像 ARMv7 及更早版本一样 - 基于 AArch32 的 VFPv4。
根据所述要求,目标是支持公开给 ARMv7 但在 AArch32 模式下受支持的 ASIMD 和 VFP 扩展。不一定会支持 AArch64 和未来的 AArch32 ISA 扩展中的其他状态。
其他寄存器映射:计数器和线程
除了前面讨论的寄存器映射之外,AArch32 和 AArch64 还有许多其他寄存器映射。不过,有两项与此设计的其余部分相关,即 tick 计数器和线程存储。
在现代操作系统上,所有进程在其地址空间中都映射了一个虚拟动态共享对象,该对象尝试在不进入内核的情况下提供对时间检查功能的访问权限。这通常通过使用时钟周期或循环计数来完成。用于此目的的 AArch64 寄存器与 AArch32 寄存器一一对应。
另一个长期存在的操作系统功能是 ELF 线程本地存储 ABI。这是一种变量存储模型,可让每个线程拥有全局变量的唯一副本。如需详细了解 Fuchsia 的使用,请点击此处。 如需详细了解 Android 的使用情况,请点击此处。 这些接口在 AArch64 上使用的寄存器是 TPIDR_EL0。虽然这会映射到 AArch32 中的 TPIDRURW,但 ARMv7 软件不希望拥有对 TLS 寄存器的读/写访问权限。它希望内核代表其更新 TPIDRURO。在 AArch64 上,此寄存器映射到 TPIDRRO_EL0。
异常和调用约定
如前所述,异常级别更改提供了一种更改执行状态的方法。一旦在 EL1 运行的 AArch64 内核设置了 M[4] 位,然后返回到用户空间 EL0,处理器就会在 AArch32 中运行。这意味着,触发的任何异常都将是 AArch32 异常。
当触发 AArch32 异常时,处理器将切换回 AArch64 状态以执行异常处理代码。由于寄存器是直接映射的,因此只需进行极少的更改即可处理 AArch32 异常。从异常返回同样简单。不过,有必要保留 APSR/CPSR 位,以确保执行模式和处理器模式标志相同。值得注意的是,程序计数器可能不会直接在 AArch64 上设置,而是将异常链接寄存器 (ELR) 设置为 PC 在从异常返回 (ERET) 时需要设置的值。对于从 AArch64 返回到 AArch32(通过 ERET),R15 将从异常链接寄存器 (ELR_EL0) 恢复。
另请注意,AArch64 和 AArch32 的系统调用 Linux 调用规范有所不同。在 AArch64 中,x8 必须包含系统调用号,但在 AArch32 中,该寄存器为 r7。还有其他细微差别需要涵盖,以确保可靠运行。例如,交叉检查恢复的寄存器和 APSR/CPSR 条件位是否以与 Linux 类似的方式恢复,将有助于避免难以诊断的故障。在初始审核中,恢复的状态在功能上与 starnix 的 AArch64 行为一致,但考虑到进入和返回异常状态的潜在路径数量,这是一个在实现期间应仔细审核的领域。
启用受限模式
受限模式是一项功能,可为 Zircon 线程和进程提供内核辅助沙盒。具体而言,当进程或线程启用受限模式时,它会向 Zircon 提供所需的“受限”寄存器状态、返回位置(向量),然后发出 zx_restricted_enter() 系统调用。当 Zircon 进入受限模式时,它将按照指示配置处理器寄存器,然后以受限地址空间运行沙盒线程。当受限模式线程中发生任何异常时,Zircon 将保存受限模式处理器状态,从调用线程进入受限模式时恢复其状态,然后使用指向状态和异常信息的指针在调用线程中的指定向量处恢复。除了过滤和传递异常之外,Zircon 本身不会处理受限线程引发的大多数异常。(值得注意的是,Zircon 会以透明方式处理页面错误。)因此,受限线程的监督部分负责处理进入受限模式的配置,以及处理退出受限模式的各种状态。
我们将首先探讨 Zircon 中支持进入 AArch32 受限模式所需的内容,然后探讨支持退出该模式所需的内容。
入门
当调用 zx_restricted_enter() 时,可能会进入受限模式。如果提供的参数和绑定状态已针对宿主架构进行适当配置,Zircon 将在受限线程的上下文中重新进入用户空间。请务必注意,进入受限模式并非一次性操作。虽然任何给定线程都有一个初始入口,但任何受限异常都将由线程的监督部分处理,该部分必须调用 zx_restricted_enter() 才能恢复线程。
将处理器转换为 AArch32 执行状态
鉴于此以及上述有关 ARM 架构的详细信息,需要进行少量更改才能使 AArch32 使用受限模式。也就是说,我们需要允许正常模式(受限模式的监督者)设置 M[4]、T、IT、GE 和 Q 位。在初始进入时,CPSR 需要能够设置 M[4] 和 T 位,以便线程解释 A32 或 T32 指令集。IT、GE 和 Q 需要在退出和重新进入时保持不变。
在当前的 AArch64 受限模式入口代码路径 (RestrictedState::ArchValidateStatePreRestrictedEntry) 中,CPSR 会被过滤,仅允许条件标志。需要更改掩码,以允许上述任何操作,并确保仅在配置有效的情况下才允许这样做。这种方法非常适合 AArch32,因为它是 AArch64 的自然扩展,但如果未来支持不同的 ISA,则可能不适合。从长远来看,或许可以将过渡决策移至受限的入口系统调用,但目前无需进行此更改。
由于在 AArch32 模式下不需要寄存器的高 32 位,并且过时的值可能会被意外解释,因此在进入 AArch32 或返回时主动清除这些位是有意义的。
有很多方法可以解决此问题,从在汇编异常路径中通过“w”寄存器访问器(32 位)保存/恢复寄存器,到在 C++ 中进入和/或返回时清零。由于控制流路径单一,因此在进入时清零是最简单的方法,但如果调试器可以访问寄存器,则无法保证高位保持清除状态。
为了限制对 Zircon 的更改范围,最初重点关注 ArchSave* 受限模式函数。这将确保寄存器高 32 位中的任何过时或意外值不会被受限模式的监督程序(如 starnix)使用,同时在需要时为未来的优化留出空间。此外,除了任何默认的 AArch64 更新之外,未使用的寄存器不需要保存其状态。
启用 ELF 线程本地存储 ABI
线程本地存储 ABI 的某些实现依赖于 set_tls() 系统调用和读取 TPIDRURO 寄存器。不过,现代 armv7-linux 二进制文件往往更倾向于使用 TPIDRURW 寄存器,该寄存器不需要系统调用即可修改。(对于 AArch32,TPIDRURW 映射到 TPIDR_EL0,TPIDRURO 映射到 TPIDRRO_EL0。)遗憾的是,我们可能会遇到想要运行的二进制文件,这些文件也硬编码了 TPIDRURO 的使用,或者在 glibc 的情况下,如果 Linux 内核用户帮助程序不存在,则默认使用 TPIDRURO。
今天无法从受限模式的监督者设置 TPIDRURO。 AArch64 的 zx_restricted_state_t 结构体公开了对 TPIDRURW (TPIDR_EL0) 的访问权限,但未公开对 TPIDRRO_EL0 的访问权限。然后,可以选择显式添加接口来设置寄存器,例如向 TPIDRURO (TPIDRRO_EL0) 的结构体添加条目,也可以选择隐式将为 TPIDRURW 设置的值镜像到 TPIDRURO。目前,为此目的更改 zx_restricted_state_t 接口似乎并不值得,因为没有已知的二进制文件同时使用 TPIDRURW 和 TPIDRURO,并且没有必要为 AArch64 二进制文件支持该寄存器。
如果实现,镜像应仅在进入受限模式时发生。如果 TPIDRURW 由线程更改,则 TPIDRURO 将不会更新,直到下一次重新进入。这应该是可以接受的,因为 ARMv7 用户空间不希望能够更改 TPIDRURO,除非通过系统调用。如果我们发现某个 Linux 二进制文件通过系统调用访问 TPIDRURO,并直接访问 TPIDRURW,那么我们可能需要添加对设置 TPIDRURO (TPIDRRO_EL0) 的显式支持。
退出
从受限模式返回发生在系统调用或其他异常时,并分两个阶段处理。在第一阶段,AArch32 EL0 中会引发异常,然后由 Zircon 在 AArch64 EL1 中处理该异常,Zircon 会将异常状态捆绑在一起,然后将其传递回 AArch64 EL0 受限模式的监督线程。
如前所述,AArch32 异常将由 AArch64 异常向量处理。目前,Zircon 有意不处理 AArch32 异常。同步异常、异步异常和(异步)系统错误异常还需要三个额外的处理程序。Zircon 的异常处理程序主要使用汇编宏实现。这样一来,添加新处理程序就非常简单了。另一项简化是,入站异常应由受限模式监督程序处理。这样,异步异常和系统错误(即 serror)都可以由与 AArch64 对应的处理程序来处理。只有同步异常需要特殊处理,而同步异常中只有系统调用需要特殊处理。对于传入的系统调用,我们可以使用 r7 作为数量,将任何调用传递给受限模式退出处理代码。如果系统调用到达,但线程未标记为受限,我们将使 Zircon 陷入 panic 状态,因为不应能够到达该代码路径。这种行为也会应用于每个其他 32 位处理程序周围的封装容器中。
这种方法应尽可能减少对 Zircon 所需的更改,同时仍能确保 AArch32 异常得到正确保留并传递给受限的 supervisor 线程,而不会在受限模式之外意外启用 AArch32 异常处理。
调试支持
最初,调试支持将分为两种:使用受限模式软件(例如通过 ptrace 使用 gdb)或使用没有 AArch32 ISA Linux ABI 支持的 zxdb。由于寄存器状态从 AArch32 扩展到 AArch64,反之亦然,因此寄存器信息将是正确的,但处理堆栈轨迹以及 A32 和 T32 指令将无法正常运行。读取和写入寄存器状态和标志的调试访问权限将与授予受限模式监控程序的访问权限相匹配。调试器将能够通过检查 CPSR 位来确定线程处于哪种执行模式,并且应该适用于第三方调试器。需要扩展 zxdb 以支持 AArch32。本文不会详细介绍该工作,您需要进行一些额外的研究和实验。
工具链
通过上述更改,Zircon 能够支持执行 32 位 ARM 指令,但如果没有工具链支持,就无法交付要执行的指令。为了编译任何针对 AArch32 受限模式的测试,我们需要一个能够以 ARMv8 AArch32 或 ARMv7 为目标平台的工具链,这是 Arm AArch32 旨在实现兼容的版本。此外,为了支持 Starnix,我们需要一个可以处理 Linux 和 Bionic 目标的工具链。
启用 linux_arm
我们的工具链目前包含一个 Linux ARM clang 目标,以及编译静态和动态目标所需的系统根目录 (sysroot)。不过,它并未在我们的 build 系统中配置。为此,我们必须为目标架构元组添加配置,并添加必要的 build 系统目标。
Linux ARM 工具链和目标平台
在 build/config/BUILDCONFIG.gn 中,我们必须定义新的工具链。例如,
linux_arm_toolchain = "//build/toolchain:linux_arm"
同时,将目标元组添加到 build/config/current_target_tuple.gni。例如:
. . .
} else if (current_cpu == "arm" && is_linux) {
current_target_tuple = "armv7-unknown-linux-gnueabihf"
} . . .
幸运的是,Fuchsia 中已有的工具链是 gnueabihf。所有支持 AArch32 的 AArch64 处理器都将支持硬浮点(VFPv4,如早期讨论中所述)。
我们还需要在 build/toolchain/BUILD.gn 中添加我们之前定义的工具链目标:
clang_toolchain_suite("linux_arm") {
toolchain_cpu = "arm"
toolchain_os = "linux"
use_strip = true
}
由于 Fuchsia 不打算在旧版 ARM 核心上或在 AArch32 状态下构建或运行,因此我们无需为特定于 Fuchsia 的 build 目标宏启用此支持。
可能还有一些其他配置(例如 rust)需要更新,才能继续构建。
Rust
除了 vDSO 之外,Fuchsia 树中很少有构建为在 Starnix 中运行的组件。 其中大部分是通过 Android 构建系统提供的。不过,有些测试以 Starnix 为目标平台,并使用 Rust 构建。支持这些目标需要我们的工具链中提供 armv7-linux-gnueabihf 的运行时库支持。同时支持此版本和上述 LLVM 版本会增加工具链团队的支持负担,无论是前期还是在计划维护 AArch32 build 环境的整个期间。
如果成本过高,我们应考虑其他策略,通过常规工具链和运行时测试相同的代码路径(例如,向 AArch64 starnix 添加自定义个性,使其能够访问兼容性系统调用表,或类似方法)。
Starnix 目标
Starnix 包含自己的 build 目标,适用于 Linux 和 Linux/Bionic 目标。两者都可以基于工具链团队的现有工作构建。在 src/starnix/toolchain/config/BUILD.gn 中,必须为 bionic 目标配置正确的元组和头文件路径,同时在 src/starnix/toolchain/BUILD.gn 中声明相同的工具链。
Starnix
Zircon 更改之所以简单,是因为复杂性已传递给 Starnix。Starnix 服务于 Linux 用户空间软件所依赖的所有内核接口。它必须能够执行 ELF 二进制文件,并处理该代码发出的系统调用以及可能发生的其他异常(32 位或 64 位)。虽然有些差异是无法避免的,例如指针大小或系统调用编号,但大部分 AArch32 功能都可以通过现有代码无缝处理。
此设计中采用的方法是尽可能接近其接口点来处理 AArch32 功能,这样一来,大多数 Starnix 代码都不需要考虑 32 位架构的影响。这些交互点位于处理器级别,然后是系统调用入口、用户空间复制(到和从)、可执行文件加载。唯一其他需要考虑寄存器、指令长度或指针大小的领域是信号处理、ptrace 和核心转储等。
以下各部分将以建设性的方式逐步介绍 Starnix。
任务支持
虽然从“任务”支持开始而不是从接口点开始可能看起来很奇怪,但如上所述,这是有充分理由的。Starnix 对 AArch32 的支持不会是二进制状态,它将同时支持 AArch32 和 AArch64 任务。因此,任务是可注释执行模式的逻辑单元。此外,执行模式是线程特定的,因此此设计假定当前任务上的 ThreadState 结构体将指示线程是否为“arch32”线程。
请注意,此处使用的是“arch32”而非“AArch32”,以避免将命名与特定的处理器实现相关联。
虽然可能可以完全避免使用注释(例如,始终检查 CPSR 中的寄存器值),但专用注释提供了两个有用的属性。第一种是使 starnix 代码能够适应未来变化。例如,如果添加了 riscv32,则所有使用“arch32”检查的代码都将正常运行,而无需进行特定于架构的更改。
第二种属性是在初始化时声明任务的状态。这一点很重要,因为任务的内存管理器至少配置为 32 位布局和强制执行。如果任务更改了 CPSR 执行模式位,则会更改其可执行的指令和引发的异常类型,但不会更改预配置的任务状态。
虽然让 Starnix 混淆执行模式不应有任何直接的安全隐患,但在模式之间任意切换会违反 Starnix 开发者在处理任何 64 位或 32 位特定代码路径时可能做出的正常假设。这种混淆以前在 Linux 上发生过。最好在“arch32”访问器或异常处理层强化此区域,以确保任务的 arch32() 值与其异常模式的系统调用或异常处理程序相匹配。
处理器支持
在处理器级别,寄存器处理是主要接口点。需要更新 ARM64 RegisterState 结构,以确保对 PC、SP、LR、CPSR 和 TPIDR 的更新能够准确更新 AArch32 寄存器,并且不会清除必须保持的状态(例如,在 CPSR 中)。除了 starnix/kernel/arch/arm64 代码之外,还需要在 Zircon Rust 代码(例如 src/lib/zircon/rust/fuchsia-zircon-types/src/lib.rs)中解决此问题。对于处理结构的寄存器,必须确保更新应用于正确的寄存器,并且命名与底层寄存器匹配,例如,syscall_register() 使用 r7。在 Starnix arm64 代码和 Zircon Rust 库中,必须确保所有 From/Into 实现都已更新,以便在受限状态结构和寄存器结构之间正确映射。
在某些情况下,代码只能依靠 CPSR 值来确定是否执行寄存器更改。如果可以访问当前任务,则使用 arch32 布尔值。
UAPI 支持
每种架构的 Linux 内核接口都称为 Linux UAPI 或用户空间 API。Starnix 使用 bindgen 及其自己的 Python 脚本(用于驱动 bindgen)生成 Rust 绑定。由于 UAPI 包含传入和传出内核接口的结构,因此 Starnix 需要 AArch32 UAPI 才能正确处理这些结构。
需要解决的三个主要方面是:
- 32 位指针和类型
- 并行访问两个 UAPI
- 系统调用命名
指针和类型大小
对于第一种情况,存在两个问题。首先,我们需要将所有指针替换为 AArch32 大小的指针类型。Starnix 通常使用 uaddr 和 uref。对于 32 位大小的指针,添加 uaddr32 和 uref32 并确保它们取代指针和引用就足够了。此外,如果可以通过 From/Into 轻松转换为 uaddr 和 uref,那么除了明确需要时,uaddr32 基本上应该不可见。
第二个问题是,相同的类型名称具有不同的大小。例如,long 在 AArch64 上为 64 位宽,在 AArch32 上为 32 位宽。这意味着,默认类型映射无法在两个 UAPI 之间共享,并且必须为每个 UAPI 定义并注入一组特定的内核 C 类型。
并行 UAPI 访问
为了能够清晰地生成 ARM UAPI 绑定,Starnix 必须能够同时访问 AArch64 和 AArch32。幸运的是,通过在 linux_uapi 的 lib.rs 中将生成的“arch32”UAPI 映射到嵌套命名空间下,可以解决此问题。例如,“pub use arm as arch32”以及正确的 cfg 修饰器。(请注意,在 Linux 中,“arm”表示旧版 ARM 32 位 ISA 以及 AArch32。)
这样一来,starnix 中的任何代码都可以使用 arch32 命名空间访问相应大小的类型和生成的绑定。
系统调用编号
借助专用命名空间,可以使用 arch32 前缀将 AArch64 mmap 系统调用与 AArch32 mmap 分开。不过,我们会尽可能分享系统调用实现。有些系统调用的参数和行为是特定于 arch32 的。在这些情况下,使用特定命名(例如 __NR_arch32_open)来隐藏定义可能是有意义的,这样一来,后续构建的系统调用表就可以提供更易读的名称。
由于 Starnix 在 UAPI 生成步骤中驱动所有更改,因此此处也可以这样做。为桩和重定义添加新的 arch32 标头后,任何自定义定义都可以拉入 arch32 命名空间,而无需持续维护单独的基于 Rust 的映射。
加载器支持
即使有了上述三个部分,仍然没有有效的方法来启动 AArch32 任务或线程。由于 CPSR 中的 M[4] 位只能在从异常返回时更改 (EL1->0),因此需要一种方法让用户空间在返回到 EL0 之前请求 Starnix 设置该位。无需特定支持,此功能只能在调用 execve 系统调用或在第一个进程(init)启动时发生。在这两种情况下,都必须加载文件,且其文件格式必须受 Starnix 支持。目前,Starnix 支持 ELF64 格式的文件,它可以解析这些文件、将其加载到内存中,并最终运行。对于 ELF32 格式的文件,也需要相同的功能,最好在整个 Starnix 内核中进行最少的更改。
ELF 解析
ELF 解析是在 src/lib/process_builder/elf_parse.rs 中实现的。所有必需的 ELF64 定义都存在,并且能够从 VMO 解析 ELF64 文件头。对于 ELF32,需要复制相同的定义。不过,Starnix 希望能够传递 Elf64* 结构体(而不是 Elf64* 特征等)。因此,可以添加 ELF32 支持,类似于 uaddr32/uref32:定义尽可能多的内容,以便直接与 32 位类型和数据交互,然后提供 From<> 实现以转换为 64 位表示形式。虽然这种方法会导致生成的 Elf32->Elf64 实例中出现 e_ident 类不匹配的情况,但该结构体将与所有现有接口和调用兼容。
此外,可以通过辅助入口点(例如 from_vmo_with_arch32() 而不是 from_vmo())支持加载 arch32 兼容的 ELF 文件,以确保调用方了解可能的输出。同样,一个辅助函数(例如 arch32()->bool)将使加载器中的任何后续调用方能够轻松检查 ElfIdent 类,以确定是否需要 arch32 行为。
除了决定转换为与 Elf64 兼容的实例之外,这项工作在很大程度上是机械性的(很像 UAPI 和其他已讨论过的主题)。值得考虑升级 Elf64 接口,使其能够处理特定格式,而无需文件格式特定的预期,这与 elfldltl 非常相似。
ELF 解析和 MemoryManager
当 ELF 及其解释器已成功解析时,Starnix 会认为该 ELF 已解析。这种情况发生在提交以完成 exec 调用之前。虽然 AArch32 支持可以在不进行任何更改的情况下继续进行,但这是任务设置的关键时刻。此时,系统会为用户进程创建虚拟内存区域 (VMAR)。通常,此 VMAR 是完整的可用受限地址空间。不过,通过将调用扩展到 MemoryManager (mm.exec()),Starnix 可以建立一个适合于最多具有 32 位可寻址内存空间的进程(例如,4Gb)。
由于所有面向内存的操作都使用任务的“mm”,因此适当大小的 VMAR 将保证映射仅发生在 arch32 任务可寻址的地址空间中。此外,它还允许从可用范围中随机或自动选择任何内存地址,而无需使用特殊标志来限制映射偏移量。使用 user_vmar 是 Starnix 的一项新功能,但它极大地简化了对受限线程可与内存交互的位置强制执行限制的过程。下一部分将使用此简化版本。
除了内存配置之外,分辨率还允许在完全加载可执行文件之前跟踪执行模式 (arch32),从而使后续步骤能够确保内存配置与生成的已加载 ELF 文件之间不会出现不匹配的情况。
ELF 加载
文件解析完毕后,内核将知道哪些部分应映射到内存中以及使用哪些权限。一般来说,Starnix 会尝试根据 mm 的基指针和区域中列出的最低地址来确定偏移。它通过 wrapping_sub 和 wrapping_add 操作来实现这一点。
一般而言,当计算虚拟地址时,封装的值会回绕(或者回绕非常严重,以至于该区域可以映射到 64 位地址范围的高端)。大多数 ELF64 可执行文件的较低地址似乎相当于 mm 基本地址 0x20000,因此此功能可能未得到广泛使用。ELF32 可执行文件通常具有小于 (0x10000) 的文件基址,这会导致 64 位封装,但通常不会封装回来。
解决此问题的一种方法是使用 32 位环绕操作。不过,任何 0x0 偏移区段的最终放置位置仍可能会失败。因此,确保 ELF32 的文件基不会下溢更有意义。为此,您可以将可执行文件的文件基址设置为 mm 基址指针或最低 ELF 信息值(以较高者为准)。 完成此操作后,可以完全忽略 wrapping_add/sub,如果使用它,则将其保持原样以供 64 位使用。对于大多数二进制文件,这并不相关,因为许多 Linux 和 Android 二进制文件都是位置无关可执行文件 (PIE) - 这意味着它们将获得随机化的基址 - 该基址始终至少位于 mm 的基址指针或更高位置。设置相对基址后,其余 ELF 段加载将正常进行,即将 mm/文件偏移量转换为虚拟地址,然后从该地址进行映射,就像对 ELF64 文件所做的那样。
请注意,如果未按文件中的指定方式映射任何偏移量较低的不可重定位 ELF 二进制文件,这些文件将无法按预期运行。虽然这可能是一个需要解决的问题,但由于大多数现代 armv7-linux 二进制文件都是 PIC 或 PIE,因此这不是优先事项。
内存映射和 ASLR
虽然 ASLR 同时适用于 PIE 基地址(在上方)和堆栈及堆起始点(在下方),但值得注意的是,上述用户 VMAR 的更改会影响后续的映射。在当前尚不成熟的 ASLR 实现中,系统会提取一定数量的熵位并将其应用于所需的目标地址,从而使该地址在内存中高于或低于起始点。默认情况下,由于可寻址内存的差异较大,这会在 32 位线程上失败。AArch64 期望能够添加 28 位熵。在 Linux 上,AArch32 幸运的话可以实现 18 位的熵。
虽然当前方法在计算方面成本最低,但灵活调整用户 VMAR 大小的能力意味着可以相应调整随机化策略。对于每个需要随机化的位置,地址随机化及其随机化方向的过程将集中到 MemoryManager 函数中。然后,它将根据提供的最大大小或范围计算 ASLR 熵掩码。掩码将比左移的位数少一位,该位数是范围大小中的位数减去页面大小中的位数。这意味着,对于最多 4 GB 的可寻址区域,最多可以应用 19 位的熵。对于 64 位可寻址区域,该值会高得多,但原始的 AArch64 特定熵位用作上限。
实际上,任何给定尝试的随机化范围都不是完整的地址空间,而是其他映射对象之间的区域,例如堆顶部与堆栈之间的区域。这就是为什么随机化熵在实践中最终会低得多的原因。 此设计更侧重于实现 AArch32 支持 - 应单独进行一项工作来衡量 ASLR 实现中的熵,并确定是否应单独修改它,而不考虑任何性能问题。
如果性能成为问题,我们可以缓存熵掩码,并改用硬编码的掩码,一个用于 arch32 任务,一个用于正常任务。最初,目标是简化调用者的体验,但可以根据需要引入优化,例如,如果它对程序启动时间产生负面影响。
vDSO
虚拟动态共享对象是一种合成共享库,当进程执行时,内核会将其映射到进程中。其目的是帮助平滑用户空间和内核之间的接口。vDSO 是由内核提供的代码,旨在由进程在用户空间中运行。例如,通常会提供系统调用帮助程序来确保使用内核调用惯例。同样,通常也有与系统时间相关的调用,当用户空间进程尝试检查系统时间时,可以使用任何特定于架构的优化。
如需详细了解如何在 Starnix 中管理 vDSO 时间,请点击此处。 目前,vDSO 依赖于 Zircon(即 fasttime)以库形式呈现的功能。不过,vDSO 必须以 AArch32 为目标平台进行构建,而 Zircon 并不打算支持为 AArch32 架构进行构建。因此,可以提取 fasttime 逻辑,以便将其纳入特定于 AArch32 的 vDSO 帮助程序中。请务必注意,在实现长期 Zircon vDSO 之前,此方法只是权宜之计。通过依赖与 AArch64 Starnix 相同的接口,而不会为 Zircon 增加额外开销,此模型应能为 AArch32 提供类似的上下文切换次数减少,而无需为临时解决方案投入大量技术工作。
除了 fasttime 之外,今天的 Starnix vDSO 不能依赖于其他共享对象。 时间计算通常至少部分使用 64 位整数完成。 Clang 和其他编译器为 ARM 提供辅助程序(例如 compiler-rt),以执行 64 位模数和除法运算。最初,此工作将仅在 C++ 中使用二进制除法并导出预期符号。很可能需要将这些代码替换为优化的汇编代码。可能还需要向 AArch32 vDSO 添加其他符号和函数,这些符号和函数可能与 AArch64 vDSO 不同。您可以根据需要添加这些内容。同样,如果时间相关功能的需求不合理,可以从 AArch32 vDSO 中移除这些功能。
AArch32 vDSO build 本身也必须与 AArch64 vDSO 并行完成。 因此,必须添加一个新目标,该目标可以使用 arch32 工具链强制构建,并且不依赖于 Zircon 标头。将在内核结构中添加一个新字段,该字段与“vdso”字段类似,即“arch32_vdso”。它将是一个 Option<>,用于在缺少 arch32 vDSO 时仅允许 arch32 可执行文件正常失败。
堆栈准备
在启动可执行文件之前,最后一步是设置其堆栈,以便 ELF 入口点处的代码知道如何处理。Starnix 已经处理了这些步骤,从设置辅助向量 (AUXV) 到放置实参和环境变量。在 64 位平台上,两个实参、环境变量和 AUXV 的整数的指针均为 64 位宽。此外,堆栈必须以 16 字节对齐。
这意味着,至少必须针对 ELF32 二进制文件调整指针和值的大小。在当前的 Starnix 实现中,系统会构建一个堆栈向量,然后将其复制到内存中的相应位置。支持 AArch32 的最简单方法是复制代码路径并单独处理。不过,堆栈向量是一个无符号 8 位字节的向量。这意味着,可以封装对堆栈的写入,以选择正确的宽度(8 字节或 4 字节)。进行此类更改后,最终堆栈应会自动调整为正确的大小,因为要写入的值必须已正确调整大小。此外,由于 32 位堆栈必须 8 字节对齐,因此 16 字节对齐已满足该要求。
当前 Starnix 堆栈准备工作中的一个缺失部分是可通过探测发现但可能在进程执行时通过 HWCAP 条目披露的功能,例如是否支持 Thumb 模式或允许哪种类型的浮点运算。这些条目最早用于 AArch32 glibc 和 bionic 加载器。需要向每个内核/架构/处理器添加一个新函数,该函数会返回处理器支持的硬件功能列表。各实现可以使用“zx_cpu_get_features”调用来确定如何填充“AT_HWCAP”字段。最初,这可以专门为 arch32 支持而添加,因为当前支持的架构在没有任何支持的情况下也能正常运行。
入口点
最后,可执行文件或其解释器将提供 Starnix 或 Linux 内核启动线程的入口点。通常,此入口点必须与指令对齐。不过,ARM 上的 ELF32 使用未对齐的入口点来指示处理器应在 Thumb 指令集执行模式下启动。如果入口点与 0x1 进行按位与运算的结果为 0x1,则必须在执行代码之前在 CPSR 上设置 T 位,否则处理器会尝试将 T32 指令解释为 A32。(请注意,这种对齐方式也适用于处理器级别的异常返回。)
由于新线程的入口点必须是程序计数器(和异常链接寄存器值),因此它将通过 ThreadStartInfo 转换为 zx_thread_state_general_regs_t 来填充。kernel/arch/arm64/loader.rs 中的 From<> 实现可以检查 PC 的对齐情况,然后相应地设置 Thumb 模式。在所有其他情况下,Thumb 值将通过用户空间中的分支指令启用或停用,并且其状态需要在对 Starnix 的调用中保持不变。
配置完成后,可以启动 AArch32 可执行文件,但由于使用 AArch64 系统调用号调用 AArch32 系统调用,该文件会立即失败。
系统调用
除了错误情况之外,系统调用是用户空间软件与 (Starnix) 内核之间的主要接口。这意味着,我们需要应对以下两个主要挑战:
- 调用约定和路由
- 用户空间内存读取和写入
AArch32 系统调用
如前所述,Linux AArch32 系统调用使用的编号与 AArch64 系统调用使用的编号不同。此外,AArch32 使用的系统调用编号寄存器与 AArch64 不同,并且两者之间的系统调用参数也可能不同。
从参数的角度来看,无需处理指针大小差异,因为在进入 Starnix 时寄存器会被扩展。数字和形参的差异带来了更具挑战性的问题。Starnix 必须能够按编号查找系统调用,并将该编号映射到实现。虽然可以强制每个系统调用实现执行一些 arch32 检查,但这项工作可以在系统调用处理时完成。也就是说,Starnix 可以设置一个仅用于 arch32 任务的辅助系统调用表。这会导致内存使用量略有增加,但它提供了一种强制执行 AArch32 与 AArch64 用户空间 ABI 的方法,并提供了一种通过 AArch32 感知封装容器直接或间接共享系统调用实现的简单方法。
这种方法至少在最初阶段需要复制一些宏,不过也可以尽量减少复制。
AArch32 数据结构
虽然系统调用参数中的指针不会带来挑战,但在从 AArch32 用户空间复制结构或将结构复制到 AArch32 用户空间时,情况并非如此。AArch32 软件将使用 AArch32 Linux UAPI 中定义的结构,而不是 AArch64 UAPI。例如,IO 向量 (iovec) 是一种结构,它只是一个指针和大小配对的数组。在 AArch32 上,iovec 是两个 32 位字段,而在 AArch64 上,它们都是 64 位字段。这意味着,任何使用 iovec 的系统调用都必须进行适当调整。
不过,此问题并非会影响所有系统调用。大约三分之一的 AArch32 系统调用与对应的 AArch64 系统调用相比,具有不同的结构。对于特定调用,可以在特定于相应架构的系统调用实现中处理特定结构。例如,stat 系统调用需要返回 stat64 结构,而该结构与 AArch64 stat 调用返回的结构不匹配。这可以通过在 kernel/arch/arm64/syscalls.rs 下添加新的统计信息入口点以及进行类型转换来解决。
可以更通用地处理常见的结构,例如 iovec。Starnix 的 MemoryManager 提供了一个用于读取 iovec 的辅助函数 read_iovec()。它依赖于 read_objects_to_smallvec(),其中类型多态性采用 UserBuffer 别名的 SmallVec 作为要读取到的类型。对于所有现有架构,UserBuffer 是 iovec 的 Rust 版本。通过向 Starnix 的 UAPI 添加 UserBuffer32,可以创建并行 AArch32 兼容的 iovec SmallVec 类型。然后,可以通过 read_iovec32() 函数公开此功能,该函数本身可以返回一个通用的 UserBuffer(再次感谢 into()/collect()!)。虽然它不会隐藏 AArch32 iovec 读取,但会将 AArch32 特定类型的公开范围限制为调用代码的其余部分。目前有一个开放的设计问题,即是将任务的 arch32 字段传递给 read_iovec() 调用,还是调用一个单独的函数更有意义。这种标记可能会简化向常见系统调用实现(如 sys_writev())添加对 arch32 的支持。
信号
信号处理和 ptrace() 通常是与底层架构密切相关的领域。在大多数情况下,需要添加信号处理和 ptrace 的机械更新,以正确连接正确的信号结构内容和架构信息。
系统调用重启会稍微复杂一些,因为重置程序计数器/指令指针将取决于模式:A32 或 T32,因为 T32 指令为 2 字节,而 A32 指令为 4 字节。
此方面需要更多关注,但一旦其余 arch32 支持正常运行,探索起来就会更加轻松。
测试
与大多数软件一样,测试中的代码行数预计会超过实现 AArch32 支持所需的行数,尤其是对于 Zircon 而言。本部分介绍了计划中的测试代码。
工具链
Fuchsia Clang 工具链和工具链的其他部分在 Fuchsia build 之外有充分的支持和测试。需要考虑两个方面:build 配置测试和内在工具链测试。通常,两者是相互关联的,目标是确保 armv7-linux-gnueabihf 的 build 能够持续正常运行。一个当前的示例是使用“--allow-experimental-crel”标志。为 Linux/ARM 启用此标志会导致二进制文件损坏。此失败可能是由工具链中的代码生成错误导致的,但 build 配置决定了它是否会中断任何正在进行的 build。
尽早发现这些变化非常重要。如果我们要在不进行测试的情况下支持此功能,则只能在 Zircon 测试(稍后讨论)中的崩溃或访问 AArch32 Starnix VDSO 失败时捕获此故障。我们需要为已启用的任何工具链目标元组启用核心/基本 LLVM 测试,以确保操作和正确的代码生成。
Zircon 测试支持
Zircon 测试具有挑战性,因为测试应明确执行受限模式,而不依赖于功能齐全的监督程序/内核。此外,如果测试不从替代位置(例如完整容器)加载二进制文件,则代码必须是自包含的。
对于 Zircon utest,我们将利用构建系统的 loadable_module() 支持来构建特定于架构的共享库,这些库可以使用 elfldltl 库进行动态加载和映射。这样,集成测试就可以在 AArch64 的主机架构上运行,同时从以 AArch32 为目标 (armv7-linux) 的共享库加载和解析符号。这些共享库将像 BOOTFS 上的任何其他可执行代码一样加载并映射为可执行文件。
其余要求围绕主机支持的内容以及受限的 AArch32 共享库可访问的内容展开。共享库必须映射到可寻址内存(低于 4Gb),以及任何共享指针,例如 std::atomic 或 TLS 存储地址。在此基础上,测试还必须确定宿主架构是否支持执行 AArch32,如果支持,则针对以宿主为目标的受限模式和以 AArch32 为目标的受限模式运行测试套件。
Starnix 测试支持
初始验证将针对在 Starnix 下运行的最小 Linux 系统完成。这样可确保在验证 Starnix 的工具链和 build 系统更改时,依赖项最少。为了实现这一点,ARM64 Debian 容器将在其映像中添加 AArch32 sysroot,并且 example/hello_debian 示例将扩展为包含 AArch32 示例,包括动态链接的 C++ 和静态链接的汇编。目标支持和工具链 sysroot 必须大致同步。
初始测试环境包括 Starnix 针对 AArch64 库或 AArch32 执行一个二进制文件 (hello_debian)。不会有其他流程或工作正在进行。不过,仍需进行常规测试,以涵盖与 AArch64 的差异:
- 系统调用支持覆盖范围
- 信号和 ptrace 行为
- 混合执行模式环境(共享 sysroot、容器等)
- 每个受支持的 ELF 文件类型的测试
- ASLR 熵测量
其中一些测试需要专门为 Starnix 编写,但理想状态是使用现有项目(例如 Linux 测试项目),这些项目旨在确保 Linux 持续兼容。
Zircon
应添加一些特定于 AArch32 的测试:
- CPSR:M[4]:A32 执行
- CPSR:T|M[4]: T32 执行
- 寄存器设置和清除
- >32 位寄存器设置、清除行为
- 系统调用处理
- 非系统调用同步异常处理
- 异步异常处理
- SError 处理
- CPSR:Q:通过系统调用设置饱和位并保持饱和位
- CPSR:GE:通过系统调用保持 GE 位
- IT 位使用情况
- 线程的 read_state 和 write_state API
虽然基本条件(例如 A32 和 T32 的执行模式位以及寄存器操作)很容易验证,但更复杂的异常处理可能更具挑战性。例如,从 EL0 诱发 SError 可能需要提供对未映射内存的测试访问权限才能进行交互。IT 位测试需要能够执行中间状态,这应该可以使用线程内测试来实现,但可能需要一些额外的调试器驱动的测试用例。
还应使用受限的 aarch32 目标来执行现有测试:
- FloatingPointState
- 本奇语
- ExceptionChannel
- InThreadExecution
- EnterBadStateStruct
- KickBeforeEnter
- KickWhileStartingAndExiting
- KickJustBeforeSyscall
目前,测试会选择编译器目标架构进行测试。虽然可以对 arch32 进行硬编码检查和特定测试,但最好将 32 位架构作为另一个测试目标。为此,我们需要能够跨共享的 AArch64 架构重用某些功能,同时为 AArch32 ABI 定义测试夹具。重构这些测试以使用具有特定于架构的类实现的通用基类,将使派生 arch32 子类变得更加容易。如果每个架构类型都有可靠的测试,则可以使用 TEST_P 宏根据要使用的特定于架构的类实例来对测试进行参数化。针对同一功能的常见测试将避免因测试复杂性导致的错误而遗漏覆盖范围。不过,在需要用于非通用测试用例时,仍可使用 TEST_F()。
Starnix
如工具链部分所述,初始集成测试将使用 hello_debian 完成。这样可以隔离功能:加载和系统调用。从单元测试的角度来看,现有的类测试将扩展到涵盖添加的功能。
除此之外,还需要进行新的测试来练习 AArch32 特定的行为,例如内存限制和系统调用。除了常规的单元测试和功能测试之外,还将使用 Linux 测试项目和其他系统调用测试框架来确保覆盖率和正确性。
基础架构中的测试
基础设施中的自动化测试必须注意两个主要问题:
- 缺少 AArch32 支持
- 硬件变体
在 ARM64 处理器上进行测试时使用的任何硬件加速虚拟化功能都不太可能适用于 AArch32 测试,因为面向服务器的处理器通常不支持该功能。完全模拟的虚拟化将正常运行,但速度会更慢。
此外,应在预期会使用此功能的任何硬件上进行测试,因为不同的处理器实现可能会存在异常处理缺陷。
实现
实现策略如下:
- 更新了 CIPD 中的 Debian arm 映像
- 工具链更改和测试,以启用 arm clang
- Zircon 更改,用于启用和测试 32 位受限模式
- Starnix:
- 新任务标志
- ELF 解析和加载
- AArch64 寄存器
- 可调整大小的用户 VMAR 更改(使用 ASLR)
- Linux UAPI 变更
- AArch32 vDSO
- 加载程序变更
- arch32 系统调用表和支持
- hello_debian 示例
接下来,我们需要确定需要哪些系统调用,并继续实现这些调用。此外,还可以继续进行调试器和额外工具链调查。
性能
重要的是,这些更改应尽可能减少对 Fuchsia 或 Starnix 中任务的正常 64 位操作造成的开销。这可以通过 Zircon 版本之间的微基准进行评估,不过我们也可以只评估完整的系统基准,以确定是否存在重大损失。
对于 AArch32 操作,受限模式基准将是比较性能与 AArch64 受限模式的第一条途径。之后,最好使用实际工作负载针对类似的 Linux 系统进行基准比较。
持续进行的系统调用优化、内存利用率和功耗监控将有助于确认此功能的可行性。
工效学设计
在 Zircon 中,当前方法不会导致特定的 API 更改。相反,AArch32 支持是通过在受限模式下支持现有的架构 ABI 来实现的。这种方法不会显著增加复杂性,也不会降低开发者的舒适度。使用此功能的任何开发者都需要自行实现 ABI 的其余部分。
在 Starnix 中,这些更改最初会受到标志保护。必须区分的代码路径应尽可能靠近发生区分的位置,并尽可能减少整个 starnix 中出现的代码分支。例如,如果可以在 MemoryManager 中强制执行内存限制,那么从开发者角度来看,所有依赖于 MM 的系统调用和其他功能都将保持不变。
向后兼容性
AArch32 用户空间支持本质上是一种向后兼容性功能。预计 AArch32 的实用性会随着时间的推移而逐渐减弱。
安全注意事项
此提案更改了 Zircon 处理异常和 ABI 的方式。这意味着应仔细评估。此外,代码应采用防御性编程方式编写,主动禁止意外的代码路径。如果基准表明需要优化,则可以进行优化。
从 Starnix 的角度来看,正在添加额外的系统调用和信号处理路径,以及额外的文件解析。地址空间随机化也会直接受到内存占用空间减少的影响。这些都是与安全相关的更改。
我们应尽可能重用现有的 Linux 安全测试工具,例如 syzkaller。
隐私注意事项
此更改不应带来任何特定的隐私权变更。
文档
受限模式 API 文档将更新,以指明允许的其他寄存器标志。
应更新 Starnix 文档,以包含有关如何使用和开发 AArch32 支持的信息。其他文档应主要侧重于对开发者的影响,例如管理两个系统调用表或在 AArch64 和 AArch32 路径上测试变更。
在 Starnix 中运行的 AArch32 二进制文件示例位于 src/starnix/examples/hello_debian 中。
缺点、替代方案和未知因素
此提案会增加长期维护成本以及系统复杂性。但这样做的代价是,在面向消费者的 SoC 上支持 AArch32 Linux 软件的剩余年份内,无法访问该软件。
没有性能良好的替代方案来支持未修改的 AArch32 程序,替代方案是将所有代码重新编译为 AArch64。这些性能较差的替代方案包括转译或选择性用户模式模拟。
我们持续调查的主要领域是,在分析同一回溯中同时具有 AArch64 和 AArch32 调用约定和指令集的线程时,如何实现高质量的调试体验。
具体而言,当前的调试工作流程依赖于 libunwind 来通过 AArch64 上的 Linux 调用堆栈进行展开,并依赖于 zxdb 来分析给定调用帧内的状态。需要向 zxdb 和 libunwind 添加对类似行为的支持。libunwind 应该已经支持 AArch32 Linux ABI,但不太可能支持在同一线程中在 ABI 之间干净地转换。我们的目标是让这两款工具达到同等水平,但这需要进行更多调查和实验。
除了调试之外,开发者工作流还可能因全面测试而受到影响。使用强大的基于 Intel 的环境的开发者需要完全模拟 AArch64/AArch32 环境。不过,如果他们已经以 AArch64 为目标平台,那么现在确实如此。如前所述,这里增加的复杂性在于面向服务器的 ARM SoC 缺少 AArch32 支持。基于 Intel 的开发者可能一直依赖于加速的服务器端 AArch64 测试,但现在需要使用支持这两种执行模式的桌面设备或托管设备。可能可以使用 starnix 中的线程个性化设置来启用或停用特定于 AArch32 的调用路径,以进一步减少在没有 AArch32 硬件支持的情况下无法大规模访问的代码量。
在先技术和参考资料
引用已嵌入为链接。Linux 将 AArch64 上的 AArch32 支持称为兼容模式,并提供了现有技术。