diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfd0e0c..8e95294 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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: | @@ -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 diff --git a/README.md b/README.md index 4fe1717..f95d94a 100644 --- a/README.md +++ b/README.md @@ -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_.cpp` files @@ -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 | diff --git a/abi/mcpp.toml b/abi/mcpp.toml index e35ade7..582e178 100644 --- a/abi/mcpp.toml +++ b/abi/mcpp.toml @@ -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"] diff --git a/backends/aarch64/mcpp.toml b/backends/aarch64/mcpp.toml index bc251dd..b48972d 100644 --- a/backends/aarch64/mcpp.toml +++ b/backends/aarch64/mcpp.toml @@ -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"] diff --git a/backends/riscv64/mcpp.toml b/backends/riscv64/mcpp.toml index 4b33603..9cfec3e 100644 --- a/backends/riscv64/mcpp.toml +++ b/backends/riscv64/mcpp.toml @@ -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"] diff --git a/backends/x86_64/mcpp.toml b/backends/x86_64/mcpp.toml index 500ff2e..309cc3e 100644 --- a/backends/x86_64/mcpp.toml +++ b/backends/x86_64/mcpp.toml @@ -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"] diff --git a/examples/clock-study/FINDING.md b/examples/clock-study/FINDING.md new file mode 100644 index 0000000..0170e31 --- /dev/null +++ b/examples/clock-study/FINDING.md @@ -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'` +会把它丢掉 —— 这不是缺陷,是串口输出没有换行。 diff --git a/examples/clock-study/aarch64.ld b/examples/clock-study/aarch64.ld new file mode 100644 index 0000000..3c07764 --- /dev/null +++ b/examples/clock-study/aarch64.ld @@ -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 = .; +} diff --git a/examples/clock-study/build.mcpp b/examples/clock-study/build.mcpp new file mode 100644 index 0000000..78a476d --- /dev/null +++ b/examples/clock-study/build.mcpp @@ -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_.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; +} diff --git a/examples/clock-study/mcpp.toml b/examples/clock-study/mcpp.toml new file mode 100644 index 0000000..2d298c0 --- /dev/null +++ b/examples/clock-study/mcpp.toml @@ -0,0 +1,64 @@ +[package] +name = "clock-study" +version = "0.1.0" + +# ⚠️ NO `[build] target`. The probe's whole claim is that one project serves two +# machines, so the target arrives on the command line — `mcpp run --target +# riscv64-none-elf` and `--target aarch64-none-elf` — and nothing in this file +# names either of them except the two source selections below. +[build] +sources = ["src/main.cpp"] + +# The zero-libc tier on both machines. openarch references no C library symbol, +# and the probe prints by storing to a UART rather than by calling one, so a C +# library would be a payload nothing in this project calls. +[target.riscv64-none-elf] +sysroot = "" + +[target.aarch64-none-elf] +sysroot = "" + +[target.x86_64-none-elf] +sysroot = "" + +[target.'cfg(arch = "riscv64")'.build] +sources = ["src/machine_riscv64.cpp"] + +[target.'cfg(arch = "aarch64")'.build] +sources = ["src/machine_aarch64.cpp"] + +# ⚠️ TWO FILES AND NOT ONE. Every other machine's half is a single `.cpp` whose +# `asm` block carries a four-instruction entry stub. Reaching the state openarch +# assumes on x86_64 — long mode, a 64-bit code segment, page tables — takes its +# own linker section, so it is a file rather than a block. +[target.'cfg(arch = "x86_64")'.build] +sources = ["src/machine_x86_64.cpp", "src/boot_x86_64.S"] + +[dependencies] +# ⭐ ONE LINE, AND IT NAMES NEITHER AN ARCHITECTURE NOR A BACKEND. The `backend` +# feature is on by default and its target-conditional `feature-deps` row selects +# the implementation for whatever `--target` this build was given. That is the +# property the probe exists to demonstrate, so it is stated here rather than +# worked around. +# ⚠️ NO DEPENDENCY ON openarch, AND THAT IS THE METHOD. This study exists to +# decide whether a clock belongs in that layer; building it against the layer +# would answer the question by assuming it. + +# ⚠️ THE EMULATORS ARE DECLARED, AND THE PROBE DOES NOT WORK WITHOUT THIS. +# +# `mcpp::xpkg_dir` resolves a package the PROJECT declares, not any package that +# happens to be installed — measured: with the payload present on disk and no +# declaration here, it returned an empty string and build.mcpp emitted no +# runner, so `mcpp run` reported "no runner is configured" for a project whose +# build program had just tried to configure one. +# +# A board-support package declares its emulator in the index descriptor's +# platform `deps`, which is why an ordinary bare-metal project never writes this +# section. This probe has no board package — it cannot, because no board package +# serves both of its machines — so it declares them itself. +# +# Both, on every build: which one is used is decided by the target, and +# declaring only one would make the manifest depend on the target the same way +# a `runner` key would. +[xlings] +deps = ["qemu-riscv", "qemu-arm", "qemu-x86"] diff --git a/examples/clock-study/riscv64.ld b/examples/clock-study/riscv64.ld new file mode 100644 index 0000000..a14c513 --- /dev/null +++ b/examples/clock-study/riscv64.ld @@ -0,0 +1,10 @@ +/* QEMU riscv `virt` begins executing at 0x80000000 with `-bios none -kernel`. */ +ENTRY(_start) +SECTIONS { + . = 0x80000000; + .text : { *(.text.entry) *(.text*) } + .rodata : { *(.rodata*) } + .data : { *(.data*) } + .bss : { __bss_start = .; *(.bss*) *(COMMON) __bss_end = .; } + . = ALIGN(16); . = . + 0x4000; __stack_top = .; +} diff --git a/examples/clock-study/src/boot_x86_64.S b/examples/clock-study/src/boot_x86_64.S new file mode 100644 index 0000000..83fcdfe --- /dev/null +++ b/examples/clock-study/src/boot_x86_64.S @@ -0,0 +1,185 @@ +/* The x86_64 half of the probe's machine: reaching the state openarch assumes. + * + * ⭐ THIS FILE HAS NO COUNTERPART ON THE OTHER TWO MACHINES, AND ITS EXISTENCE + * IS THE THIRD ARCHITECTURE'S MOST VISIBLE DISAGREEMENT. + * + * riscv64 and aarch64 begin executing 64-bit instructions at reset. A probe for + * them sets a stack pointer and calls C. x86_64 begins in 16-bit real mode, and + * a multiboot loader hands over in 32-bit protected mode with paging off — so + * everything openarch's backend assumes (long mode, a 64-bit code segment, a + * stack) has to be constructed first, in about a hundred instructions that + * belong to no layer of the library. + * + * ⚠️ AND THAT IS WHY THEY ARE HERE RATHER THAN IN THE BACKEND. `backends/x86_64` + * states that it assumes long mode is already entered, for the same reason no + * backend carries a linker script: how a particular image reaches that state is + * a property of how it is LOADED, and a multiboot image, a UEFI application and + * a bzImage arrive by three different routes. A backend that picked one would + * be a bootloader wearing a machine layer's name. + */ + +/* ── The multiboot header ────────────────────────────────────────────────── + * + * Bit 16 is the "a.out kludge": the header carries the load addresses itself and + * the loader never parses the ELF. That is what lets QEMU load this image at + * all — its multiboot loader accepts only a 32-bit ELF, and this one is 64-bit. + * See x86_64.ld for why `SIZEOF_HEADERS` makes the address arithmetic come out. + * + * The checksum is defined as the value that makes the first three fields sum to + * zero in 32-bit arithmetic. */ +.set MB_MAGIC, 0x1BADB002 +.set MB_FLAGS, 0x00010000 /* bit 16: the a.out kludge */ +.set MB_CHECKSUM, -(MB_MAGIC + MB_FLAGS) + + .section .multiboot,"a",@progbits + .align 4 +mb_header: + .long MB_MAGIC + .long MB_FLAGS + .long MB_CHECKSUM + /* The five address fields the kludge requires. With them the loader copies + * bytes and jumps; it never looks at the ELF, which is what lets a 64-bit + * image be loaded by a loader that only parses 32-bit ones. */ + .long mb_header /* header_addr */ + .long __load_start /* load_addr */ + .long __load_end /* load_end_addr */ + .long __bss_end /* bss_end_addr — zeroed by the loader */ + .long _start /* entry_addr */ + +/* ── The 32-bit entry ──────────────────────────────────────────────────────*/ + .code32 + .section .text.entry,"ax",@progbits + .globl _start + .type _start, @function +_start: + cli + movl $boot_stack_top, %esp + + /* ⚠️ ZERO `.bss` BEFORE ANYTHING READS IT, AND THE PAGE TABLES ARE WHY + * THIS IS NOT OPTIONAL HERE. On the other two machines an unzeroed `.bss` + * gives the probe a wrong counter. Here it gives the processor a page table + * full of whatever the previous occupant of that memory left, and the walk + * reads those bytes as present entries pointing at addresses that are not + * page tables. */ + movl $__bss_start, %edi + movl $__bss_end, %ecx + subl %edi, %ecx + xorl %eax, %eax + rep stosb + + /* PML4[0] → PDPT, PDPT[0] → PD. Present, writable. */ + movl $pdpt, %eax + orl $0x03, %eax + movl %eax, pml4 + + movl $pd, %eax + orl $0x03, %eax + movl %eax, pdpt + + /* PD[i] maps a 2 MiB page at i * 2 MiB, identity, for the first gigabyte. + * + * ⚠️ 2 MiB PAGES RATHER THAN 1 GiB ONES. A single PDPT entry with `PS` set + * would map the whole gigabyte in one line of code, and 1 GiB pages are an + * OPTIONAL feature — `cpuid` leaf 0x80000001, `edx` bit 26. Emulators and + * processors that lack it do not fault on the attempt; they interpret the + * `PS` bit as reserved and take a page fault on the first access, from + * inside the code that has just enabled paging. */ + xorl %ecx, %ecx +1: movl %ecx, %eax + shll $21, %eax + orl $0x83, %eax /* present | writable | page-size */ + movl %eax, pd(,%ecx,8) + incl %ecx + cmpl $512, %ecx + jb 1b + + movl $pml4, %eax + movl %eax, %cr3 + + /* CR4.PAE. Long mode requires it; without it the mode switch below is a + * #GP rather than a transition. */ + movl %cr4, %eax + orl $(1 << 5), %eax + movl %eax, %cr4 + + /* EFER: LME to enable long mode, NXE so that bit 63 of a page-table entry + * means "no execute" rather than "reserved". + * + * ⚠️ NXE IS NOT DECORATION HERE. `openarch.pte`'s encoder sets that bit for + * a non-executable mapping. With NXE clear the processor treats it as a + * reserved bit and every access through such an entry is a page fault — + * which is the encoder producing entries that are correct by the manual and + * fatal on the machine. The backend's `install_memory_attributes` sets it + * too, for a kernel that reaches this layer by some other route. */ + movl $0xC0000080, %ecx + rdmsr + orl $((1 << 8) | (1 << 11)), %eax + wrmsr + + /* CR0.PG. Paging on; the processor is in long mode from this instruction, + * but still executing in a 32-bit compatibility segment. */ + movl %cr0, %eax + orl $(1 << 31), %eax + movl %eax, %cr0 + + lgdt gdt64_descriptor + ljmp $0x08, $long_mode_entry + .size _start, . - _start + +/* ── The 64-bit entry ──────────────────────────────────────────────────────*/ + .code64 + .section .text.entry64,"ax",@progbits + .type long_mode_entry, @function +long_mode_entry: + /* ⚠️ THE DATA SEGMENTS STILL HOLD 32-BIT SELECTORS AND MUST BE RELOADED. + * In long mode the processor ignores the base and limit of `ds`, `es` and + * `ss`, which makes it easy to believe they need no attention — but their + * SELECTORS are still checked, and one left pointing at a descriptor the + * new GDT does not define faults on the first stack operation. */ + movw $0x10, %ax + movw %ax, %ds + movw %ax, %es + movw %ax, %ss + movw %ax, %fs + movw %ax, %gs + + movq $__stack_top, %rsp + xorq %rbp, %rbp + + call kmain +1: hlt + jmp 1b + .size long_mode_entry, . - long_mode_entry + +/* ── The descriptor table ────────────────────────────────────────────────── + * + * Three entries: the required null descriptor, a 64-bit code segment, and a + * data segment. + * + * ⚠️ THE `L` BIT — BIT 53, THE `0xA` IN THE SIXTH BYTE — IS WHAT MAKES THE CODE + * SEGMENT 64-BIT, AND `D` MUST BE CLEAR BESIDE IT. A descriptor with both set + * is reserved: the far jump below faults instead of transferring, and the fault + * is delivered through an IDT that does not exist yet, so the machine triple- + * faults and the emulator resets with nothing printed. */ + .section .rodata.gdt,"a",@progbits + .align 8 +gdt64: + .quad 0x0000000000000000 /* null */ + .quad 0x00AF9A000000FFFF /* code: P, DPL0, execute/read, L=1, D=0 */ + .quad 0x00AF92000000FFFF /* data: P, DPL0, read/write */ +gdt64_end: + +gdt64_descriptor: + .word gdt64_end - gdt64 - 1 + .long gdt64 + +/* ── Storage ───────────────────────────────────────────────────────────────*/ + .section .bss + .align 4096 +pml4: .skip 4096 +pdpt: .skip 4096 +pd: .skip 4096 +boot_stack: .skip 4096 +boot_stack_top: + + .section .note.GNU-stack,"",@progbits diff --git a/examples/clock-study/src/machine.h b/examples/clock-study/src/machine.h new file mode 100644 index 0000000..38c35f3 --- /dev/null +++ b/examples/clock-study/src/machine.h @@ -0,0 +1,26 @@ +/* The three things a probe needs from a machine, and nothing else. + * + * ⚠️ THIS FILE EXISTS SO THAT THE PROBE ITSELF DOES NOT. + * + * The property under test is that ONE piece of code compiles and runs on two + * genuinely different instruction sets. That test is worthless if the code is + * written twice, and it is also worthless if the difference is hidden — so the + * difference is put here, in three functions and two implementations of about + * thirty lines each, and everything else is shared. + * + * A console address and a power-off register are BOARD facts. openarch does not + * carry them, and a probe that depended on a board package would be limited to + * the architectures that have one — which today is one architecture, which is + * the situation the gate exists to leave. + */ +#ifndef OPENARCH_PROBE_MACHINE_H +#define OPENARCH_PROBE_MACHINE_H + +namespace machine { +void putc(char c); +void print(const char* s); +void print_int(int v); +[[noreturn]] void poweroff(int code); +} // namespace machine + +#endif diff --git a/examples/clock-study/src/machine_aarch64.cpp b/examples/clock-study/src/machine_aarch64.cpp new file mode 100644 index 0000000..ee68e5d --- /dev/null +++ b/examples/clock-study/src/machine_aarch64.cpp @@ -0,0 +1,49 @@ +// The aarch64 half of the probe's machine: QEMU's `virt`. +#include "machine.h" + +extern "C" int probe_main(); + +namespace { +// The PL011 UART. ⚠️ A different device at a different address from the +// riscv64 machine's 16550A — which is the point: nothing about the probe above +// changes, and everything about reaching a terminal does. +volatile unsigned int* const kUart = reinterpret_cast(0x09000000); +} // namespace + +namespace machine { + +void putc(char c) { *kUart = static_cast(c); } +void print(const char* s) { while (s && *s) putc(*s++); } + +void print_int(int v) { + if (v < 0) { putc('-'); v = -v; } + char d[12]; int n = 0; + do { d[n++] = static_cast('0' + v % 10); v /= 10; } while (v); + while (n-- > 0) putc(d[n]); +} + +// ⚠️ NO SYSCON HERE, AND THE DIFFERENCE IS REAL RATHER THAN AN OMISSION. +// +// QEMU's aarch64 `virt` has no memory-mapped power-off register of the kind +// riscv's syscon provides. Shutdown goes through PSCI, a firmware call — +// `SYSTEM_OFF` is function 0x84000008, reached by `hvc` when the machine +// starts at EL1 under QEMU's default configuration. +[[noreturn]] void poweroff(int code) { + (void)code; // PSCI SYSTEM_OFF carries no status + register unsigned long x0 asm("x0") = 0x84000008UL; + asm volatile("hvc #0" :: "r"(x0) : "memory"); + for (;;) { asm volatile("wfi"); } +} + +} // namespace machine + +extern "C" [[noreturn]] void kmain() { machine::poweroff(probe_main()); } + +asm(".section .text.entry,\"ax\",@progbits\n" + ".globl _start\n" + "_start:\n" + " ldr x30, =__stack_top\n" + " mov sp, x30\n" + " bl kmain\n" + "1:\n" + " b 1b\n"); diff --git a/examples/clock-study/src/machine_riscv64.cpp b/examples/clock-study/src/machine_riscv64.cpp new file mode 100644 index 0000000..b2f2b67 --- /dev/null +++ b/examples/clock-study/src/machine_riscv64.cpp @@ -0,0 +1,47 @@ +// The riscv64 half of the probe's machine: QEMU's `virt`. +#include "machine.h" + +extern "C" int probe_main(); + +namespace { +volatile unsigned char* const kUart = reinterpret_cast(0x10000000); +volatile unsigned int* const kPowerOff = reinterpret_cast(0x100000); +} // namespace + +namespace machine { + +void putc(char c) { *kUart = static_cast(c); } +void print(const char* s) { while (s && *s) putc(*s++); } + +// ⚠️ Shared by both machines in behaviour but written twice, because a third +// translation unit for six lines would be a file whose only purpose is to be +// shared. If a fourth machine arrives, that trade changes. +void print_int(int v) { + if (v < 0) { putc('-'); v = -v; } + char d[12]; int n = 0; + do { d[n++] = static_cast('0' + v % 10); v /= 10; } while (v); + while (n-- > 0) putc(d[n]); +} + +// QEMU's `virt` syscon: 0x5555 is "pass", and the exit status the emulator +// reports is derived from it. Ending on the firmware's terms rather than on a +// timeout is what lets CI read a verdict. +[[noreturn]] void poweroff(int code) { + *kPowerOff = code == 0 ? 0x5555u : 0x3333u; + for (;;) { } +} + +} // namespace machine + +// The entry point. `.text.entry` is placed first by the linker script, because +// execution begins at the load address rather than at whichever function the +// linker happened to put there. +extern "C" [[noreturn]] void kmain() { machine::poweroff(probe_main()); } + +asm(".section .text.entry,\"ax\",@progbits\n" + ".globl _start\n" + "_start:\n" + " la sp, __stack_top\n" + " call kmain\n" + "1:\n" + " j 1b\n"); diff --git a/examples/clock-study/src/machine_x86_64.cpp b/examples/clock-study/src/machine_x86_64.cpp new file mode 100644 index 0000000..2e548b7 --- /dev/null +++ b/examples/clock-study/src/machine_x86_64.cpp @@ -0,0 +1,96 @@ +// The x86_64 half of the probe's machine: QEMU's `pc`/`q35`. +// +// ⭐ THE CONSOLE IS NOT MEMORY, WHICH IS THE THIRD MACHINE'S QUIETEST +// DISAGREEMENT. +// +// Both other halves of this probe reach a terminal by storing to an address: +// riscv's 16550A at 0x10000000, aarch64's PL011 at 0x09000000. x86 has a +// SEPARATE ADDRESS SPACE for devices, reached only by the `in` and `out` +// instructions, and no pointer can name port 0x3F8. A `machine.h` that had +// offered "the console's address" instead of "print a character" would have +// been an interface that two machines could implement and a third could not. +#include "machine.h" + +extern "C" int probe_main(); + +namespace { + +constexpr unsigned short kCom1 = 0x3F8; + +// Line status register bit 5: the transmit holding register is empty. +constexpr unsigned char kThre = 0x20; + +inline void outb(unsigned short port, unsigned char v) noexcept { + asm volatile("outb %0, %1" :: "a"(v), "Nd"(port)); +} + +inline void outw(unsigned short port, unsigned short v) noexcept { + asm volatile("outw %0, %1" :: "a"(v), "Nd"(port)); +} + +inline unsigned char inb(unsigned short port) noexcept { + unsigned char v; + asm volatile("inb %1, %0" : "=a"(v) : "Nd"(port)); + return v; +} + +// ⚠️ A NAMESPACE-SCOPE `bool` AND NOT A FUNCTION-LOCAL `static`. A local static +// with a non-constant initialiser needs `__cxa_guard_acquire`, which lives in +// the C++ runtime this image does not link. The compiler emits the call without +// complaint and the link fails naming a symbol that appears nowhere in the +// source. +bool g_uart_ready = false; + +void uart_init() noexcept { + outb(kCom1 + 1, 0x00); // no interrupts + outb(kCom1 + 3, 0x80); // DLAB: the next two writes are the divisor + outb(kCom1 + 0, 0x01); // 115200 baud + outb(kCom1 + 1, 0x00); + outb(kCom1 + 3, 0x03); // 8 bits, no parity, one stop bit + outb(kCom1 + 2, 0xC7); // enable and clear the FIFOs + outb(kCom1 + 4, 0x03); // data terminal ready, request to send + g_uart_ready = true; +} + +} // namespace + +namespace machine { + +void putc(char c) { + if (!g_uart_ready) uart_init(); + while ((inb(kCom1 + 5) & kThre) == 0) { } + outb(kCom1, static_cast(c)); +} + +void print(const char* s) { while (s && *s) putc(*s++); } + +void print_int(int v) { + if (v < 0) { putc('-'); v = -v; } + char d[12]; int n = 0; + do { d[n++] = static_cast('0' + v % 10); v /= 10; } while (v); + while (n-- > 0) putc(d[n]); +} + +// ⚠️ TWO PORTS, BECAUSE QEMU MOVED THE REGISTER AND BOTH SPELLINGS ARE STILL +// IN THE FIELD. Writing 0x2000 to the ACPI PM1a control block requests soft-off; +// QEMU's `q35` and modern `pc` place that block at 0x604, and versions before +// 2.0 placed it at 0xB004. Neither write faults on a machine that does not +// decode the port, so issuing both costs nothing and removes a dependency on +// which QEMU is installed. +// +// The `hlt` loop is what runs if neither is decoded — a machine that will not +// power itself off, which is a correct outcome rather than a hang to diagnose. +[[noreturn]] void poweroff(int code) { + (void)code; // the ACPI request carries no status + outw(0x604, 0x2000); + outw(0xB004, 0x2000); + for (;;) { asm volatile("hlt"); } +} + +} // namespace machine + +// ⚠️ NO `_start` HERE. The other two machine files carry an entry stub in an +// `asm` block, because on those machines the entry sets a stack pointer and +// calls C. Reaching that state on x86_64 takes a hundred instructions and its +// own linker section, so it lives in `boot_x86_64.S` beside this file. +extern "C" [[noreturn]] void kmain() { machine::poweroff(probe_main()); } diff --git a/examples/clock-study/src/main.cpp b/examples/clock-study/src/main.cpp new file mode 100644 index 0000000..eb32729 --- /dev/null +++ b/examples/clock-study/src/main.cpp @@ -0,0 +1,155 @@ +// A study, not a demonstration. It answers where a clock belongs. +// +// ⭐ THIS FILE IS DELIBERATELY NOT WRITTEN AGAINST AN openarch INTERFACE, AND +// THAT IS THE WHOLE METHOD. +// +// The other four interfaces in this layer were written after a probe showed +// that all the machines could answer the same question. A clock has not been +// shown to have that property, and writing the interface first would decide the +// question by assumption: whatever shape got written would then look inevitable. +// +// So this reads each machine directly, and the SOURCE is the evidence. What it +// costs to get a reading on each machine — how much of it is architectural and +// how much has to be told by a board — is visible here as code rather than +// asserted in a document. +// +// ⚠️ TWO SEPARATE QUESTIONS, AND CONFLATING THEM IS THE MISTAKE THIS STUDY +// EXISTS TO AVOID. +// +// 1. "What time is it" — read a monotonic counter. +// 2. "Interrupt me at time T" — arm a comparator. +// +// They have different answers on the same machine. A `timer` interface that +// bundled them would be as wide as the wider of the two, and would then be +// unimplementable wherever the wider one needs board knowledge. + +#include "machine.h" + +namespace { + +// ── What a reading costs, per machine ─────────────────────────────────────── +// +// Each `read_*` below is annotated with where its numbers come from. That +// annotation is the study's output; the printed values only prove the code ran. + +#if defined(__riscv) + +// ⚠️ TWO SOURCES ON THIS MACHINE, AND THEY DIFFER IN EXACTLY THE WAY THE STUDY +// IS ABOUT. +// +// * `rdtime` reads the `time` CSR. No address, no board knowledge. Whether it +// is available at all depends on the platform wiring it: on a real machine +// it may trap, and the trap handler is expected to emulate it from the +// memory-mapped counter below. +// * `mtime` lives in the CLINT, at an address only the BOARD knows. QEMU's +// `virt` puts it at 0x0200BFF8; a different board puts it elsewhere. +// +// So riscv can answer "what time is it" without board knowledge *if* `rdtime` +// works, and cannot otherwise. The comparator (`mtimecmp`, 0x02004000 on this +// board) has no CSR form at all in M-mode — Sstc adds `stimecmp` for S-mode, +// and this backend runs in M. +inline unsigned long long read_counter_arch() { + unsigned long long v; + asm volatile("rdtime %0" : "=r"(v)); + return v; +} +constexpr unsigned long long kClintMtime = 0x0200BFF8ULL; // BOARD +inline unsigned long long read_counter_board() { + return *reinterpret_cast(kClintMtime); +} +inline unsigned long long frequency_hint() { return 0; } // not architectural +constexpr const char* kArchNote = + "riscv64: rdtime is a CSR (no board knowledge); mtime/mtimecmp are in the " + "CLINT at a board address; no frequency register"; + +#elif defined(__aarch64__) + +// ⭐ THE OPPOSITE SHAPE. The counter AND its frequency are architectural system +// registers. Nothing here needs an address. +// +// ⚠️ The comparator is architectural too (`CNTP_CVAL_EL0`), but the INTERRUPT it +// raises arrives on a PPI whose number is a board fact, and routing it needs an +// interrupt controller this layer does not own. +inline unsigned long long read_counter_arch() { + unsigned long long v; + asm volatile("mrs %0, cntpct_el0" : "=r"(v)); + return v; +} +inline unsigned long long read_counter_board() { return read_counter_arch(); } +inline unsigned long long frequency_hint() { + unsigned long long v; + asm volatile("mrs %0, cntfrq_el0" : "=r"(v)); + return v; +} +constexpr const char* kArchNote = + "aarch64: cntpct_el0 and cntfrq_el0 are architectural registers; the " + "comparator is architectural but its interrupt number is a board fact"; + +#elif defined(__x86_64__) + +// ⚠️ THE THIRD SHAPE AGAIN. `rdtsc` needs no address, but it is a cycle counter +// whose frequency is not architectural: CPUID leaf 0x15 reports it on some +// processors and not others, and on older ones it must be calibrated against +// another timer. +// +// The comparator is worse: this machine has at least four unrelated candidates +// (PIT, HPET, the local APIC timer, TSC-deadline), and WHICH ONE EXISTS is a +// runtime question — HPET is discovered from an ACPI table, the local APIC is +// at an MSR-provided address, TSC-deadline is a CPUID bit. +inline unsigned long long read_counter_arch() { + unsigned lo, hi; + asm volatile("rdtsc" : "=a"(lo), "=d"(hi)); + return (static_cast(hi) << 32) | lo; +} +inline unsigned long long read_counter_board() { return read_counter_arch(); } +inline unsigned long long frequency_hint() { + // CPUID leaf 0x15: EAX = denominator, EBX = numerator, ECX = core crystal + // Hz. A zero anywhere means the processor declines to say. + unsigned a, b, c, d; + asm volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x15), "c"(0)); + if (a == 0 || b == 0 || c == 0) return 0; + return static_cast(c) * b / a; +} +constexpr const char* kArchNote = + "x86_64: rdtsc needs no address but its frequency is not architectural " + "(CPUID 0x15 may decline); the comparator is one of at least four, and " + "which exists is a runtime question"; + +#else +# error "the clock study has no reading for this architecture" +#endif + +} // namespace + +extern "C" int probe_main() { + machine::print("clock-study: "); + machine::print(kArchNote); + machine::putc('\n'); + + // ⚠️ TWO READINGS WITH WORK BETWEEN THEM, AND THE WORK MATTERS. Two reads + // back to back can both land in the same tick of a slow counter, and a + // study that accepted `a <= b` would then report success for a counter that + // is stuck. + const auto a = read_counter_arch(); + for (volatile int i = 0; i < 200000; ++i) { } + const auto b = read_counter_arch(); + + machine::print("clock-study: architectural counter "); + if (b > a) machine::print("advances\n"); + else machine::print("DID NOT ADVANCE\n"); + + const auto ba = read_counter_board(); + for (volatile int i = 0; i < 200000; ++i) { } + const auto bb = read_counter_board(); + machine::print("clock-study: board-addressed counter "); + if (bb > ba) machine::print("advances\n"); + else machine::print("DID NOT ADVANCE\n"); + + const auto f = frequency_hint(); + machine::print("clock-study: frequency "); + if (f) { machine::print("is reported as "); machine::print_int(static_cast(f / 1000)); machine::print(" kHz\n"); } + else machine::print("is NOT reported by the architecture\n"); + + machine::print("clock-study done\n"); + return 0; +} diff --git a/examples/clock-study/x86_64.ld b/examples/clock-study/x86_64.ld new file mode 100644 index 0000000..3e195ae --- /dev/null +++ b/examples/clock-study/x86_64.ld @@ -0,0 +1,56 @@ +/* QEMU's x86 machines load a multiboot `-kernel` image at the addresses its + * multiboot header names. + * + * ⚠️ QEMU'S MULTIBOOT LOADER ACCEPTS ONLY A 32-BIT ELF, AND THIS IMAGE IS A + * 64-BIT ONE. Measured, with the image linked the obvious way: + * + * qemu-system-x86_64: Cannot load x86-64 image, give a 32bit one. + * + * There is no way to produce an ELF32 here — the code is x86-64 and an ELF's + * class is a property of the whole file — so the image is loaded through + * multiboot's OTHER route: the "a.out kludge", flag bit 16, under which the + * header carries explicit load addresses and the loader never parses the ELF + * at all. GRUB2 accepts ELF64 and does not need this; QEMU does. + * + * ⭐ `SIZEOF_HEADERS` IS WHAT MAKES THE KLUDGE'S ARITHMETIC COME OUT. + * + * The loader computes the file offset it starts copying from as + * + * header_addr - load_addr + * + * so that difference must equal the multiboot header's ACTUAL offset in the + * file. Starting the image at `0x100000 + SIZEOF_HEADERS` makes the ELF + * headers occupy exactly the bytes between `load_addr` and `header_addr`: + * offset and address stay congruent, the linker emits no padding, and the + * difference is the offset by construction rather than by a number somebody + * has to keep correct. + * + * Linking it the plain way — `. = 0x100000` — put `.multiboot` at file offset + * 0x1000 with address 0x100000, so `load_addr` would have had to be 0xFF000, + * inside the legacy BIOS window where a write is discarded. + */ +ENTRY(_start) +SECTIONS { + __load_start = 0x100000; + . = __load_start + SIZEOF_HEADERS; + + .multiboot : { KEEP(*(.multiboot)) } + .text : { *(.text.entry) *(.text.entry64) *(.text*) } + .rodata : { *(.rodata*) } + .data : { *(.data*) } + __load_end = .; + + /* ⚠️ 4 KiB, BECAUSE THE PAGE TABLES LIVE HERE. The boot stub builds a + * PML4, a PDPT and a PD in `.bss`, and the processor requires each to be + * page-aligned; a table at a lower alignment is not diagnosed, it is walked + * with the low bits of its address read as flags. */ + .bss ALIGN(4096) : { + __bss_start = .; + *(.bss*) + *(COMMON) + . = ALIGN(16); + __bss_end = .; + } + + . = ALIGN(16); . = . + 0x4000; __stack_top = .; +} diff --git a/examples/switch/build.mcpp b/examples/switch/build.mcpp index 4ef7c0d..78a476d 100644 --- a/examples/switch/build.mcpp +++ b/examples/switch/build.mcpp @@ -30,25 +30,25 @@ int main() { // against whichever home owns it, which is not necessarily the home this // build is using. // - // The two packages are different — xPack builds QEMU per target family, so - // `qemu-riscv` carries only the two RISC-V emulators and `qemu-arm` only - // the two Arm ones. There is no single package that runs both machines. - // ⚠️ x86_64 IS ABSENT FROM THIS TABLE, AND THE ABSENCE IS THE ECOSYSTEM'S - // RATHER THAN THIS PROBE'S. xPack publishes QEMU per target family, and the - // index carries `qemu-riscv` and `qemu-arm` because those are the families - // it publishes. There is no `xim:qemu-x86`, so on that target this build - // program configures no runner and `mcpp build` is the whole of what CI can - // do — which is stated here rather than worked around, because a runner - // silently pointing at a host `qemu-system-x86_64` would make the build - // depend on what happens to be installed. - if (arch == "x86_64") { - mcpp::rerun_if_env_changed("MCPP_TARGET_ARCH"); - mcpp::rerun_if_changed("x86_64.ld"); - return 0; - } - - const char* pkg = (arch == "riscv64") ? "qemu-riscv" : "qemu-arm"; - const char* sys = (arch == "riscv64") ? "riscv64" : "aarch64"; + // ⭐ 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. // @@ -79,8 +79,14 @@ int main() { // 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("virt"); + 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 @@ -98,5 +104,6 @@ int main() { 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; } diff --git a/examples/switch/mcpp.toml b/examples/switch/mcpp.toml index 7b5bf7d..36a33c4 100644 --- a/examples/switch/mcpp.toml +++ b/examples/switch/mcpp.toml @@ -59,4 +59,4 @@ openarch = { path = "../.." } # declaring only one would make the manifest depend on the target the same way # a `runner` key would. [xlings] -deps = ["qemu-riscv", "qemu-arm"] +deps = ["qemu-riscv", "qemu-arm", "qemu-x86"] diff --git a/mcpp.toml b/mcpp.toml index e8539f6..bc06864 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -33,7 +33,7 @@ [package] namespace = "mcpplibs" name = "openarch" -version = "0.4.1" +version = "0.5.0" description = "openarch: the architecture-mechanism layer — execution contexts, traps, per-CPU state and address spaces, as one interface over several instruction sets" license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/templates/three-machines/README.md.in b/templates/three-machines/README.md.in index 3a56916..9b69d92 100644 --- a/templates/three-machines/README.md.in +++ b/templates/three-machines/README.md.in @@ -1,14 +1,14 @@ # {{project.name}} ``` -xlings install qemu-riscv qemu-arm -y # once, per machine +xlings install qemu-riscv qemu-arm qemu-x86 -y # once, per machine 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 first line is not optional, and leaving it out fails in a way that -does not name it.** `mcpp.toml` DECLARES those two packages under `[xlings] +does not name it.** `mcpp.toml` DECLARES those three packages under `[xlings] deps`, and a declaration there is not an install trigger — it is what lets `build.mcpp` ask where they landed. Without them installed, every target still builds and `mcpp run` reports @@ -60,12 +60,13 @@ different routes. `riscv64` and `aarch64` run under emulators the package index carries, and `build.mcpp` configures them. -⚠️ **x86_64 builds but does not run from `mcpp run`,** and the manifest says so -rather than working around it. The index has no `xim:qemu-x86` — xPack publishes -QEMU per target family and has no x86 build — and a build program that fell back -to whatever `qemu-system-x86_64` happened to be on the machine would make the -build depend on what is installed. Run it by hand: +All three run under emulators the package index carries, and `build.mcpp` +configures each from the package it was installed into rather than from a bare +name on `PATH`. -``` -qemu-system-x86_64 -machine q35 -nographic -no-reboot -kernel -``` +⚠️ **The machine type is not the same on all three.** `riscv64` and `aarch64` +boot the para-virtual `virt` board; x86_64 has no such board and boots `q35`, a +model of real PC chipset hardware, reaching it through multiboot rather than +through the raw-image path `-kernel` takes on the other two. `build.mcpp` +selects it; this is noted so the difference is legible when reading the +generated command line. diff --git a/templates/three-machines/mcpp.toml.in b/templates/three-machines/mcpp.toml.in index a094b76..6e9d5d3 100644 --- a/templates/three-machines/mcpp.toml.in +++ b/templates/three-machines/mcpp.toml.in @@ -62,4 +62,4 @@ openarch = "{{self.version}}" # declaring only one would make the manifest depend on the target the same way # a `runner` key would. [xlings] -deps = ["qemu-riscv", "qemu-arm"] +deps = ["qemu-riscv", "qemu-arm", "qemu-x86"]