Skip to content

Latest commit

 

History

History
291 lines (199 loc) · 14.4 KB

File metadata and controls

291 lines (199 loc) · 14.4 KB

mcpp 在内核 / 嵌入式 / freestanding 方向的评估

2026-08-21。本文评估 mcpp 及其生态在裸机方向的现状,分七个角度。

0. 本文的证据边界

本文区分三类陈述,并逐条标注:

  • 实测 —— 本次或既往会话中跑出过输出的;
  • 读码 —— 从 src/ 的表与分支读出的,未单独跑验证;
  • 推断 —— 由前两类推出的判断,可能被后续测量推翻。

⚠️ 这个区分不是形式。 本次会话里,「MSVC STL 为什么不能服务裸机目标」被推断了两次 都错,第三次去问机器才对;「载荷是否自足」的第一次测量因为工具崩在 stderr 而什么也 没测到,空输出读起来与「没有越界」完全一样。凡未标注实测的结论,应当按可能有错来读。


1. freestanding:这一层的性质

1.1 零 libc 是一个层,不是一个开关(读码 + 实测)

目标表(src/toolchain/triple.cppm)当前十三行,其中裸机四行:

Triple Tier 目标 C 库
riscv64-none-elf verified xim:picolibc-riscv
riscv32-none-elf verified xim:picolibc-riscv
aarch64-none-elf preview 无 —— 零 libc 层
x86_64-none-elf preview 零 libc 层

后两行的 C 库列为空,而空列的语义与清单里 sysroot = "" 完全相同。由此得到一条 设计性质:

「没有 C 库」与「选择某个 C 库」走同一套机制。 一个工程针对这两个目标时不必 声明任何东西就落在零 libc 层;要 C 库就声明一个,而那与「换一个别的 C 库」是同一个 动作。缺省不是特例。

1.2 链接行是从零构造的,不是从宿主行删减的(读码)

方面 裸机目标上的行为
链接行 -nostdlib -nostartfiles -static,无 crt、无动态链接器、无 C++ 运行时
链接器选择 ld.lld绝对路径寻址,路径由驱动自身目录推出
ISA 参数 -march / -mabi / -mcmodel 来自一个目标一行的表,故 --target <triple> 单独即足够
异常与 RTTI 图中每个翻译单元都关掉,包括依赖的
import std 不可用,且在 configure 期拒绝并给出替代
默认链接方式 静态 —— 没有加载器,所以没有第二种选择

「删减」与「构造」的差别是可观察的。 从宿主链接行删减会漏,而漏掉的那条宿主假设 在链接期才现形,表现为一堆未定义符号。从零构造则要求每一项被显式加入,遗漏在配置期 就是缺失。

⚠️ -fuse-ld=lld 按名字经 PATH 解析,而绝对路径不。 这与 openarch 的构建程序按绝对 路径要模拟器是同一条纪律:一个按名字解析的东西,解析到哪取决于机器上装了什么。

1.3 ISA 表的形状被一次实测证实(实测)

x86_64-none-elf 落地时,该表新增两列(extralldEmulation),因为:

  • clang 的 BareMetal 工具链只覆盖 arm / aarch64 / riscv,x86_64 落到通用 GCC 工具链, 链接经宿主 g++ —— 六个裸 triple × 四个 flag 实测确认;
  • -mno-red-zone 在中断可以踩栈的机器上不是偏好而是目标的属性

⭐ 这两条都不是「多加一个 case」,而是表的列不够。一个抽象是否成立,看的是新增一行 要不要改表的形状。

1.4 诊断在 configure 期(读码)

import std 在裸机目标上给出的是解释加替代方案,而不是链接期的符号墙。缺失 runner 给 出的是「构建成功而执行不可能」之间的那条缝,并附可粘贴的 runner 键。

⚠️ 可粘贴那行里的版本号也是承诺,复查时必须与索引对齐。


2. 内核开发

2.1 已经具备的(实测)

生态里有一条从「机器机制」到「可引导镜像」的完整链:

版本 提供什么
机器机制 openarch 0.5.1 执行上下文、陷入、地址空间、per-CPU、屏障
语言子集 std-freestanding 及其三个分配半 0.5.0 / 0.2.0 / 0.1.0 不需要 OS 的那部分标准库
内核抽象 openkal 0.5.2 内核抽象层
固件侧 openkal-opensbi / openkal-uefi 0.1.0 SBI 与 UEFI 两条启动路径
板级 riscv-virt-rt 0.5.1 qemu virt 板的运行时
模拟器 qemu-riscv / qemu-arm / qemu-x86 9.2.4-1 三个目标族各一个

实测:mcpp new mykernel --template openarch 生成的工程在 riscv64 / aarch64 / x86_64 三台机器上 mcpp run 全部打印 switch ok

2.2 ⭐ 三台机器是这条链最强的一条证据(实测)

openarch 的判据是一份探针源码在三台机器上输出逐字节相同。这条判据的力量来自第三 台机器:

riscv64 与 aarch64 都是定长指令、弱内存序的 load/store RISC 机器,一个同时适配两者 的接口,分不清它是对的还是它们像。x86_64 变长指令、TSO(四条屏障里三条不需要任何 指令)、中断机制是 256 项的门表而不是一个基址寄存器。三台都活下来的才是抽象。

第三台机器实测挖出三条另外两台不可能暴露的事实:IA32_PAT 的复位值让一处早期设备映射 被当作可缓存而不报错;pc 在故障与陷阱两类事件里分别指故障地址与下一条指令;NX 是 单级的,所以规则必须由 CR4.SMEP 承担。

2.3 边界是被测量决定的,不是被口味决定的(实测)

时钟归属的调研是最清楚的一例。三台机器都能用一条无地址指令读到单调递增的计数器,只有 一台能说出它走多快(aarch64 的 cntfrq_el0;riscv 架构不报告;x86_64 的 CPUID leaf 0x15 拒绝)。

counter() 属于本层,frequency()set_deadline() 不属于。

先写接口会得到三件一套,其中两件在三台里的两台上无法兑现 —— 而那种接口的失败方式 是「在一台机器上写得出、在另一台上写不出」,只会在第二台机器被加进来时才暴露。

2.4 明确不做的

  • 页表遍历 —— 造一个表项是机制,决定表项放哪是策略,属于内核而不是本层;
  • 单 ISA 的第二个后端 —— 结构已支持(backend-riscv64 命名的是后端而非架构),但 尚未有;riscv 会需要它,因为现有后端陷入 M 态,而 SBI 之下的内核陷入 S 态。

3. 嵌入式开发

3.1 强项

  • riscv32-none-elfriscv64-none-elf 是 verified 级,且索引里有目标 C 库 (xim:picolibc-riscv);
  • 产物集面向烧录:docs/13-baremetal.md 有专节;
  • 板级包模型成立:板级包在索引描述符的平台 deps 里声明模拟器/工具链,而那些 是随包安装的 —— 这是普通裸机工程不会踩到「声明≠安装」的原因。

3.2 ⚠️ 结构性缺口:构建程序没有「成功但有话说」的通道

mcpp: 指令表有 action / cfg / cflag / cxxflag / generated / graph / include / link / protocol / rerun / runner / source,没有 warning。而 mcpp 用 capture_exec 抓构建 程序,只在非零退出时打印抓到的东西。

后果(实测):清单里的 [xlings] deps 是声明不是安装触发器,干净机器上 mcpp::xpkg_dir 返回空 → 构建程序静默地不配 runner → mcpp run 报「没有 runner」并建议 写一个 runner 键 —— 这句话一般情况下对,在这里不对,因为工程有 runner,只是条件性 的。而构建程序无法说出这件事:写一条 std::cerr 提示实测后被删掉,因为它在最需要它 的那些构建上一个字都不打印。留着比不留更糟,因为它看起来像个修复。

⇒ 目前唯一到得了用户的通道是 README。这是一个真实的引擎缺口,而不是文档问题。

3.3 ⚠️ 裸机四行的覆盖是不对称的

aarch64-none-elfx86_64-none-elf 的 C 库列为空,一半是设计(零 libc 层),一半是 索引里没有这两个架构的 picolibc 构建。想在这两台机器上用 C 库的工程,目前要自己解决 目标 C 库。


4. 跨平台

4.1 目标侧(读码)

十三个目标,六个 verified(含两个裸机),三个 planned,两个裸机 preview。索引服务五个 宿主(linux x64/arm64、darwin x64/arm64、win32 x64)。

4.2 ⭐ 宿主侧:一条本次实测得到的一般结论

宿主的 clang 是拿什么标准库编出来的,不参与一次交叉编译。

Windows 的 LLVM 载荷把 clang 编译在 MSVC 标准库上、不带 libc++。此前这被记为「Windows 不支持 freestanding」。实测(Windows runner,载荷自己的 clang):

── MSVC STL, for a bare-metal target ──
t.cpp:1:10: fatal error: 'array' file not found
── MSVC STL, for its own target, as a control ──
(无诊断)

那些头从来没被读到。 clang 只在目标是 MSVC 目标时才搜索 MSVC 标准库。 ⇒「用 MSVC STL 服务裸机目标」不是一件会失败的事,是一件驱动从不尝试的事。

4.3 ⭐ 「freestanding 与标准库实现无关」是错的

同样 40 个 C++23/26 freestanding 强制头,libstdc++ 16.1.0 + -D_GLIBCXX_HOSTED=0, 只改 target:

x86_64-linux-gnu   40 / 40 编过
riscv64-none-elf    0 / 40,全部倒在
    bits/c++config.h -> bits/os_defines.h -> 'features.h' not found

判据不是「哪个实现」,是「那份实现有没有为这个目标 configure 过」。 libc++ 的逐 目标配置是一个扁平宏文件(__config_site),可以合成;libstdc++ 的是 configure 输出, 会把宿主 C 库拽进来。

解法是目标的 C++ 标准库应当像目标的 C 库一样解析:载荷带就用载荷的,不带就用一个 包(xim:libcxx-headers)。Windows 那一行因此从「断言缺口」变成真正构建。


5. 方便性

5.1 强项(实测)

动作 结果
mcpp new x --template openarch 三个目标的工程,含链接脚本与板级源
mcpp run --target <triple> 板级包或构建程序供 runner 时直接在模拟器里引导
mcpp build --target <triple> ISA 参数、链接行、目标 C 库全部由 triple 推出
失败 命名诊断 + 可粘贴的修法,而不是原始工具输出

一个无依赖工程仍能构建(Size norunner text 12),这是「ISA 表一行即足够」的证据。

5.2 ⚠️ 一次性安装步骤不可发现

见 §3.2。用户第一次在干净机器上跑模板,会看到一条一般情况下正确、在此处不正确的建议。 这是当前方便性上最大的一处折损,且成因在引擎而不在文档


6. 兼容性

  • 索引是数据,mcpp 是程序 —— 发布数据不得让既有程序失效(min_mcpp 下限必须降级 而不是变砖);
  • 跨档位 BMI 硬拒,因此没有「看起来能用」的缓存;
  • 已发布版本保留在索引表里 —— 一个已发布的版本可能已被 pin,即使它有缺陷,也只在描述 符里记录缺陷而不删行(std-freestanding 0.3.0/0.3.1 即如此);
  • ⚠️ 模板的 CI 不可能测试尚未发布的模板 —— mcpp new --template 从索引解析、不接受 路径。解法是 CI 手工渲染模板文件,测的是模板的内容;能否被抓取是 mcpp 自己的 测试范围。

7. 生态

7.1 规模(实测)

索引 120 个包;裸机方向十个仓,全部有 CI 覆盖且 main 全绿。引擎自身 274 个 e2e,其中六个 直接针对裸机与 freestanding。

7.2 分发链的纪律

  • 双端镜像(GitHub + GitCode),每个资产带 .sha256 侧文件;
  • ⚠️ 判定上传成功只能靠回探下载并逐字节核验。本次会话该判据两次救场:一次抓住被超时 杀掉的截断 zip(算出的 hash 会让每个 Windows 用户校验失败),一次抓住 gtc 报告 uploaded 而对象 404;
  • 模拟器不再依赖宿主发行版:qemu-x86 由生态自建,五宿主载荷自足(实测:linux 载荷解析 15 个对象,12 个在载荷内,只有 libc/libm 跨界)。

7.3 ⚠️ 生态最反复出错的一处:判据的放置

本谱系内三次同源事故:

  1. 模板断言放在不装模拟器的作业里 —— 物理上只可能断言 mcpp build,一个「三台机器 都编得过、一台都跑不起来」的工程通过了;
  2. 修好后放在 gate一行上,而每行只装自己那台机器的模拟器 —— 它能支持的主张只有 「模板在 riscv64 上能跑」,于是模板自己的 build.mcpp.in 漏改了 x86_64 分支而无人察觉;
  3. 载荷闭包检查写成 ldd … 2>/dev/null | grep -v <载荷>,而 ldd 崩在 stderr —— 空输出 与「没有越界」不可区分

共同形状:断言必须放在能观察到所要主张之现象的环境里,而矩阵的每一行是一个独立的 观察环境。 前两次把这条规则执行到了作业一级,没执行到行一级。

⚠️ 派生规则:一处逻辑存在两份拷贝时,改一份必漏另一份。 修法不是更仔细,是让两份 逐字节相同并用 diff 断言。


8. 综合判断

8.1 这套东西现在适合什么

  • 教学与研究性内核:三 ISA 同源探针 + 一条命令引导,是当前最顺的路径;
  • RISC-V 嵌入式:两行 verified、目标 C 库在索引里、板级包与模拟器齐备;
  • 需要「同一份源码跑在几台不同机器上」作为判据的工作 —— 这正是 openarch 的判据本身。

8.2 现在还不适合什么

  • 产品级嵌入式的完整 BSP 生态 —— 板级包目前只有 qemu virt 一块板;
  • aarch64 / x86_64 上需要 C 库的裸机工程 —— 索引里没有这两个架构的 picolibc;
  • 依赖动态加载的裸机场景 —— 共享库在裸机目标上被命名诊断拒绝(正确,但是个边界)。

8.3 优先级建议

优先级 事项 理由
给构建程序一条 warning 通道 §3.2 的缺口是当前方便性上最大的折损,且成因在引擎;它让「一次性安装」这类信息物理上到不了用户
补 aarch64 / x86_64 的目标 C 库 让裸机四行的能力对称,零 libc 才真正成为「选择」而不是「只有这一种」
第二块板 板级包模型只被一块板验证过,而一个只服务过一块板的抽象与 openarch 的两台 RISC 机器同形 —— 分不清对还是像
把「判据放在能观察的环境里」做成可检查的东西 三次同源事故说明这条规则靠人执行不可靠
单 ISA 第二后端(riscv S 态) 结构已支持,需求尚未出现