Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 22 additions & 40 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,18 @@ jobs:
fail-fast: false
matrix:
include:
- { arch: riscv64, triple: riscv64-none-elf, qemu: 'xim:qemu-riscv', apt: '' }
- { arch: aarch64, triple: aarch64-none-elf, qemu: 'xim:qemu-arm', apt: '' }
# ⚠️ THE THIRD ROW'S EMULATOR COMES FROM apt, AND THAT IS A GAP IN THE
# ECOSYSTEM RATHER THAN A PREFERENCE.
- { arch: riscv64, triple: riscv64-none-elf, qemu: 'xim:qemu-riscv' }
- { arch: aarch64, triple: aarch64-none-elf, qemu: 'xim:qemu-arm' }
# ⭐ ALL THREE ROWS NAME AN xlings PACKAGE FROM 0.5.0, AND THE THIRD
# ONE TOOK A NEW PACKAGE TO GET THERE.
#
# The other two emulators are xlings packages because xPack publishes
# QEMU per target family and the index carries what it publishes.
# xPack has no x86 build, so there is no `xim:qemu-x86` to install —
# see .agents/docs/2026-08-21-freestanding-outstanding-four.md §4 in
# the engine repository, where building one is staged work.
#
# Naming apt here rather than quietly relying on whatever is on the
# runner keeps the difference visible: this row is the one whose
# emulator the ecosystem does not yet own.
- { arch: x86_64, triple: x86_64-none-elf, qemu: '', apt: 'qemu-system-x86' }
# xPack publishes QEMU per target family and has no x86 build, so
# until now this row installed `qemu-system-x86` from apt and ran the
# emulator by hand. That made it the one row whose result depended on
# what Ubuntu happened to ship. `mcpplibs/qemu-x86` builds
# `qemu-system-x86_64` from source for five hosts, `xim:qemu-x86`
# carries it, and the row is now the same shape as the other two.
- { arch: x86_64, triple: x86_64-none-elf, qemu: 'xim:qemu-x86' }
env:
MCPP_VERSION: 2026.8.21.1
XLINGS_VERSION: v2026.8.17.2
Expand Down Expand Up @@ -110,19 +107,13 @@ jobs:
# runner through mcpp's own — an emulator present in only one of them
# answers "not installed" from the other.
#
# The two packages are different builds: xPack compiles QEMU per target
# family, so `qemu-riscv` carries only the two RISC-V emulators and
# `qemu-arm` only the two Arm ones. Measured; no single package runs both.
# The three packages are different builds, one per target family:
# `qemu-riscv` carries the RISC-V emulators, `qemu-arm` the Arm ones and
# `qemu-x86` the x86_64 one. Measured; no single package runs all three.
- name: Install the emulator
run: |
if [ -n "${{ matrix.qemu }}" ]; then
xlings install ${{ matrix.qemu }} -y
XLINGS_HOME="$HOME/.mcpp/registry" xlings install ${{ matrix.qemu }} -y
else
sudo apt-get update -qq
sudo apt-get install -y -qq ${{ matrix.apt }}
qemu-system-x86_64 --version | head -1
fi
xlings install ${{ matrix.qemu }} -y
XLINGS_HOME="$HOME/.mcpp/registry" xlings install ${{ matrix.qemu }} -y

- name: The layer builds for ${{ matrix.arch }}
run: |
Expand All @@ -148,21 +139,12 @@ jobs:
working-directory: examples/switch
run: |
set -euo pipefail
if [ -n "${{ matrix.qemu }}" ]; then
mcpp run --target ${{ matrix.triple }} 2>&1 | tee run.log
else
# ⚠️ `mcpp build` AND THEN qemu BY HAND, BECAUSE `build.mcpp`
# CONFIGURES NO RUNNER FOR THIS TARGET. It asks mcpp where
# `xim:qemu-x86` landed and there is no such package; a build
# program that fell back to a bare `qemu-system-x86_64` would make
# the build depend on what happens to be installed on the machine,
# which is the thing the other two rows do not do.
mcpp build --target ${{ matrix.triple }}
IMG=$(find target/${{ matrix.triple }} -type f -name switch | head -1)
test -n "$IMG"
timeout -k 5 60 qemu-system-x86_64 -machine q35 -nographic -no-reboot \
-kernel "$IMG" 2>&1 | tee run.log
fi
# ⭐ ONE PATH FOR ALL THREE MACHINES. Until 0.5.0 this step branched:
# two rows went through `mcpp run` and x86_64 built and then invoked
# qemu by hand, because `build.mcpp` could configure no runner for a
# package that did not exist. The branch is gone, and with it the
# asymmetry that made the third machine the least-tested one.
mcpp run --target ${{ matrix.triple }} 2>&1 | tee run.log
grep -q "task: arg=42" run.log
grep -q "witness=7 before=1234" run.log
grep -q "switch ok" run.log
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ mcpp new mykernel --template openarch
cd mykernel
mcpp run --target riscv64-none-elf
mcpp run --target aarch64-none-elf
mcpp build --target x86_64-none-elf
mcpp run --target x86_64-none-elf
```

The template is the probe: one `src/main.cpp`, three `machine_<arch>.cpp` files
Expand Down Expand Up @@ -317,7 +317,6 @@ loop that decides whether the layer is viable.

| | Status |
|---|---|
| Timer ticks | Not started. Unlike the four interfaces here, a tick is not obviously mechanism: riscv's `mtimecmp` is a memory-mapped comparator whose address the *board* decides, aarch64's is an architectural system register, and x86_64 has at least three unrelated sources. An interface over those may be a machine layer's business or a board package's, and the question is worth answering before the code is written |
| `xim:qemu-x86` | The x86_64 row's emulator is not an ecosystem package. xPack publishes QEMU per target family and has no x86 build, so CI installs it with apt and says so. Building one for the five host targets the index serves is staged work |
| Timer ticks | **Answered, not implemented.** `examples/clock-study` reads a counter on all three machines directly and `FINDING.md` records the result: all three provide a monotonic counter with one address-free instruction, and only aarch64 reports how fast it runs. So `counter()` belongs here and `frequency()` and `set_deadline()` do not — the interface is narrower than the one that would have been written first |
| Page-table **walking** | Out of scope. Building an entry is mechanism; deciding where entries go is policy, and belongs to the kernel |
| A second backend for one ISA | The arrangement now supports it — `backend-riscv64` names a backend rather than an architecture — and riscv will want it: this backend traps into M-mode, and a kernel under SBI traps into S-mode |
2 changes: 1 addition & 1 deletion abi/mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
[package]
namespace = "mcpplibs"
name = "openarch-abi"
version = "0.4.1"
version = "0.5.0"
description = "openarch's C ABI: the contract between the interface and an instruction set's backend"
license = "Apache-2.0"
authors = ["mcpplibs"]
Expand Down
2 changes: 1 addition & 1 deletion backends/aarch64/mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
[package]
namespace = "mcpplibs"
name = "openarch-aarch64"
version = "0.4.1"
version = "0.5.0"
description = "openarch's aarch64 backend: the instructions behind the ABI"
license = "Apache-2.0"
authors = ["mcpplibs"]
Expand Down
2 changes: 1 addition & 1 deletion backends/riscv64/mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
[package]
namespace = "mcpplibs"
name = "openarch-riscv64"
version = "0.4.1"
version = "0.5.0"
description = "openarch's riscv64 backend: the instructions behind the ABI"
license = "Apache-2.0"
authors = ["mcpplibs"]
Expand Down
2 changes: 1 addition & 1 deletion backends/x86_64/mcpp.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
[package]
namespace = "mcpplibs"
name = "openarch-x86-64"
version = "0.4.1"
version = "0.5.0"
description = "openarch's x86_64 backend: the instructions behind the ABI"
license = "Apache-2.0"
authors = ["mcpplibs"]
Expand Down
76 changes: 76 additions & 0 deletions examples/clock-study/FINDING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# 时钟归属:调研结论

2026-08-21。三台机器各跑一次 `examples/clock-study`,结论由输出得出,而不是由偏好。

## 测量

| | 计数器读数(无板级知识) | 频率 |
|---|---|---|
| riscv64 | ✓ `rdtime`(`time` CSR) | ✗ **架构不报告** |
| aarch64 | ✓ `cntpct_el0` | ✓ **62500 kHz**(`cntfrq_el0`) |
| x86_64 | ✓ `rdtsc` | ✗ **不报告**(CPUID leaf 0x15 拒绝) |

三条腿都打印了 `architectural counter advances`,即两次读数之间确有推进 —— 而两次读数
之间**插入了工作**,否则一个卡住的计数器也会满足 `a <= b`。

## 结论

⭐ **三台机器都能在没有板级知识的前提下读到一个单调递增的计数器;只有一台能说出它走多
快。** 这条不对称是整份调研的产出。

由此,把「时钟」当作一件事是错的,它至少是三件:

| | 归属 | 理由 |
|---|---|---|
| `now()` —— 读一个原始单调计数器 | **属于 openarch** | 三台机器各用一条指令、无地址地提供它。这正是本层其余四个接口成立的那种性质 |
| `frequency()` —— 那个计数器走多快 | **不属于** | 三台里两台不报告。由本层去校准需要第二个计时源,而那个计时源在 riscv 上是板级的、在 x86_64 上是四选一的 |
| `set_deadline()` —— 到点打断我 | **不属于** | riscv 的 `mtimecmp` 在**板级地址**上(qemu virt 为 0x02004000);aarch64 的比较器虽是架构寄存器,但它引发的中断落在一个**板级 PPI 号**上,路由它需要一个本层不拥有的中断控制器;x86_64 有至少四个候选(PIT / HPET / local APIC / TSC-deadline),而**哪一个存在是运行时问题** |

## 被否掉的候选,以及为什么

**候选 A:时钟整体留在 openarch,由「本层拥有那个寄存器」解决**,与
`openarch.pte` 接管 `MAIR_EL1` 同法。

⚠️ **否掉。** 那次接管成立,是因为 `MAIR_EL1` 是一个**架构寄存器**,本层写它不需要知道
任何板级事实 —— 它只是把一个「两台机器上意思必须相同」的承诺兑现在唯一能兑现的地方。
时钟的比较器不是这样:riscv 的在板级地址上,x86_64 的连存在性都要运行时问。接管它意味
着 openarch 的接口第一次带上板级形状(要么收 CLINT 基址与中断号作参数,要么内嵌一张板子
表),而现有四个接口没有一个需要知道板子。**代价不是多写几行,是这一层的性质变了。**

**候选 B:时钟整体归板级包。**

⚠️ **也否掉,但只否掉一半。** 它对 `set_deadline` 与 `frequency` 是对的,对 `now()` 是
过度的:三台机器都用一条无地址指令提供它,把它推给板级包意味着每块板子重写同一条指令,
而那正是本层存在的理由。

⇒ **采纳的是切开:`now()` 进 openarch,另外两件留给板级包。**

## 这条结论对接口的影响

若日后实现,`openarch.clock` 的表面应当只有:

```cpp
namespace arch {
// 一个原始的、单调递增的计数器读数。
//
// ⚠️ 单位是「计数」而不是纳秒,而这是有意的:三台机器里只有一台报告频率,所以本层
// 无法把计数换算成时间。一个返回纳秒的接口在另外两台机器上只能猜。
unsigned long long counter() noexcept;
}
```

⭐ **接口比原先设想的窄,而这正是调研的价值。** 先写接口会得到 `now()` + `frequency()`
+ `set_deadline()` 三件一套,其中两件在三台机器里的两台上无法兑现 —— 而那种接口的失败
方式是「在一台机器上写得出、在另一台上写不出」,它只会在第二台机器被加进来时才暴露。

## 复现

```
cd examples/clock-study
mcpp run --target riscv64-none-elf
mcpp run --target aarch64-none-elf
mcpp run --target x86_64-none-elf
```

⚠️ x86_64 的第一行会与 SeaBIOS 的 `Booting from ROM..` 共用一行,`grep '^clock-study'`
会把它丢掉 —— 这不是缺陷,是串口输出没有换行。
10 changes: 10 additions & 0 deletions examples/clock-study/aarch64.ld
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* QEMU aarch64 `virt` places a `-kernel` image at 0x40000000. */
ENTRY(_start)
SECTIONS {
. = 0x40000000;
.text : { *(.text.entry) *(.text*) }
.rodata : { *(.rodata*) }
.data : { *(.data*) }
.bss : { __bss_start = .; *(.bss*) *(COMMON) __bss_end = .; }
. = ALIGN(16); . = . + 0x4000; __stack_top = .;
}
109 changes: 109 additions & 0 deletions examples/clock-study/build.mcpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import mcpp;
import std;

// The two things this project cannot ask a board package for, because no board
// package serves both of its machines: where the image is loaded, and which
// emulator boots it.
//
// ⚠️ BOTH ARE SELECTED BY THE TARGET'S ARCHITECTURE RATHER THAN WRITTEN INTO
// THE MANIFEST. The probe's whole claim is that one project serves two
// machines; a manifest key would have to be edited between them, which would
// make that claim false in exactly the way the probe exists to disprove.
int main() {
const std::string arch = mcpp::target_arch() ? mcpp::target_arch() : "";

// The memory map. A relative path resolves against the PACKAGE root, which
// is this directory.
if (arch == "riscv64") mcpp::link_script("riscv64.ld");
else if (arch == "aarch64") mcpp::link_script("aarch64.ld");
else if (arch == "x86_64") mcpp::link_script("x86_64.ld");
else {
std::cerr << "openarch probe: no memory map for arch '" << arch
<< "'. The probe runs on the two machines openarch has "
"backends for; adding a third means adding a linker "
"script beside this file and a machine_<arch>.cpp.\n";
return 1;
}

// The emulator. ⚠️ Asked for by absolute path rather than named: a bare
// `qemu-system-riscv64` resolves through PATH to a shim that dispatches
// against whichever home owns it, which is not necessarily the home this
// build is using.
//
// ⭐ THREE PACKAGES, ONE PER MACHINE, AND THAT IS THE ECOSYSTEM'S SHAPE
// RATHER THAN THIS PROBE'S. QEMU is published per target family, so
// `qemu-riscv` carries the RISC-V emulators, `qemu-arm` the Arm ones and
// `qemu-x86` the x86_64 one. No single package runs all three machines,
// which is why this table exists at all.
//
// ⚠️ x86_64 WAS AN EXCEPTION HERE UNTIL 0.5.0, and the exception was the
// absence of a package rather than a property of the machine: with no
// `xim:qemu-x86` this program configured no runner, CI could assert only
// `mcpp build`, and the third machine was the one machine whose findings
// could not be reproduced by running it. `mcpplibs/qemu-x86` builds that
// emulator from source for five hosts and the exception is gone — the row
// below is now the same shape as the other two.
const char* pkg = (arch == "riscv64") ? "qemu-riscv"
: (arch == "aarch64") ? "qemu-arm"
: "qemu-x86";
const char* sys = (arch == "riscv64") ? "riscv64"
: (arch == "aarch64") ? "aarch64"
: "x86_64";
// ⚠️ DECLARED IS NOT INSTALLED, AND THE DIFFERENCE IS INVISIBLE UNTIL
// SOMEBODY BUILDS THIS ON A MACHINE THAT HAS NOT ALREADY GOT THE EMULATOR.
//
// The manifest's `[xlings] deps` names the emulator packages, and
// `mcpp::xpkg_dir` answers "where did that package land" — it installs
// nothing, and a manifest's `[xlings] deps` is not an install trigger. On a
// clean registry it returns empty, the branch below configures no runner,
// and `mcpp run` reports
//
// error: no runner is configured for 'riscv64-none-elf'
//
// with advice about writing a `runner` key — true in general, and not the
// cause here. Measured against a freshly unpacked mcpp: the project builds
// for all three machines and runs on none of them.
//
// An ordinary bare-metal project never meets this, because a BOARD package
// declares its emulator in the index descriptor's platform `deps`, and
// those ARE installed with the package. This project has no board package —
// none serves three machines — so it declares them itself.
//
// ⚠️ AND IT CANNOT WARN. A build program's output reaches the user only
// when it FAILS: mcpp captures the process and prints what it captured
// solely on a non-zero exit. A `std::cerr` note here was written, measured,
// and removed — it printed nothing on the very builds that needed it, which
// is worse than no note at all because it looks like a fix. Failing instead
// would be wrong too: `mcpp build` has no need of an emulator.
//
// So the README carries it, and this comment records why it has to.
if (const char* dir = mcpp::xpkg_dir("xim", pkg); dir && *dir) {
mcpp::runner(std::format("{}/bin/qemu-system-{}", dir, sys).c_str());
// ⚠️ THE MACHINE TYPE IS NOT UNIVERSAL. `virt` is the para-virtual
// board the two RISC machines use; x86_64 has no such board and boots
// `q35`, a model of real PC chipset hardware. The image reaches it
// through multiboot rather than through `-kernel`'s raw-image path,
// which is why the linker script for that machine carries an a.out
// kludge the other two do not need.
mcpp::runner("-machine");
mcpp::runner(arch == "x86_64" ? "q35" : "virt");
// ⚠️ aarch64's `virt` has no default CPU that implements the features
// this image needs; riscv's does. Naming one on the machine that
// requires it rather than on both, so that the argument list says which
// machine needed it.
if (arch == "aarch64") { mcpp::runner("-cpu"); mcpp::runner("cortex-a53"); }
mcpp::runner("-nographic");
mcpp::runner("-no-reboot");
// riscv's `virt` loads OpenSBI as firmware unless told otherwise, and
// this image is the whole program. aarch64's `virt` needs no such
// instruction: with `-kernel` and no `-bios` it runs the image directly.
if (arch == "riscv64") { mcpp::runner("-bios"); mcpp::runner("none"); }
mcpp::runner("-kernel");
}

mcpp::rerun_if_env_changed("MCPP_TARGET_ARCH");
mcpp::rerun_if_changed("riscv64.ld");
mcpp::rerun_if_changed("aarch64.ld");
mcpp::rerun_if_changed("x86_64.ld");
return 0;
}
Loading
Loading