Skip to content

Latest commit

 

History

History
257 lines (184 loc) · 11.7 KB

File metadata and controls

257 lines (184 loc) · 11.7 KB

openarch:接口层与多指令集后端的实现方案

2026-08-20。对象是 mcpplibs/openarch 0.1.0,以及为使其可被验证所需的引擎与 索引改动。

1. 这一层是什么,以及它凭什么成立

生态的三层各自回答一个不同的问句:

问句
openkal 一个程序向内核要求什么
openarch 一个内核向机器要求什么
openhal 一个程序向设备要求什么

openarch 承载机制而非策略:它能切换上下文,但不做调度;它能装载陷入向量、 构造页表项,但不判定一次缺页意味着什么、也不规定内存怎么排布。

这一层与另外两层有一处根本差别,而它决定了本方案的形状:openkal 与 openhal 的实现者集合是开放的——谁都可以为一个新内核或一块新设备写后端,而 openarch 的实现者集合是有界且很小的,并且没有人会把 aarch64 的上下文切换 写成第二种样子。因此接口与实现必须共同演化:判定一个接口是否写错了,唯一的 办法是加入第二个架构。

2. 现状,以及"门槛"这个说法的含义

仓库当前共 82 行接口 加 一份 riscv64 的裸汇编:

src/context.cppm              openarch.context —— context / switch / init
src/arch/riscv64/context.S    裸汇编的切换
src/arch/riscv64/context_init.cpp
examples/switch/              两个上下文之间来回切换的探针

仓库自己的 README 写明了判据,而本方案接受它:

这一层的可行性由两条原语决定,而不由它最终承载多少个接口决定: ① 上下文切换;② 页表项。

以及那句最要紧的自述:

⚠️ 门槛尚未通过,而一个架构无法通过它。 判据是同一个接口在第二台、 且确实不同的机器上仍然成立。只有 riscv64 时,抽象没有任何可供其失败的对象 ——围绕一套指令集塑形的接口,总是恰好合于它。

⭐ 所以本方案的第一优先级不是"多写几个接口",而是让门槛可被判定。在第二个 架构存在之前增加接口数量,产出的是"没有任何测试能运行的大量代码",而这正是 这个项目的设计记录最直接警告的失败形态。

3. 两个前提,均为实测

3.1 aarch64-none-elf 在 mcpp 的目标表里没有行

src/toolchain/triple.cppm 的裸机行只有两条:

{ "riscv64-none-elf", "verified", "bare", "llvm@22.1.8", "xim:picolibc-riscv@1.8.12", true },
{ "riscv32-none-elf", "verified", "bare", "llvm@22.1.8", "xim:picolibc-riscv@1.8.12", true },

解法不需要新的 C 库包。 openarch 是纯机制层,不引用任何 C 库符号;它要的 是编译器与指令集,不是 <stdio.h>。因此 aarch64-none-elf 可以作为零 libc 档的一行加入——sysroot 为空,不解析任何 C 库,不添加任何 include 或 library 路径。这一档的能力本身是 2026.8.20.2 才具备的([target.<triple>].sysroot = ""), 而 openarch 恰好是它的第一个真实用例。

一条需要 C 库的 aarch64 裸机行是另一件事,需要 picolibc-aarch64 之类的载荷; 它不在本方案范围内,也不是通过门槛所必需的。

3.2 索引里没有 aarch64 的模拟器

xlings search qemu 只有 xim:qemu-riscv。而实测其载荷的 bin/ 只有两个程序:

qemu-system-riscv32
qemu-system-riscv64

xPack 是按架构分包构建的,不是全量 QEMU。⚠️ "QEMU 默认就支持很多指令集"对源码 构建成立,对这些预编译包不成立。

xPack 发布的 QEMU 只有两个:qemu-arm-xpackqemu-riscv-xpack。前者 v9.2.4-1 的载荷经实测含 qemu-system-aarch64qemu-system-arm,五个宿主目标齐全 (linux x64/arm64、darwin x64/arm64、win32 x64),每个资产带 .sha 侧文件——与 qemu-riscv 描述符所述的收录门槛完全一致。

⇒ 新增 xim:qemu-arm,照 qemu-riscv 的式样:上游 xPack 为 GLOBAL, xlings-res/qemu-arm 双端镜像为 CN。

x86 一档暂缓且有据:qemu-riscv 描述符写明 xPack 是"唯一一个为本索引服务的 全部五个宿主目标、从同一个版本化发布、且带每资产侧文件发布预编译件的上游"。 qemu.org 只出 Windows 安装器,macOS 与 Linux 只有发行版包,都不满足这条线。 与其凑合,不如记为缺口。

4. 接口集合与顺序

顺序由门槛决定,不由完整性决定。

接口 内容 为何在这个位置
I1 openarch.context 上下文的表示、切换、构造 已有 riscv64;门槛的第一条原语,加入 aarch64 后才第一次可被证伪
I2 openarch.pte 页表项的构造与解读 门槛的第二条原语。README 已指出预期的失败点:内存属性在 x86 走 PAT、aarch64 走 MAIR、riscv 直接写进项内——三种机器把同一个概念放在三处
I3 openarch.trap 陷入向量的装载与最小陷入帧 前两条通过之后才有意义:它的形状取决于上下文的形状
I4 openarch.cpu per-CPU 基址、屏障 架构间差异最小,先做说明不了任何问题
I5 openarch.timer 时钟节拍源 同上

本方案实现 I1 与 I2,并把 I3–I5 明确留在门槛之后。这不是保守,而是这个项目 自己写下的判据:先证明抽象成立,再扩展它覆盖的面。

5. 后端的选择方式:cfg 决定指令集,feature 决定接口与可测面

这是本方案里唯一需要论证的设计决定。

指令集不是一个选择。 它由目标 triple 决定,所以后端里带汇编的那部分cfg(arch = ...) 选中。用 feature 强行选入一个与目标不符的后端,产出的是编不过 或链不上的代码,而一个能表达"错"的开关不如没有这个开关。

但每个后端里有一半不是汇编。 页表项的构造是纯位运算:给定物理地址、权限与 内存属性,产出一个 64 位的项。这一半:

  • 在任何宿主上都能编译;
  • 在任何宿主上都能单元测试——判据是产出的位模式,不是它能否运行;
  • 恰恰是最可能在第二个架构上暴露接口错误的一半(见 I2 那一行)。

⇒ feature 的作用是把某个后端与架构无关的那一半强制编入当前构建,使其可在 宿主上被测试:

[features]
default    = []
# 接口子集:一个只要上下文的内核不必为页表付出任何东西
context    = {}
pte        = {}
# 可测面:把该后端的纯计算部分编入当前构建(不含汇编),
# 用于在宿主上对位模式做断言。
probe-riscv64 = {}
probe-aarch64 = {}

⭐ 这样,"同一个接口在两台机器上是否成立"这个问题,有一半可以在没有模拟器的 情况下、在任意宿主上、由普通单元测试回答;另一半由模拟器上的真实切换回答。

6. CI

三个作业,每个断言一件不同的事:

作业 断言
riscv64 探针在 qemu-system-riscv64 上切换、返回,且被调用者保存的寄存器不变
aarch64 同一份探针源码,在 qemu-system-aarch64 上,同样的断言
host 两个后端的页表项编码在宿主上编译并通过位模式断言

⚠️ 模拟器一律来自 xlings 生态(xim:qemu-riscvxim:qemu-arm),不用发行版的 包。openkal-uefi 目前用 apt 装 qemu 与 OVMF,那是在没有对应载荷时的权宜;本层 不复制它,因为 qemu-arm 已经补上。

⚠️ 判据是"同一份探针源码"。两个作业各写一份探针,证明的是两份代码分别能跑; 一份源码在两台机器上跑,证明的才是接口成立。

7. 任务与依赖

E1 引擎:aarch64-none-elf 目标行(零 libc 档)
        └─ 需要:2026.8.20.2 的 sysroot 覆盖能力(已发布)

X1 索引:xim:qemu-arm 描述符 + xlings-res/qemu-arm 双端镜像
        └─ 与 E1 无依赖,可并行

A1 openarch:I1 的 aarch64 后端(context.S + context_init)
        └─ 依赖 E1(否则无法为该目标编译)

A2 openarch:门槛判定 —— 同一份探针在两台机器上运行
        └─ 依赖 A1 与 X1

A3 openarch:I2 页表项接口 + 两个后端的编码器
        └─ 依赖 A2(接口在第二个架构上成立之后,再加第二条原语)

A4 openarch:feature 拆分(接口子集 + 宿主可测面)与宿主单元测试
        └─ 依赖 A3

A5 openarch:CI 三个作业
        └─ 依赖 A2(riscv64/aarch64 两腿)与 A4(host 一腿)

A6 发布 0.2.0 + 索引描述符
        └─ 依赖 A5 全绿

8. 明确不做的事,以及理由

  • 不实现 I3–I5。它们的形状取决于 I1 与 I2 的结论;在门槛通过之前写下它们, 等于用一个未经验证的抽象去生成更多代码。
  • 不加 x86_64 后端。缺的是模拟器载荷这一个环节(见 3.2),而不是代码;等到 有满足收录门槛的上游再补。
  • 不为 aarch64 引入 C 库。openarch 不需要,而引入它会把一条"因为需要所以存在" 的目标行,变成一条"因为对称所以存在"的目标行。
  • 不把 feature 用于选择指令集。理由见第 5 节:那样的开关只能表达错误。

9. 实施结果(2026-08-20 补记)

方案第 4–6 节的安排在实施中被三条实测修改,记录如下,因为每一条都推翻了写 方案时的一个假设。

9.1 门槛已通过

examples/switch 是一份探针源码,为两个目标构建、在两个模拟器里运行,输出 逐字相同,两边退出码均为 0:

main: switching to task
task: arg=42
main: back, witness=7 before=1234
switch ok

9.2 第二个架构改变了接口本身的两处

保存的上下文只含整数状态。 riscv64 不保存 fs0fs11,这在一个后端时 看起来像该后端的做法。AAPCS64 同样把 d8d15 列为被调用者保存,而算术给出 了结论:连它们一起保存需要 168 字节,接口只预留了 128。预留的大小早已假定了 一条契约,而没有任何地方写着它。

页表项不总是自带其含义。 riscv64 把内存类型写进项内(Svpbmt);aarch64 写的是指向 MAIR_EL1 的三位索引,所以同样的位在两台 CPU 上描述不同的内存。 memory_type::device 因此无法仅靠编码满足,本层接管了那个寄存器。

这正是方案第 2 节所引 README 预言的失败点,而它以「接口欠定义」而非「接口 错误」的形式出现。

9.3 ⚠️ 第 5 节的 feature 设计不成立,而理由是两条引擎事实

方案第 5 节提出用 probe-riscv64 / probe-aarch64 把后端与架构无关的那一半 编入宿主构建。实施中测出:

  1. 一个文件只要被任何 feature 命名,就归该 feature 独占。 feature 未激活 时它不被编译,即便 cfg 块显式列出了它;而库构建仍然报告成功,缺失 只在消费方链接时以 undefined reference 出现。
  2. 改为两个编码器都无条件编入之后,外来那个往 riscv64 镜像里放了 450 字节 (nm --size-sort 实测),而整个探针的 .text 约 2000 字节。

头文件 inline 同时满足两侧且不需要开关:目标构建只实例化它调用的那个(实测 两个镜像中的外来符号数均为 0),宿主构建调用两个便得到两个。

本包没有 feature 轴。 指令集由 cfg 决定(能选错的开关不如没有),接口 子集无法用加性 feature 表达,而宿主可测性由 inline 达成。

9.4 探针需要自行声明模拟器

mcpp::xpkg_dir 解析的是工程声明过的包,而不是任何已安装的包。实测:载荷 在磁盘上而工程未声明时它返回空串,build.mcpp 因此不发出 runner,mcpp run 报 「no runner is configured」——而它的构建程序刚刚试图配置一个。

板级支持包在索引描述符的平台 deps 里声明模拟器,所以普通裸机工程从不写 [xlings]。这个探针没有板级包(也不可能有,因为没有板级包同时服务它的两台 机器),所以它自己声明。