RFC-0264:在 Fuchsia 上运行未修改的 AArch32 Linux 程序

RFC-0264:在 Fuchsia 上运行未修改的 AArch32 Linux 程序
状态已接受
区域
  • 外部 ABI 兼容性
  • 内核
  • 安全
  • 工具链
说明

通过受限模式和 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@

Reviewers:

  • maniscalco@
  • lindkvist@
  • mcgrathr@
  • jamesr@
  • mvanotti@

咨询了

  • travisg@
  • phosek@
  • abarth@
  • tkilbourn@
  • olivernewman@
  • ajkelly@

社交

我们首先制作了该方案的技术原型,以验证其技术可行性并进行初步成本评估。我们已将该原型与受影响组件的代表分享,并附上了本文档的草稿。通过原型实现,我们能够更准确地评估潜在的短期和长期影响,设计审核则促成了对设计选择的深入讨论,并发现了一些现已解决的问题。

要求

可能有很多指导性要求,但选择问题陈述后,我们会对后续设计应用以下要求:

Starnix 中的 AArch32 支持必须:

  • 能够解析和加载现代 armv7-linux 二进制文件
  • 能够管理单独的 AArch32 系统调用路径
  • 合理控制,不会影响整个 Starnix 中的代码
  • 能够停用(例如,通过构建时标志)
  • 尽可能减少对现有 AArch64 代码路径的影响
  • 能够在同一 starnix 实例上执行 AArch64 和 AArch32 程序

值得注意的是,虽然 AArch64 和 AArch32 软件可以在同一 starnix 内核实例上并行执行,但目前不打算针对这两个环境之间的互动提供行为保证。

Zircon 中的 AArch32 支持必须:

  • 仅适用于受限模式线程的函数
  • 允许共享进程同时托管 AArch32 或 AArch64 受限模式代码
  • 不会造成过度的长期维护负担
    • 尽量减少更改 API Surface
    • 易于移除和/或停用
    • 不会影响在 Zirecon 下运行的所有任务
    • 无需对 Zircon 进行大量更改

虽然未来可能需要支持其他 Fuchsia 架构,但我们目前希望运行的程序以 AArch32 环境(例如 DebianAndroid)为目标平台。提供对 armv7-linux 的完美模拟并非我们的目标,而是基于 starnix 的 API 层来支持公开的处理器 AArch32 功能。

设计

Starnix 可与 Linux 内核兼容。Linux 内核已经支持所谓的“兼容”模式。启用 CONFIG_COMPAT 后,64 位 Linux 内核可以运行主机 CPU 架构支持的 32 位或 64 位 ISA 目标二进制文件。对于 Arm 处理器,这分别是 AArch64 和 AArch32。

通常,CONFIG_COMPAT 可启用 32 位架构。此设计旨在实现在 Starnix 上实现相同的行为,以便 Starnix 上的 Android 可以运行 32 位 (AArch32) 或 64 位 (AArch64) ARM 二进制文件。在 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(State0)和 T32(State1)指令集(作为 ARMv7 支持)。为了支持 Linux 的 32 位兼容用户空间,这两个指令集是必需的。请务必注意,许多面向服务器的 64 位 ARM 芯片都不支持任何 AArch32 功能,但在支持该功能的情况下,这两组功能都将可用

核心寄存器映射

AArch32 到 AArch64 寄存器映射

AArch32 AArch64
R0-R12 X0-X12
单板回转 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 位。

从架构上讲,对于 AArch64,AArch32 CPSR 会在发生异常时映射到 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 设置为指示溢出或饱和。Thumb 指令使用 IT 进行条件执行,对于 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 扩展中的其他状态。

其他寄存器映射:计数器和线程

除了上述 AArch32AArch64 寄存器映射之外,还有许多其他寄存器映射。不过,有两个与此设计的其余部分相关 - 计数器和线程存储。

在现代操作系统中,所有进程都有一个虚拟动态共享对象映射到其地址空间,该对象会尝试在不进入内核的情况下提供对时间检查功能的访问权限。这通常是通过计数秒针或周期来完成的。用于此目的的 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),系统会从异常链接寄存器 (ELR_EL0) 恢复 R15。

另请注意,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 的自然延伸,因此此方法非常适合 AArch32,但如果将来支持不同的 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 中处理该异常,将异常状态打包,然后将其传回给 AArch64 EL0 受限模式监督程序线程。

如前所述,AArch32 异常将由 AArch64 异常矢量处理。目前,Zircon 有意不处理 AArch32 异常。同步异常、异步异常和(异步)系统错误异常需要另外三个处理程序。Zircon 的异常处理程序在很大程度上是使用汇编宏实现的。这样一来,添加新的处理脚本就非常简单了。另一项简化是,入站异常应由受限模式监督程序处理。这样,异步异常和系统错误 (serror) 便可由与 AArch64 对等的处理程序处理。只有同步异常需要特殊处理,并且其中只有系统调用需要特殊处理。对于传入的系统调用,我们可以将任何调用(使用 r7 作为编号)传递给受限模式退出处理代码。如果收到系统调用且线程未标记为受限,我们将 panic Zircon,因为它不应能够到达该代码路径。同样的行为也将应用于每个其他 32 位处理程序的封装容器中。

这种方法应最大限度地减少对 Zircon 所需的更改,同时确保 AArch32 异常得到妥善保留并传递给受限监督程序线程,而不会在受限模式之外意外启用 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)。不过,它未在我们的构建系统中配置。为此,我们必须为目标架构元组添加配置,并添加必要的构建系统目标。

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 包含适用于 Linux 和 Linux/Bionic 目标的自有 build 目标。这两项工作都可以基于工具链团队的现有工作进行构建。在 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 脚本生成 Rust 绑定。由于 UAPI 包含传入和传出内核接口的结构体,因此 Starnix 必须使用 AArch32 UAPI 才能正确处理这些结构体。

需要解决以下三个主要方面:

  1. 32 位指针和类型
  2. 并行访问两个 UAPI
  3. 系统调用命名
指针和类型大小

对于第一个问题,存在两个问题。第一个是,我们需要将所有指针替换为 AArch32 大小的指针类型。Starnix 通常使用 uaddr 和 uref。对于 32 位大小的指针,只需添加 uaddr32 和 uref32,并确保将它们替换为指针和引用,就足以满足需求。此外,如果可以通过 From/Into 轻松转换为 uaddr 和 uref,那么除非明确需要,否则 uaddr32 应该在很大程度上变得不可见。

第二个问题是,相同类型的名称大小不同。例如,在 AArch64 上,long 的宽度为 64 位,在 AArch32 上为 32 位。这意味着,两个 UAPI 之间无法共享默认类型映射,并且必须为每个 UAPI 定义并注入一组特定的内核 C 类型。

并行 UAPI 访问

有了生成 ARM UAPI 绑定的明确路径,Starnix 必须能够同时访问 AArch64 和 AArch32。幸运的是,可以通过将生成的“arch32”UAPI 映射到 linux_uapi 的 lib.rs 中的嵌套命名空间来解决此问题。例如,“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* trait 等)。因此,可以类似于 uaddr32/uref32 添加 ELF32 支持:定义与 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

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 info 值(以较高者为准)。完成后,可以完全忽略 wrapping_add/sub,并将其保持不变以供 64 位使用(如果使用)。对于大多数二进制文件,这不相关,因为许多 Linux 和 Android 二进制文件都是位置无关可执行文件 (PIE),这意味着它们将获得随机基址,该基址始终至少位于 mm 的基址指针或更高位置。设置相对基准后,ELF 段加载的其余部分将照常进行 - 将 mm/file 偏移量转换为虚拟地址,然后从该地址进行映射,就像对 ELF64 文件所做的那样。

请注意,如果任何偏移量较小的不可重定位 ELF 二进制文件未按文件中指定的方式映射,则其性能将无法达到预期。虽然这可能是一个需要解决的问题,但它并不是优先事项,因为大多数现代 armv7-linux 二进制文件都将是 PIC 或 PIE。

映射和 ASLR

虽然 ASLR 同时适用于上述 PIE 基地址和下述堆栈和堆起始点,但值得注意的是,对上述用户 VMAR 所做的更改如何影响后续映射。在当前处于起步阶段的 ASLR 实现中,系统会提取一定数量的熵位并应用于所需的目标地址,从而使其在内存中的位置高于或低于起始点。默认情况下,这在 32 位线程中会失败,因为可寻址内存存在很大差异。AArch64 预计能够添加 28 位熵。在 Linux 上,AArch32 能达到 18 位的熵已是万幸。

虽然当前方法在计算方面所需的开销最少,但能够灵活调整用户 VMAR 的大小意味着随机化策略也可以进行类似调整。对于每个需要随机化的地址,地址随机化过程及其随机化方向将集中到 MemoryManager 函数中。它会根据提供的最大大小或范围计算 ASLR 熵掩码。掩码将是左移(范围大小的位数 - 页面大小的位数)后小于 1 的值。这意味着,对于 4GB 的可寻址区域,最多只能应用 19 位熵。对于 64 位可寻址区域,该值会高得多,但原始的 AArch64 专用熵位数会用作上限。

实际上,任何给定尝试的随机化范围都不是整个地址空间,而是其他映射对象之间的区域,例如堆顶与堆栈之间的区域。这就是为什么在实践中随机化熵会低得多的原因。此设计更侧重于启用 AArch32 支持 - 应单独进行工作来衡量 ASLR 实现中的熵,并确定是否应单独修改它以免出现任何性能问题。

如果性能成为一个挑战,我们可以缓存熵掩码并改用硬编码掩码,一个用于 arch32 任务,另一个用于常规任务。最初,目标是简化调用方的体验,但可以根据需要引入优化措施,例如,如果它会对程序启动时间产生负面影响。

vDSO

虚拟动态共享对象是一种合成共享库,内核会在进程执行时将其映射到进程。其目的是帮助平滑用户空间与内核之间的接口。vDSO 是由内核提供的代码,旨在由进程在用户空间中运行。例如,系统调用辅助程序通常会存在,以确保使用内核调用惯例。同样,系统时间相关调用通常允许在用户空间进程尝试检查系统时间时使用任何特定于架构的优化。

如需详细了解如何在 Starnix 中管理 vDSO 时间,请点击此处。 目前,vDSO 依赖于 Zircon 以库的形式提供的功能,即 fasttime。不过,vDSO 必须以 AArch32 为目标平台进行构建,而 Zircon 不打算支持针对 AArch32 架构进行构建。因此,可以提取快速时间逻辑,以纳入 AArch32 专用 vDSO 帮助程序中。请务必注意,在长期的 Zircon vDSO 成熟之前,此方法只是权宜之计。通过依赖于与 AArch64 Starnix 相同的接口,而不会为 Zircon 造成额外的开销,此模型应可让 AArch32 实现类似的上下文切换次数减少,而无需为临时解决方案投入大量技术工作。

除了 fasttime 之外,目前的 Starnix vDSO 不能依赖于其他共享对象。时间计算通常至少部分使用 64 位整数进行。Clang 和其他编译器会提供 compiler-rt 等辅助程序,以便 ARM 执行 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 条目披露,例如是否支持拇指模式或允许哪些类型的浮点运算。这些条目最早在 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) 内核之间的主要接口。这意味着,我们需要应对两个主要挑战:

  1. 调用惯例和路由
  2. 用户空间内存读写
AArch32 系统调用

如前所述,Linux AArch32 系统调用使用的编号与 AArch64 系统调用不同。此外,AArch32 使用的系统调用号寄存器与 AArch64 不同,这两种架构的系统调用参数也可能不同。

从参数角度来看,无需处理指针大小差异,因为寄存器会在进入 Starnix 时进行扩展。数字和参数的差异构成了更有趣的挑战。Starnix 必须能够按编号查找系统调用,并将该编号映射到实现。虽然可以强制每个系统调用实现执行一些 arch32 检查,但也可以在系统调用处理时执行此工作。也就是说,Starnix 可以设置一个仅用于 arch32 任务的次要系统调用表。这将导致额外的内存用量增加不多,但会提供一种强制执行 AArch32 与 AArch64 用户空间 ABI 的方法,并提供一种通过 AArch32 感知封装容器直接或间接共享系统调用实现的简单方法。

这种方法至少在开始时需要重复使用一些宏,但也可能能够尽可能减少重复。

AArch32 数据结构

虽然系统调用参数中的指针不会造成问题,但从 AArch32 用户空间复制结构或向其中复制结构时,情况并非如此。AArch32 软件将使用 AArch32 Linux UAPI 中定义的结构体,而不是 AArch64 UAPI。例如,IO 矢量 (iovec) 是一种结构,它只是指针和大小配对的数组。在 AArch32 上,iov 是两个 32 位字段,而在 AArch64 上,它们都是 64 位字段。这意味着,使用 iovec 的任何系统调用都必须进行适当调整。

不过,此问题不会影响所有系统调用。大约三分之一的 AArch32 系统调用与其 AArch64 对应项具有不同的结构。对于特定调用,可以通过特定架构的系统调用实现来处理特定结构体。例如,stat 系统调用预期返回 stat64 结构,但该结构与从 AArch64 stat 调用返回的结构不匹配。可以通过 kernel/arch/arm64/syscalls.rs 下的新的 stat 入口点以及类型之间的转换来解决此问题。

可以更通用地处理常见的结构体(例如 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 build 之外,Fuchsia Clang 工具链和工具链的其他部分也获得了充分的支持和测试。需要考虑两个方面: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 的工具链和构建系统更改时,依赖项尽可能少。为此,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 定义测试 fixture。重构这些测试以使用具有架构专用类实现的通用基类,将有助于更轻松地派生 arch32 子类。使用可靠的架构专用类型时,可以使用 TEST_P 宏根据要使用的架构专用类实例对测试进行参数化。针对同一功能的常规测试可避免因测试复杂性导致的错误而导致覆盖率缺失。不过,在非通用测试用例需要时,仍可使用 TEST_F()。

Starnix

如工具链部分所述,初始集成测试将使用 hello_debian 进行。这样可以隔离功能:加载和系统调用。从单元测试的角度来看,现有的类测试将扩展以涵盖添加的功能。

此外,还需要进行新的测试来演练 AArch32 专用行为,例如内存限制和系统调用。除了常规的单元测试和功能测试之外,Linux 测试项目和其他系统调用测试框架还将用于确保覆盖率和正确性。

基础架构中的测试

在基础架构中进行自动化测试时,必须注意以下两个主要问题:

  • 缺少 AArch32 支持
  • 硬件差异

在 ARM64 处理器上进行测试时使用的任何硬件加速虚拟化都不可能与 AArch32 测试搭配使用,因为面向服务器的处理器通常不支持。完全模拟的虚拟化功能将会正常运行,但运行速度会较慢。

此外,应针对预计要使用此功能的所有硬件进行测试,因为不同的处理器实现之间可能会存在异常处理问题。

实现

实现策略如下:

  • 更新 CIPD 中的 Debian arm 映像
  • 工具链变更和测试,以启用 arm clang
  • 有关启用和测试 32 位受限模式的 Zircon 更改
  • Starnix:
    • 新增任务标志
    • Elf 解析和加载
    • AArch64 寄存器
    • 可调整大小的用户 vmar 更改(使用 ASLR)
    • Linux UAPI 变更
    • AArch32 vDSO
    • 加载器变更
    • arch32 系统调用表和支持
    • hello_debian 示例

然后,我们需要确定需要哪些系统调用,并继续实现它们。此外,调试程序和额外的工具链调查可以继续进行。

性能

请务必确保这些更改对 Fuchsia 或 Starnix 中任务的正常 64 位操作的开销最小。我们可以通过 Zircon 版本之间的微基准测试来评估这一点,不过我们也可以仅评估完整的系统基准测试,以确定是否存在重大性能损失。

对于 AArch32 操作,受限模式基准测试将是与 AArch64 受限模式比较性能的第一条路径。之后,最好使用类似的 Linux 系统对真实工作负载进行基准测试。

持续进行的系统调用优化、内存用量和功耗监控都将有助于确认此功能的可行性。

工效学设计

在 Zircon 中,当前方法不会导致一组特定的 API 更改。而是通过在受限模式下支持现有的架构 ABI 来启用 AArch32 支持。这种方法不会显著增加复杂性,也不会降低开发者的工效学。任何使用此功能的开发者都需要自行实现其余的 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 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 支持称为兼容模式,并提供了先前技术。