From 289ba6b8d184847a8ea0d3d91a899aa4fc9b0ad3 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:17:19 +0800 Subject: [PATCH 1/8] feat(freestanding): Cortex-M as seven target rows, and dead-section elimination (2026.9.4.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bare-metal target table grows from four rows to eleven. M-profile is seven rows rather than one: an object built for `thumbv7em` uses instructions a Cortex-M0 does not have, so the two spellings produce incompatible objects rather than expressing a preference. The table exists so that `--target ` alone suffices to produce a correct object file, and a single `arm-none-eabi` row plus an `-mcpu` each project remembered would move a correctness decision out of the table and into every manifest. ⚠️ The float ABI does not settle whether the FPU is used. clang derives `-mfloat-abi` from the `eabi`/`eabihf` suffix without help, but that governs how floating-point values cross a function boundary, not what the compiler may emit inside one — and `thumbv7em` implies FPv4-SP. Measured on llvm 22.1.8: under the soft-float ABI clang still emits `vmul.f32` for a float multiply, which faults at run time on a Cortex-M4 with no FPU after a clean compile and a clean link. Every soft-float row therefore carries `-mfpu=none`, including the rows whose architecture has no FPU at all — a row states the property it guarantees rather than inheriting it from a default that is free to change. The uniform form was not the first draft: the flag was initially applied only where a non-zero instruction count had been observed, which made the table record a measurement rather than a guarantee, and the unit test quantified over the rows caught it. Freestanding builds now compile with `-ffunction-sections -fdata-sections` and link with `--gc-sections`, on both the driver path and the direct `ld.lld` path. Both halves belong to the engine because a dependency's translation units must carry them and a project cannot reach those. The flags became necessary rather than economical when a C library began arriving from the dependency graph: a dependency's object files enter a link unconditionally, unlike an archive member pulled only while its symbol is undefined, and a Cortex-M part has kilobytes where the existing bare-metal rows had megabytes. ⚠️ A linker script becomes load-bearing in a new way. An interrupt vector table is referenced by nothing — the hardware reads it by address — so `--gc-sections` collects it, and a board's script must say `KEEP(*(.vectors))`. The tier column records what was run. Four rows booted under `xim:qemu-arm@9.2.4-1` and printed over semihosting: thumbv6m on `microbit`, thumbv7m on `mps2-an385`, thumbv7em-eabihf on `mps2-an386`, thumbv8m.main-eabi on `mps2-an505`. The remaining three build and link and are marked `preview`. `lldEmulation` is empty on every M-profile row: clang has a BareMetal toolchain for arm, so these triples reach `ld.lld` through the driver as the RISC-V and aarch64 rows do, and the x86_64 row's workaround does not recur. `mcmodel` is empty because 32-bit ARM has no such axis. Coverage: * `tests/e2e/332_cortex_m_builds_and_boots.sh` boots the four verified rows, asserts a function nothing calls is absent while the vector table survives, and measures the float ABI from both sides — the hard row's FPU instruction count and the soft row's link failure naming `__aeabi_fmul`, which states that the multiply did not become an FPU instruction. Both guards were confirmed to fail independently against a reverted engine. * Three unit tests state the rules the e2e cannot: every soft row disables the FPU and no hard row does (with both halves counted, so neither is vacuous), M-profile rows need no code model and no direct lld driving, and every freestanding row compiles with per-function sections. * The script declares `# requires: qemu-arm`, which no sharded runner has, so it is invoked directly from the `baremetal` job where its PASS line and a count of four booted rows can both be demanded. Also backfills two claims in docs/13 that 2026.8.28.2 had already overturned: exceptions, RTTI and `import std` are available on a freestanding target when a package provides `hosted-standard-library`. The document had continued to state them as unconditional limitations, which would lead a reader to abandon a capability that already works. --- .github/workflows/ci-linux-e2e.yml | 29 ++- CHANGELOG.md | 30 ++++ docs/13-baremetal.md | 65 ++++++- docs/zh/13-baremetal.md | 58 +++++- mcpp.toml | 2 +- modules/toolchain-model/src/triple.cppm | 46 +++++ modules/versioning/src/version.cppm | 2 +- src/freestanding/linkline.cppm | 8 + src/freestanding/target.cppm | 89 +++++++++ tests/e2e/332_cortex_m_builds_and_boots.sh | 199 +++++++++++++++++++++ tests/e2e/run_all.sh | 12 ++ tests/unit/test_freestanding.cpp | 76 ++++++++ 12 files changed, 599 insertions(+), 17 deletions(-) create mode 100755 tests/e2e/332_cortex_m_builds_and_boots.sh diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml index c60fafc3..80604a21 100644 --- a/.github/workflows/ci-linux-e2e.yml +++ b/.github/workflows/ci-linux-e2e.yml @@ -114,7 +114,7 @@ jobs: # test, and assert its PASS line appeared. A skip fails this job. # ────────────────────────────────────────────────────────────────── baremetal: - name: bare-metal e2e (riscv64-none-elf, qemu) + name: bare-metal e2e (riscv64-none-elf + cortex-m, qemu) runs-on: ubuntu-24.04 timeout-minutes: 40 env: @@ -153,6 +153,18 @@ jobs: XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \ "$XLINGS_BIN" install xim:picolibc-riscv -y test -d "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-picolibc-riscv" + # ⚠️ The M-profile emulator, in BOTH homes for the reason above. + # `xim:qemu-arm` carries `qemu-system-arm` as well as + # `qemu-system-aarch64`; test 332 addresses it by absolute path out of + # the payload, so what matters is that the payload EXISTS in the home + # mcpp uses rather than that a shim resolves. + "$XLINGS_BIN" install xim:qemu-arm -y + XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \ + "$XLINGS_BIN" install xim:qemu-arm -y + # Reachable AND runnable before the tests, so that a missing emulator + # fails this step rather than silently skipping test 332. + ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm \ + | sort -V | tail -1 | xargs -I{} {} --version | head -1 - name: Bare-metal e2e timeout-minutes: 25 @@ -175,7 +187,8 @@ jobs: for t in tests/e2e/130_freestanding_riscv_build_and_run.sh \ tests/e2e/131_freestanding_bsp_supplies_everything.sh \ tests/e2e/132_freestanding_test_and_artifacts.sh \ - tests/e2e/133_freestanding_std_subset.sh; do + tests/e2e/133_freestanding_std_subset.sh \ + tests/e2e/332_cortex_m_builds_and_boots.sh; do echo "=== $t ===" bash "$t" 2>&1 | tee "$(basename "$t").log" rc=${PIPESTATUS[0]} @@ -196,6 +209,18 @@ jobs: grep -q 'PASS: the freestanding std subset' \ 133_freestanding_std_subset.sh.log || { echo "133 (std subset) skipped on the runner that must run it"; exit 1; } + # ⚠️ 332 declares `# requires: qemu-arm`, which no sharded runner has + # — so on the shards it exits 0 without running. This job is the only + # place its PASS line can be demanded. + grep -q 'PASS: cortex-m rows build, boot' \ + 332_cortex_m_builds_and_boots.sh.log || { + echo "332 (cortex-m) skipped on the runner that must run it"; exit 1; } + # ⭐ And a count, because four `grep -q` calls that each matched say + # nothing about how many rows the script actually booted: a fixture + # that stopped iterating would still print its PASS line. + booted=$(grep -c 'booted on ' 332_cortex_m_builds_and_boots.sh.log || true) + [ "$booted" = "4" ] || { + echo "332 booted $booted rows, expected 4"; exit 1; } # ────────────────────────────────────────────────────────────────── # Hermetic (no host toolchain): the ONLY environment class that diff --git a/CHANGELOG.md b/CHANGELOG.md index cf6ff563..d242dd51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,36 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [2026.9.4.1] — 2026-09-04 + +Cortex-M 落地为七个目标行,freestanding 链接开启死代码段消除。 + +裸机目标表从四行增至十一行。M-profile 是七行而不是一行:为 `thumbv7em` 构建的 +目标文件使用 Cortex-M0 没有的指令,两种拼写产出互不兼容的目标文件,而表存在的 +理由正是让 `--target ` 单独足以产出正确的目标文件。 + +```toml +[build] +target = "thumbv7em-none-eabihf" +``` + +⚠️ **浮点 ABI 不决定 FPU 是否被使用。** `eabi`/`eabihf` 由 clang 从 triple 读出, +它约束浮点值如何跨越函数边界,不约束函数内部发什么指令 —— 而 `thumbv7em` 架构 +蕴含 FPv4-SP。实测:软浮点 ABI 下 clang 对一次 float 乘法仍发出 `vmul.f32`,在 +没有 FPU 的 Cortex-M4 上于运行期触发异常,而编译与链接都是干净的。每个软浮点行 +因此携带 `-mfpu=none`,包括架构本来就没有 FPU 的那几行 —— 一行陈述它保证的性质, +而不是从一个可以改变的默认值继承它。 + +freestanding 编译加 `-ffunction-sections -fdata-sections`、链接加 `--gc-sections`。 +依赖的目标文件无条件进入链接(不像归档成员那样按未定义符号拉取),当 C 库改由 +依赖图提供时,没有死代码段消除的镜像会装进整份 C 库,而 Cortex-M 器件只有几十 KB。 + +⚠️ **链接脚本因此以新的方式承重**:中断向量表不被任何东西引用,`--gc-sections` +会回收它,板级脚本必须写 `KEEP(*(.vectors))`。 + +同时回填了 `docs/13` 中两条已被 2026.8.28.2 推翻的限制:当图中有包提供 +`hosted-standard-library` 时,裸机目标上的异常、RTTI 与 `import std` 均可用。 + ## [2026.9.3.2] — 2026-09-03 `[xlings.workspace]` 的**推荐书写形态**定为命名空间在键上,官方包全部使用它; diff --git a/docs/13-baremetal.md b/docs/13-baremetal.md index cdb752c2..4c44ae5c 100644 --- a/docs/13-baremetal.md +++ b/docs/13-baremetal.md @@ -13,7 +13,7 @@ covers the hosted link model this chapter departs from. ## Overview A freestanding target is a target whose `os` field is `none`. The target table -at `modules/toolchain-model/src/triple.cppm` carries four of them: +at `modules/toolchain-model/src/triple.cppm` carries eleven of them: | Triple | Tier | C library | |---|---|---| @@ -21,10 +21,63 @@ at `modules/toolchain-model/src/triple.cppm` carries four of them: | `riscv32-none-elf` | verified | `xim:picolibc-riscv` | | `aarch64-none-elf` | preview | none by default — the zero-libc tier; `xim:picolibc-aarch64` is declarable | | `x86_64-none-elf` | preview | none by default — the zero-libc tier; `xim:picolibc-x86` is declarable | +| `thumbv6m-none-eabi` | verified | none by default — Cortex-M0/M0+/M1 | +| `thumbv7m-none-eabi` | verified | none by default — Cortex-M3 | +| `thumbv7em-none-eabi` | preview | none by default — Cortex-M4/M7, soft float | +| `thumbv7em-none-eabihf` | verified | none by default — Cortex-M4F/M7F, hard float | +| `thumbv8m.base-none-eabi` | preview | none by default — Cortex-M23 | +| `thumbv8m.main-none-eabi` | verified | none by default — Cortex-M33/M55, soft float | +| `thumbv8m.main-none-eabihf` | preview | none by default — Cortex-M33F/M55F, hard float | `verified` means an image has been built **and run** for the row. `preview` -means it builds and has been observed to run, but is not yet covered by the -engine's own emulator jobs. +means it builds and links, and no emulator run has been recorded. + +### M-profile is seven rows rather than one + +Every other bare-metal family above is one row per architecture. Cortex-M is +not. An object built for `thumbv7em` uses instructions a Cortex-M0 does not +have, and the two spellings produce incompatible objects rather than expressing +a preference. The table exists so that `--target ` alone suffices to +produce a correct object file; a single `arm-none-eabi` row plus an `-mcpu` that +each project remembered would move a correctness decision out of the table and +into every manifest. + +The `eabi`/`eabihf` suffix is the float ABI, and clang derives it from the +triple without help: measured on llvm 22.1.8, `thumbv7em-none-eabi` yields +`-mfloat-abi soft` and `thumbv7em-none-eabihf` yields `hard`. + +⚠️ **The float ABI does not settle whether the FPU is used.** It governs how +floating-point values cross a function boundary, not what the compiler may emit +inside one, and the `thumbv7em` architecture implies FPv4-SP. Measured: under +the soft-float ABI clang still emits `vmul.f32` for a float multiply. On a +Cortex-M4 without an FPU that instruction faults at run time, after a clean +compile and a clean link. Every soft-float row therefore carries `-mfpu=none`, +including the rows describing architectures that have no FPU at all — a row +states the property it guarantees rather than inheriting it from a default. + +Cortex-M needs no `lldEmulation` column entry: clang has a *BareMetal* toolchain +for arm, so these triples reach `ld.lld` through the driver as the RISC-V and +aarch64 rows do. 32-bit ARM has no `-mcmodel` axis, so that column is empty too. + +### Dead-section elimination + +Freestanding builds compile with `-ffunction-sections -fdata-sections` and link +with `--gc-sections`. Both halves belong to the engine rather than to a project +because a dependency's translation units must carry them, and a project cannot +reach those. + +The flags became necessary rather than merely economical when a C library began +arriving from the dependency graph. A dependency's object files enter the link +unconditionally, unlike an archive member, which is pulled only while its symbol +is undefined. That costs nothing when the C library is a prebuilt archive and +the target has megabytes; a Cortex-M part has kilobytes, and without dead-section +elimination every image would carry the whole of the C library. + +⚠️ **A linker script becomes load-bearing in a new way.** An interrupt vector +table is referenced by nothing — the hardware reads it by address — so +`--gc-sections` collects it. A board's script must say `KEEP(*(.vectors))`. +Measured: with the `KEEP` present, a function nothing calls is dropped, the +table survives, and the image boots. ⚠️ The last two rows default to no C library, and that is a statement rather than an omission: the first consumer of both rows — the `openarch` layer of @@ -190,8 +243,8 @@ by pointing `main` at the source file that carries `_start`. | Linker selection | `ld.lld` is addressed by **absolute path**, derived from the driver's own directory. `-fuse-ld=lld` resolves by name and finds GNU ld on any machine with binutils earlier on `PATH`, which then fails with `unrecognised emulation mode: elf64lriscv`. | | ISA flags | `-march`, `-mabi` and `-mcmodel` come from one row per target in `src/freestanding/target.cppm`, so `--target ` alone is sufficient to produce a correct object file. | | C library | The **target's**, resolved by mcpp from the target's own table row exactly as the compiler is. A bare-metal project declares no libc, just as a hosted project declares no glibc. The engine places the sysroot's library directory on the link search path, so a board-support package selects out of it by bare name (`-lc`, `-lcrt0-semihost`). | -| Exceptions and RTTI | Off on every translation unit in the graph, including a dependency's. There is no unwinder and no `libc++abi`, so nothing can throw; `std::optional::value()` alone would otherwise reference `__cxa_throw` and three further undefined symbols. The setting belongs to the target rather than to a project's `cxxflags` because a BMI records it, and a dependency compiled with exceptions cannot be imported by a unit without them. | -| `import std` | Unavailable, and rejected at configure time with a diagnostic rather than at link time. | +| Exceptions and RTTI | Off on every translation unit in the graph, including a dependency's, **unless a package supplies a C++ runtime built for this target**. There is otherwise no unwinder and no `libc++abi`, so nothing can throw; `std::optional::value()` alone would reference `__cxa_throw` and three further undefined symbols. The setting belongs to the target rather than to a project's `cxxflags` because a BMI records it, and a dependency compiled with exceptions cannot be imported by a unit without them. A package declaring `provides = ["hosted-standard-library"]` reverses the default: exceptions and RTTI are enabled, `-ffreestanding` is dropped, and `-fasynchronous-unwind-tables` is added. | +| `import std` | Available when a package in the graph provides `hosted-standard-library` and names its own `std` module source; otherwise rejected at configure time with a diagnostic rather than at link time. | | Entry point | `int main()` is available whenever something supplies a `crt0`. A board-support package normally does. | | Default linkage | Static, and not as a preference: there is no loader, so there is no other option. | @@ -716,7 +769,7 @@ targets, but that expectation is **not** covered by a test. | Limitation | Observed behaviour | |---|---| | `std::format`, `std::sort` over builtin scalar types, and a complete `std::string` | Fail at **link** time naming the undefined symbol. libc++ places these entities in the compiled library — the scalar `__sort` instantiations are `extern template`, with no macro that disables them — so a target-built `libc++.a` is required. No such payload is published. | -| Exceptions and RTTI | Disabled across the whole graph. `try`/`catch` is unavailable at compile time. A board shipping a target-built `libc++abi` and unwinder has a genuine case for re-enabling them; that is the point at which this becomes a manifest key. | +| Exceptions and RTTI | Disabled across the whole graph **unless a package provides `hosted-standard-library`**, which `mcpplibs/openkal-llvm-runtime` does by carrying `libc++`, `libc++abi` and `libunwind` configured for the target. Without such a package `try`/`catch` remains unavailable at compile time. | | Board coverage | One board family. `riscv32-none-elf` demonstrates that the ISA table is data, not that a second machine has been ported. ARM Cortex-M has not been attempted. | | C library substitution | Expressible since 2026.8.20.2 through `[target.].sysroot`, and **verified only for the empty value** (the zero-libc tier). Pointing it at a different C library is accepted and installed through the same channel, but no second bare-metal C library is published, so that path is untested. | | `qemu-riscv` on `win32-arm64` | The upstream package publishes no asset for that host, so installation fails on it. The failure is correct rather than silent, but the host cannot run a bare-metal image. | diff --git a/docs/zh/13-baremetal.md b/docs/zh/13-baremetal.md index 214084ac..ee57def5 100644 --- a/docs/zh/13-baremetal.md +++ b/docs/zh/13-baremetal.md @@ -11,7 +11,7 @@ ## 概述 freestanding 目标是 `os` 字段为 `none` 的目标。`modules/toolchain-model/src/triple.cppm` -的目标表中有四个: +的目标表中有十一个: | Triple | 档位 | C 库 | |---|---|---| @@ -19,9 +19,53 @@ freestanding 目标是 `os` 字段为 `none` 的目标。`modules/toolchain-mode | `riscv32-none-elf` | verified | `xim:picolibc-riscv` | | `aarch64-none-elf` | preview | 默认无 —— 零 libc 层;`xim:picolibc-aarch64` 可声明 | | `x86_64-none-elf` | preview | 默认无 —— 零 libc 层;`xim:picolibc-x86` 可声明 | - -`verified` 意味着该行的镜像被构建**并被运行**过。`preview` 意味着它构建得出、 -也被观察到能运行,但尚未纳入引擎自己的模拟器作业。 +| `thumbv6m-none-eabi` | verified | 默认无 —— Cortex-M0/M0+/M1 | +| `thumbv7m-none-eabi` | verified | 默认无 —— Cortex-M3 | +| `thumbv7em-none-eabi` | preview | 默认无 —— Cortex-M4/M7,软浮点 | +| `thumbv7em-none-eabihf` | verified | 默认无 —— Cortex-M4F/M7F,硬浮点 | +| `thumbv8m.base-none-eabi` | preview | 默认无 —— Cortex-M23 | +| `thumbv8m.main-none-eabi` | verified | 默认无 —— Cortex-M33/M55,软浮点 | +| `thumbv8m.main-none-eabihf` | preview | 默认无 —— Cortex-M33F/M55F,硬浮点 | + +`verified` 意味着该行的镜像被构建**并被运行**过。`preview` 意味着它构建并链接得出, +尚无模拟器运行记录。 + +### M-profile 是七行而不是一行 + +上面每一个裸机族都是一个架构一行。Cortex-M 不是:为 `thumbv7em` 构建的目标文件 +使用 Cortex-M0 没有的指令,两种拼写产出的是互不兼容的目标文件,而不是一种偏好。 +这张表存在,是为了让 `--target ` 单独就足以产出正确的目标文件;若只写一行 +`arm-none-eabi` 再让每个工程各自记住一个 `-mcpu`,就把一个正确性决定从表里搬进了 +每一份清单。 + +`eabi`/`eabihf` 后缀即浮点 ABI,clang 直接从 triple 读出它:实测 llvm 22.1.8, +`thumbv7em-none-eabi` 得到 `-mfloat-abi soft`,`thumbv7em-none-eabihf` 得到 `hard`。 + +⚠️ **浮点 ABI 并不决定 FPU 是否被使用。** 它约束浮点值如何跨越函数边界,不约束 +编译器在函数内部可以发什么指令,而 `thumbv7em` 架构蕴含 FPv4-SP。实测:在软浮点 +ABI 下,clang 对一次 float 乘法仍然发出 `vmul.f32`。在没有 FPU 的 Cortex-M4 上, +这条指令在运行期触发异常 —— 而编译与链接都是干净的。因此每一个软浮点行都携带 +`-mfpu=none`,**包括那些架构本来就没有 FPU 的行**:一行应当陈述它所保证的性质, +而不是从一个随时可以改变的默认值继承它。 + +Cortex-M 不需要 `lldEmulation` 列:clang 有面向 arm 的 *BareMetal* 工具链,这些 +triple 与 RISC-V、aarch64 各行一样经驱动到达 `ld.lld`。32 位 ARM 没有 `-mcmodel` +这个轴,所以该列同样为空。 + +### 死代码段消除 + +freestanding 构建以 `-ffunction-sections -fdata-sections` 编译,以 `--gc-sections` +链接。两半都属于引擎而不属于工程,因为依赖的翻译单元也必须带上它们,而工程够不到 +那些单元。 + +这两个标志从「划算」变成「必需」,发生在 C 库开始由依赖图提供的时候。依赖的目标 +文件是**无条件**进入链接的,不像归档成员那样只在符号仍未定义时才被拉入。当 C 库是 +预编译归档、目标又有若干兆字节时,这不花什么代价;而 Cortex-M 器件只有几十 KB, +没有死代码段消除,每个镜像都会装进整份 C 库。 + +⚠️ **链接脚本因此以一种新的方式承重。** 中断向量表不被任何东西引用 —— 硬件按地址 +读取它 —— 所以 `--gc-sections` 会把它回收。板级脚本必须写 `KEEP(*(.vectors))`。 +实测:有这条 `KEEP` 时,无人调用的函数被丢弃、向量表被保留、镜像能够启动。 ⚠️ 后两行**默认**没有 C 库,这是声明而非遗漏:这两行的第一个消费者 —— 机器机制层 `openarch` —— 一个 C 库符号都不引用,而**如果四行里没有一行默认在这一层,就没有 @@ -167,8 +211,8 @@ extern "C" int main() { | 链接器选择 | `ld.lld` 按**绝对路径**寻址,由驱动自身所在目录推导。`-fuse-ld=lld` 按名字解析,在任何 binutils 位于 `PATH` 更靠前位置的机器上都会找到 GNU ld,随后以 `unrecognised emulation mode: elf64lriscv` 失败。 | | ISA 参数 | `-march`、`-mabi` 与 `-mcmodel` 来自 `src/freestanding/target.cppm` 中每个目标一行的表,因此仅凭 `--target ` 就足以产出正确的目标文件。 | | C 库 | 属于**目标**,由 mcpp 从目标自己的表行解析,与解析编译器的方式完全一致。裸机工程不声明 libc,正如宿主工程不声明 glibc。引擎把 sysroot 的库目录放入链接搜索路径,板级支持包因此可以按裸名从中选取(`-lc`、`-lcrt0-semihost`)。 | -| 异常与 RTTI | 在图中每个翻译单元上都关闭,包括依赖的翻译单元。没有 unwinder 也没有 `libc++abi`,因此任何东西都无法抛出;仅 `std::optional::value()` 一处就会引用 `__cxa_throw` 以及另外三个未定义符号。该设定属于目标而非工程的 `cxxflags`,因为 BMI 会记录它,而带异常编出的依赖无法被不带异常的单元导入。 | -| `import std` | 不可用,并且在配置期以诊断拒绝,而不是在链接期失败。 | +| 异常与 RTTI | 在图中每个翻译单元上都关闭,包括依赖的翻译单元,**除非有包提供为该目标构建的 C++ 运行时**。否则既没有 unwinder 也没有 `libc++abi`,任何东西都无法抛出;仅 `std::optional::value()` 一处就会引用 `__cxa_throw` 以及另外三个未定义符号。该设定属于目标而非工程的 `cxxflags`,因为 BMI 会记录它,而带异常编出的依赖无法被不带异常的单元导入。声明 `provides = ["hosted-standard-library"]` 的包会翻转这个默认:异常与 RTTI 开启,`-ffreestanding` 被去掉,并加上 `-fasynchronous-unwind-tables`。 | +| `import std` | 当图中有包提供 `hosted-standard-library` 并指明自己的 `std` 模块源码时可用;否则在配置期以诊断拒绝,而不是在链接期失败。 | | 入口点 | 只要有东西提供 `crt0`,`int main()` 就可用。板级支持包通常提供它。 | | 默认链接方式 | 静态,而且这不是偏好:没有加载器,因此没有别的选项。 | @@ -643,7 +687,7 @@ int main() { | 边界 | 观察到的行为 | |---|---| | `std::format`、内建标量类型上的 `std::sort`、以及完整的 `std::string` | 在**链接**期失败并点名未定义符号。libc++ 把这些实体放在编译版库中 —— 标量 `__sort` 的实例化是 `extern template`,没有可用于关闭它们的宏 —— 因此需要为目标编出的 `libc++.a`。该载荷尚未发布。 | -| 异常与 RTTI | 在整张图上关闭。`try`/`catch` 在编译期即不可用。一块随包提供目标版 `libc++abi` 与 unwinder 的板子有重新开启它们的正当理由;那也正是这一项应当成为一个清单键的时刻。 | +| 异常与 RTTI | 在整张图上关闭,**除非有包提供 `hosted-standard-library`** —— `mcpplibs/openkal-llvm-runtime` 就通过携带为该目标配置过的 `libc++`、`libc++abi` 与 `libunwind` 做到了这一点。没有这样的包时,`try`/`catch` 在编译期仍不可用。 | | 板子覆盖面 | 只有一个板级家族。`riscv32-none-elf` 证明的是 ISA 表为数据,而不是已移植第二台机器。ARM Cortex-M 尚未尝试。 | | 替换 C 库 | 自 2026.8.20.2 起可经 `[target.].sysroot` 表达,而**仅空值一侧经过验证**(零 libc 档)。指向另一份 C 库同样被接受并经同一通道安装,但生态中没有第二份裸机 C 库,该路径未经测试。 | | `win32-arm64` 上的 `qemu-riscv` | 上游包未为该宿主发布资产,因此在其上安装会失败。该失败是正确的而非静默的,但该宿主无法运行裸机镜像。 | diff --git a/mcpp.toml b/mcpp.toml index c38496d9..5427c6bd 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "2026.9.3.2" +version = "2026.9.4.1" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index db1eb3e8..77c60fb9 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -351,6 +351,52 @@ inline constexpr TargetInfo kKnownTargets[] = { // above `aarch64-none-elf`: the first consumer is `openarch`, which // references no C library symbol. { "x86_64-none-elf", "preview", "bare","llvm@22.1.8","", true }, + // ── Cortex-M ──────────────────────────────────────────────────────────── + // + // ⚠️ SEVEN ROWS AND NOT ONE, BECAUSE "Cortex-M" IS NOT AN INSTRUCTION SET. + // + // Every other bare-metal family here is one row per architecture. M-profile + // is not: an object built for `thumbv7em` uses instructions a Cortex-M0 + // does not have, and one built for `thumbv6m` runs on both but leaves the + // larger part unused. The two spellings are not a preference a board + // expresses — they produce incompatible objects — so they are rows. + // + // The rule this table states about itself governs: it exists so that + // `--target ` ALONE is enough to produce a correct object file. A + // single `arm-none-eabi` row plus an `-mcpu` the project remembers would + // move a correctness decision out of the table and into every manifest. + // + // ⭐ THE `eabi`/`eabihf` SUFFIX IS THE FLOAT ABI, AND CLANG ALREADY READS + // IT. Measured on llvm 22.1.8 (`-###`, `-cc1` line): `thumbv7em-none-eabi` + // gives `-mfloat-abi soft` and `-none-eabihf` gives `hard`, with no flag + // from us. So the ABI needs no entry in the ISA table's `extra` column — + // only the FPU does, and only on the soft rows. See `kThumbSoftExtra`. + // + // ⚠️ `sysroot` IS EMPTY ON EVERY ROW, AND THAT IS THE POINT RATHER THAN A + // GAP. The three older bare-metal families name an `xim:` payload here; a C + // library for these targets arrives from the DEPENDENCY GRAPH instead + // (`mcpp:c-abi=picolibc`, docs/14). A prebuilt payload would have to ship + // one multilib per row — seven here — and the `libdir` column would have to + // match its layout byte for byte, which is the defect #481 fixed. A source + // package is compiled with the consuming target's own flags, so the ABI + // agreement holds by construction and there is no multilib at all. + // + // Two rows are `verified` because two have a board and an emulator that + // runs them; the rest build and are `preview`, the same standard applied to + // `aarch64-none-elf` and `x86_64-none-elf` when they landed. + // ⚠️ THE TIER COLUMN RECORDS WHAT WAS RUN, NOT WHAT WAS REASONED. Measured + // 2026-09-04 under `xim:qemu-arm@9.2.4-1`: each `verified` row below built + // an image that BOOTED on the named machine and printed over semihosting — + // thumbv6m on `microbit`, thumbv7m on `mps2-an385`, thumbv7em-eabihf on + // `mps2-an386`, thumbv8m.main-eabi on `mps2-an505`. The three `preview` + // rows build and link; no emulator run has been recorded for them. + { "thumbv6m-none-eabi", "verified", "bare","llvm@22.1.8","", true }, + { "thumbv7m-none-eabi", "verified", "bare","llvm@22.1.8","", true }, + { "thumbv7em-none-eabi", "preview", "bare","llvm@22.1.8","", true }, + { "thumbv7em-none-eabihf", "verified", "bare","llvm@22.1.8","", true }, + { "thumbv8m.base-none-eabi","preview", "bare","llvm@22.1.8","", true }, + { "thumbv8m.main-none-eabi","verified", "bare","llvm@22.1.8","", true }, + { "thumbv8m.main-none-eabihf","preview","bare","llvm@22.1.8","", true }, }; inline std::span known_targets() { return kKnownTargets; } diff --git a/modules/versioning/src/version.cppm b/modules/versioning/src/version.cppm index de82c29a..255fcdb0 100644 --- a/modules/versioning/src/version.cppm +++ b/modules/versioning/src/version.cppm @@ -31,6 +31,6 @@ import std; export namespace mcpp { -inline constexpr std::string_view MCPP_VERSION = "2026.9.3.2"; +inline constexpr std::string_view MCPP_VERSION = "2026.9.4.1"; } // namespace mcpp diff --git a/src/freestanding/linkline.cppm b/src/freestanding/linkline.cppm index 913a9c09..d0c873c8 100644 --- a/src/freestanding/linkline.cppm +++ b/src/freestanding/linkline.cppm @@ -89,6 +89,9 @@ inline std::string link_flags(const Spec& s, const LinkInputs& in, // suggesting it does. for (auto const& f : compile_flags(s)) { out += ' '; out += f; } out += " -nostdlib -nostartfiles -static"; + // The link half of the pair set in mcpp.freestanding.target's + // `compile_flags`. Routed through the driver, hence `-Wl,`. + out += " -Wl,--gc-sections"; if (!in.lld.empty()) out += " -fuse-ld=" + esc(in.lld); // ⚠️ BEFORE the libraries the board selects, and it has to be on THIS line @@ -129,6 +132,11 @@ inline std::string link_flags_direct(const Spec& s, const LinkInputs& in, // No loader exists, so an image must not name one. The driver line reaches // this through `-static`; here it is said to the linker. out += " --no-dynamic-linker"; + // Said to the linker directly here, as `-Wl,--gc-sections` is said through + // the driver on the line above. The two paths must agree: a target whose + // link is driven by `ld.lld` itself would otherwise keep every section its + // dependencies emit, and the difference would show only as size. + out += " --gc-sections"; if (!in.sysrootLib.empty()) out += " -L" + esc(in.sysrootLib); if (!in.linkerScript.empty()) diff --git a/src/freestanding/target.cppm b/src/freestanding/target.cppm index 88871473..b545194e 100644 --- a/src/freestanding/target.cppm +++ b/src/freestanding/target.cppm @@ -108,6 +108,46 @@ inline constexpr std::string_view kX86_64NoneExtra[] = { "-mno-red-zone", }; +// ⚠️ THE SOFT-FLOAT M-PROFILE ROWS, AND WHY THE FLOAT ABI IS NOT ENOUGH. +// +// `thumbv7em-none-eabi` and `-eabihf` differ in their float ABI, and clang +// derives that from the triple with no help from us (measured, `-###`: +// `-mfloat-abi soft` and `hard` respectively). It would be reasonable to stop +// there. Measured on llvm 22.1.8, that is wrong: +// +// clang --target=thumbv7em-none-eabi -O2 -S (float a*b+1.0f) +// vmul.f32 ← an FPU instruction +// +// The float ABI governs how floating-point values cross a function boundary. +// It does not govern whether the compiler may USE the FPU inside one, and the +// `thumbv7em` architecture implies FPv4-SP, so clang emits FPU instructions +// under either ABI. A plain Cortex-M4 — the part this row exists to serve, +// since the -eabihf row serves M4F — has no FPU, and the instruction faults. +// +// ⚠️ The failure is a hard fault at run time on real silicon, with a clean +// compile and a clean link. It is the shape this table exists to prevent. +// +// `-mfpu=none` restores the property the row's name claims. Measured: the +// instruction count returns to zero, and the `-eabihf` rows must NOT receive +// this flag — there it would discard the FPU the row exists to use. +// +// ⚠️ EVERY SOFT ROW CARRIES IT, INCLUDING THE ONES WHERE NOTHING WAS MEASURED. +// +// Only `thumbv7em-none-eabi` was observed emitting an FPU instruction; +// `thumbv6m`, `thumbv7m` and `thumbv8m.base` describe architectures with no FPU +// at all, and `thumbv8m.main-none-eabi` emits none today because its default +// CPU is `generic`. Applying the flag only where a count was non-zero would +// make the table record a MEASUREMENT rather than a GUARANTEE, and the rows +// would then differ for a reason no reader could see. +// +// A row states the property it promises. `-mfpu=none` is accepted on all four +// (measured), costs nothing where there is no FPU, and is what a unit test can +// quantify over — which is the only place this rule can be stated, since a +// build of any one row cannot speak for the others. +inline constexpr std::string_view kThumbSoftExtra[] = { + "-mfpu=none", +}; + // ⚠️ THE `libdir` COLUMN WAS EMPTY ON THE LAST TWO ROWS UNTIL 2026-08-21, AND // THAT WAS CORRECT UNTIL THE DAY IT WAS NOT. // @@ -184,6 +224,31 @@ inline constexpr Spec kTable[] = { // resolves no C library, so a value here could never be checked. { "x86_64-none-elf", "x86-64", "sysv", "small", "x86-64/sysv", kX86_64NoneExtra, "elf_x86_64" }, + // ── Cortex-M ──────────────────────────────────────────────────────────── + // + // `mcmodel` is EMPTY on every row: 32-bit ARM has no `-mcmodel` axis, and + // `compile_flags` already omits the flag when this column is empty. + // + // `lldEmulation` is EMPTY on every row, and that was measured rather than + // assumed. clang has a BareMetal toolchain for arm, so these triples reach + // `ld.lld` through the driver exactly as the riscv and aarch64 rows do — + // the x86_64 row's problem does not recur here. Verified end to end: a + // freestanding thumb object links under `ld.lld` and boots under QEMU. + // + // `libdir` is EMPTY on every row because `sysroot` is (see the target + // table): the column is read only when a sysroot has been resolved, and a + // value here could never be checked. + // + // ⚠️ `-mabi=aapcs`, matching the aarch64 row and for the same reason: on + // ARM `-mabi` names a procedure call standard, not a data model. + // triple march mabi mcmodel libdir extra + { "thumbv6m-none-eabi", "armv6-m", "aapcs", "", "", kThumbSoftExtra }, + { "thumbv7m-none-eabi", "armv7-m", "aapcs", "", "", kThumbSoftExtra }, + { "thumbv7em-none-eabi", "armv7e-m", "aapcs", "", "", kThumbSoftExtra }, + { "thumbv7em-none-eabihf", "armv7e-m", "aapcs", "", "" }, + { "thumbv8m.base-none-eabi", "armv8-m.base","aapcs","", "", kThumbSoftExtra }, + { "thumbv8m.main-none-eabi", "armv8-m.main","aapcs","", "", kThumbSoftExtra }, + { "thumbv8m.main-none-eabihf","armv8-m.main","aapcs","", "" }, }; // The single read point. Returns nullopt for anything that is not a known @@ -226,6 +291,30 @@ inline std::vector compile_flags(const Spec& s, // `-ffreestanding` so the ordering of this function's output stays a // function of the table rather than of the row. for (auto flag : s.extra) out.emplace_back(flag); + // ⭐⭐ ONE SECTION PER FUNCTION, SO THE LINKER CAN DROP WHAT NOTHING CALLS. + // + // These two flags do nothing on their own; they are the half of + // `--gc-sections` that has to happen at compile time, and the link half is + // in mcpp.freestanding.linkline. Both halves are here rather than left to a + // project because a dependency's translation units must carry them too, and + // a project cannot reach those. + // + // ⚠️ WHY THIS BECAME NECESSARY RATHER THAN MERELY NICE. A dependency's + // object files enter the link unconditionally (docs/13), unlike an archive + // member, which is pulled only while its symbol is undefined. That costs + // nothing when the C library is a prebuilt archive and the target has + // megabytes — the arrangement every bare-metal row had until now. A C + // library that arrives from the dependency graph is object files, and a + // Cortex-M part has kilobytes: without this, every image carries the whole + // of the C library whether or not it calls into it. + // + // ⚠️ AND IT MAKES A LINKER SCRIPT LOAD-BEARING IN A NEW WAY: an interrupt + // vector table is referenced by nothing — the hardware reads it by address + // — so `--gc-sections` collects it. A board's script must say + // `KEEP(*(.vectors))`. Measured: with the KEEP present, a dead function is + // dropped and the table survives; the image boots. + out.emplace_back("-ffunction-sections"); + out.emplace_back("-fdata-sections"); // ⭐ AND `-ffreestanding` ITSELF IS ONE OF THE THINGS THE GRAPH DECIDES. // // The paragraph above this function names what the flag changes: "no `main` diff --git a/tests/e2e/332_cortex_m_builds_and_boots.sh b/tests/e2e/332_cortex_m_builds_and_boots.sh new file mode 100755 index 00000000..6535d107 --- /dev/null +++ b/tests/e2e/332_cortex_m_builds_and_boots.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +# requires: llvm unix-shell qemu-arm +# Cortex-M: the M-profile rows build, boot, and collect what nothing calls. +# +# ⚠️ THE ROWS THAT MATTER HERE ARE THE ONES A REASONED TABLE WOULD HAVE GOT +# WRONG. Two properties are asserted that a build alone cannot show: +# +# * `--gc-sections` reaches a bare-metal link. Without it a C library that +# arrives from the dependency graph — object files, which enter a link +# unconditionally — puts its whole self into every image, and a Cortex-M +# part has kilobytes. The assertion is that a function nothing calls is +# ABSENT from the image while the interrupt vector table, which nothing +# references either, SURVIVES because the script says KEEP. +# +# * the soft-float rows do not emit FPU instructions. `thumbv7em`'s +# architecture implies an FPU, so clang emits `vmul.f32` for a float +# multiply under the soft-float ABI as readily as under the hard one; on a +# Cortex-M4 without an FPU that faults at run time, with a clean compile and +# a clean link. The row carries `-mfpu=none` for this, and the assertion is +# an instruction count. +# +# ⚠️ AND THE IMAGE IS RUN, NOT INSPECTED. A freestanding image that links is not +# evidence: the entry point and the ordering of the vector table are only +# exercised by a machine that fetches from address zero. +set -e + +MCPP="${MCPP:-mcpp}" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +qemu_arm() { + local d + for d in "${MCPP_HOME:-$HOME/.mcpp}/registry" "$HOME/.xlings"; do + local c + c=$(ls "$d"/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm 2>/dev/null | sort -V | tail -1) + [ -n "$c" ] && [ -x "$c" ] && { echo "$c"; return 0; } + done + command -v qemu-system-arm 2>/dev/null && return 0 + return 1 +} +QEMU="$(qemu_arm)" || { echo "SKIP: qemu-system-arm not installed"; exit 0; } + +llvm_tool() { + local c + c=$(ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-llvm/*/bin/"$1" 2>/dev/null | sort -V | tail -1) + [ -n "$c" ] && { echo "$c"; return 0; } + command -v "$1" 2>/dev/null +} +NM="$(llvm_tool llvm-nm)" || { echo "SKIP: llvm-nm not found"; exit 0; } +OBJDUMP="$(llvm_tool llvm-objdump)" || { echo "SKIP: llvm-objdump not found"; exit 0; } + +mkdir -p "$work/mcu/src" +cd "$work/mcu" + +cat > mcpp.toml <<'TOML' +[package] +name = "mcu" +version = "0.1.0" +TOML + +# ⚠️ `volatile` on the operands. Without it the multiply is constant-folded and +# the FPU assertion below passes for a reason unrelated to the flag. +cat > src/main.cpp <<'CPP' +namespace { +inline void sh(int op, const void* a) { + register int r0 __asm__("r0") = op; + register const void* r1 __asm__("r1") = a; + __asm__ volatile("bkpt 0xAB" :: "r"(r0), "r"(r1) : "memory"); +} +} +extern "C" unsigned __stack_top; + +// Referenced by nothing. The image must not contain it. +extern "C" void collected_because_nothing_calls_it() { sh(0x04, (void*)"UNREACHABLE\n"); } + +extern "C" void Reset_Handler() { + sh(0x04, (void*)"cortex-m ok\n"); + sh(0x18, (void*)0x20026); // semihosting SYS_EXIT, ADP_Stopped_ApplicationExit + for (;;) {} +} + +// Referenced by nothing either — the hardware reads it by address. It survives +// only because the linker script says KEEP. +extern "C" __attribute__((section(".vectors"), used)) +void* const vectors[] = { (void*)&__stack_top, (void*)Reset_Handler }; +CPP + +cat > build.mcpp <<'BUILD' +import mcpp; +int main() { mcpp::link_script("link.ld"); return 0; } +BUILD + +# The memory map is a BOARD fact, not a target fact — which is why it is written +# here per machine rather than derived from the triple. +emit_ld() { + cat > link.ld < FLASH + .data : { *(.data*) } > RAM + .bss : { *(.bss*) *(COMMON) } > RAM + __stack_top = ORIGIN(RAM) + LENGTH(RAM); +} +EOF +} + +ran=0 +boot_row() { # triple machine cpuflag flash_org flash_len ram_org ram_len + local triple=$1 machine=$2 cpuflag=$3 + emit_ld "$4" "$5" "$6" "$7" + rm -rf target + "$MCPP" build --target "$triple" >/dev/null 2>&1 || { + echo "FAIL: $triple did not build"; exit 1; } + local elf + elf=$(find target -type f -name mcu | head -1) + [ -n "$elf" ] || { echo "FAIL: $triple produced no artefact"; exit 1; } + + # gc-sections: the dead function is gone, the vector table is not. + if [ "$("$NM" "$elf" | grep -c collected_because_nothing_calls_it)" != "0" ]; then + echo "FAIL: $triple kept a function nothing calls (--gc-sections not applied)"; exit 1 + fi + if [ "$("$NM" "$elf" | grep -c vectors)" != "1" ]; then + echo "FAIL: $triple lost the vector table (KEEP not honoured)"; exit 1 + fi + + local out + out=$(timeout 30 "$QEMU" -machine "$machine" $cpuflag -nographic -semihosting \ + -no-reboot -kernel "$elf" 2>&1 | head -3) + case "$out" in + *"cortex-m ok"*) ;; + *) echo "FAIL: $triple did not boot on $machine; got: $out"; exit 1 ;; + esac + echo " ok $triple booted on $machine" + ran=$((ran + 1)) +} + +# The four rows the target table marks `verified`, each on the machine that +# earned it. nRF51 has 16K of RAM; a generic map hard-faults before `main`. +boot_row thumbv6m-none-eabi microbit "" 0x00000000 256K 0x20000000 16K +boot_row thumbv7m-none-eabi mps2-an385 "-cpu cortex-m3" 0x00000000 4M 0x20000000 4M +boot_row thumbv7em-none-eabihf mps2-an386 "-cpu cortex-m4" 0x00000000 4M 0x20000000 4M +boot_row thumbv8m.main-none-eabi mps2-an505 "" 0x10000000 4M 0x38000000 512K + +# ⚠️ A COUNT, BECAUSE A LOOP THAT RAN ZERO TIMES ALSO REACHES THIS LINE. +[ "$ran" = "4" ] || { echo "FAIL: expected 4 rows to boot, got $ran"; exit 1; } + +# ── The float ABI, asserted on both sides of the pair ────────────────────── +# +# ⚠️ A SEPARATE PROJECT, AND THAT SEPARATION IS ITSELF A MEASUREMENT. Float +# arithmetic cannot live in the fixture above: on a soft-float row it lowers +# onto `__aeabi_fmul`, and this tier has no C library and no builtins to resolve +# it against, so every boot row would fail to link. The boot fixture is +# therefore integer-only and the float question is asked here. +# +# ⚠️ AND ONE SIDE ALONE PROVES NOTHING. "The soft row emitted no FPU +# instruction" is equally true of a build that emitted no float code, so the +# hard row is measured from the same source as the control. +mkdir -p "$work/fp/src" +cd "$work/fp" +cp "$work/mcu/build.mcpp" . +printf '[package]\nname = "fp"\nversion = "0.1.0"\n' > mcpp.toml +cat > src/main.cpp <<'CPP' +extern "C" unsigned __stack_top; +// `volatile`, so the multiply survives constant folding and is really emitted. +volatile float fa = 3.0f, fb = 4.0f, fout = 0.0f; +extern "C" void Reset_Handler() { fout = fa * fb + 1.0f; for (;;) {} } +extern "C" __attribute__((section(".vectors"), used)) +void* const vectors[] = { (void*)&__stack_top, (void*)Reset_Handler }; +CPP +emit_ld 0x00000000 4M 0x20000000 4M + +rm -rf target +"$MCPP" build --target thumbv7em-none-eabihf >/dev/null 2>&1 || { + echo "FAIL: the hard-float row did not build"; exit 1; } +hard=$("$OBJDUMP" -d "$(find target -type f -name fp | head -1)" \ + | grep -cE '\bv[a-z]+\.f32' || true) +[ "$hard" -gt 0 ] || { + echo "FAIL: thumbv7em-none-eabihf emitted no FPU instruction — the control is vacuous"; exit 1; } +echo " ok thumbv7em-none-eabihf uses the FPU ($hard instructions)" + +# ⭐ The soft row's proof is its LINK FAILURE, and that is the strongest form +# available at this tier. `-mfpu=none` makes the compiler lower the multiply +# onto `__aeabi_fmul` rather than `vmul.f32`; with no C library and no builtins +# there is nothing for that call to resolve against. A link that fails naming +# `__aeabi_fmul` therefore states exactly what is asserted: the multiply did +# NOT become an FPU instruction. +rm -rf target +soft_out=$("$MCPP" build --target thumbv7em-none-eabi 2>&1 || true) +case "$soft_out" in + *__aeabi_fmul*) echo " ok thumbv7em-none-eabi lowered the multiply onto a libcall, not the FPU" ;; + *) echo "FAIL: thumbv7em-none-eabi did not reference __aeabi_fmul; -mfpu=none may not be applied" + echo "$soft_out" | tail -5 | sed 's/^/ /'; exit 1 ;; +esac + +echo "PASS: cortex-m rows build, boot, collect dead code and honour the float ABI" diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index a9279640..38a26edc 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -84,6 +84,18 @@ case "$OS" in # qemu-riscv: the emulator a bare-metal riscv artifact runs in # (xim:qemu-riscv, or any qemu-system-riscv64 on PATH). command -v qemu-system-riscv64 &>/dev/null && CAPS+=(qemu-riscv) + # qemu-arm: the emulator an M-profile artifact runs in. Unlike + # `qemu-riscv` this also looks inside the xim payload, because + # `xim:qemu-arm` installs no shim for `qemu-system-arm` on every host + # and the tests address it by absolute path out of the payload anyway. + # Probing only PATH would report the capability absent on a machine + # that has it, and the test would skip while looking supported. + if command -v qemu-system-arm &>/dev/null \ + || ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm \ + &>/dev/null \ + || ls "$HOME"/.xlings/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm &>/dev/null; then + CAPS+=(qemu-arm) + fi # pack capability: ELF + patchelf both required if [[ " ${CAPS[*]} " == *" patchelf "* ]]; then CAPS+=(pack) diff --git a/tests/unit/test_freestanding.cpp b/tests/unit/test_freestanding.cpp index 37d7814a..7d2cdcba 100644 --- a/tests/unit/test_freestanding.cpp +++ b/tests/unit/test_freestanding.cpp @@ -469,3 +469,79 @@ TEST(FreestandingTarget, EveryIsaProfileHasABareRowInTheTargetTable) { << spec.triple << " has an ISA profile but is not marked bare"; } } + +// ── Cortex-M ──────────────────────────────────────────────────────────────── +// +// ⭐ THESE ARE RULES OVER THE TABLE, WHICH IS THE ONLY PLACE THEY CAN BE +// STATED. The e2e boots four M-profile images and measures instruction counts, +// but it can only speak about the rows it happens to exercise. A rule quantified +// over every row is what stops the eighth one from being added wrong, and the +// property it protects is invisible in a build: a soft-float row that reaches +// the FPU compiles, links, and faults on real silicon. + +namespace { +bool is_m_profile(std::string_view triple) { return triple.starts_with("thumb"); } +bool has_flag(const mcpp::freestanding::Spec& s, std::string_view f) { + for (auto e : s.extra) if (e == f) return true; + return false; +} +} // namespace + +// The pair that the float-ABI suffix does NOT settle on its own. clang reads +// `eabi`/`eabihf` and sets `-mfloat-abi` accordingly, but the ABI governs how +// floats cross a call boundary, not whether the compiler may use the FPU inside +// one — and `thumbv7em` implies an FPU. Measured on llvm 22.1.8: without +// `-mfpu=none` the soft row emits `vmul.f32`. +TEST(FreestandingTarget, SoftFloatMProfileRowsDisableTheFpu) { + int soft = 0, hard = 0; + for (const auto& spec : mcpp::freestanding::known()) { + if (!is_m_profile(spec.triple)) continue; + if (spec.triple.ends_with("-eabihf")) { + ++hard; + EXPECT_FALSE(has_flag(spec, "-mfpu=none")) + << spec.triple << " is a hard-float row and must not discard the FPU"; + } else { + ++soft; + EXPECT_TRUE(has_flag(spec, "-mfpu=none")) + << spec.triple + << " is a soft-float row without -mfpu=none: it will emit FPU" + " instructions that fault on a part with no FPU"; + } + } + // ⚠️ A denominator, because a loop that never ran satisfies every EXPECT + // above. Both halves must be non-empty for the contrast to mean anything. + EXPECT_GT(soft, 0); + EXPECT_GT(hard, 0); +} + +// 32-bit ARM has no `-mcmodel`, and clang has a BareMetal toolchain for arm so +// its driver reaches `ld.lld` without the x86_64 row's workaround. Both columns +// being empty is what says so; a filled one would be a value nothing checks. +TEST(FreestandingTarget, MProfileRowsNeedNoCodeModelAndNoDirectLldDriving) { + int rows = 0; + for (const auto& spec : mcpp::freestanding::known()) { + if (!is_m_profile(spec.triple)) continue; + ++rows; + EXPECT_TRUE(spec.mcmodel.empty()) << spec.triple << " has no -mcmodel axis"; + EXPECT_TRUE(spec.lldEmulation.empty()) + << spec.triple << " reaches ld.lld through the clang driver"; + EXPECT_EQ(spec.mabi, "aapcs") << spec.triple; + } + EXPECT_GT(rows, 0); +} + +// The compile half of `--gc-sections`, which has to hold for EVERY freestanding +// target rather than only the M-profile ones: a dependency's translation units +// carry these flags too, and a project cannot reach those. +TEST(FreestandingTarget, EveryRowCompilesWithPerFunctionSections) { + int rows = 0; + for (const auto& spec : mcpp::freestanding::known()) { + ++rows; + auto flags = mcpp::freestanding::compile_flags(spec); + EXPECT_NE(std::find(flags.begin(), flags.end(), "-ffunction-sections"), + flags.end()) << spec.triple; + EXPECT_NE(std::find(flags.begin(), flags.end(), "-fdata-sections"), + flags.end()) << spec.triple; + } + EXPECT_GT(rows, 0); +} From 2bcc0cd09f4f3622fd4463354f6085c2d67499c4 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:18:50 +0800 Subject: [PATCH 2/8] docs: sync the target tables in README and docs/16 with the seven new rows Both mirror kKnownTargets and would otherwise describe a four-row bare-metal table that no longer exists. The README's legend for a verified row is that CI builds AND executes the artifact, which the baremetal job now does for the four rows marked so. --- README.md | 5 +++++ docs/16-the-target-triple.md | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index 2c660390..01335697 100644 --- a/README.md +++ b/README.md @@ -382,6 +382,11 @@ the right toolchain payload is resolved and installed automatically. | `aarch64-macos` | llvm *(macOS default)* | ✅ | | `riscv64-none-elf` | llvm 22 — bare metal, no OS; needs no per-host cross payload ² | ✅ | | `riscv32-none-elf` | llvm 22 — bare metal, no OS; needs no per-host cross payload ² | ✅ | +| `thumbv6m-none-eabi` | llvm 22 — Cortex-M0/M0+/M1, bare metal ² | ✅ | +| `thumbv7m-none-eabi` | llvm 22 — Cortex-M3, bare metal ² | ✅ | +| `thumbv7em-none-eabihf` | llvm 22 — Cortex-M4F/M7F, hard float ² | ✅ | +| `thumbv8m.main-none-eabi` | llvm 22 — Cortex-M33/M55, soft float ² | ✅ | +| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 22 — builds and links; no emulator run recorded | 🔄 | | `riscv64-linux-musl` | — | 🔄 | | `aarch64-linux-gnu` | — | 🔄 | | `x86_64-macos` | — | 🔄 | diff --git a/docs/16-the-target-triple.md b/docs/16-the-target-triple.md index bd884e1e..a5137790 100644 --- a/docs/16-the-target-triple.md +++ b/docs/16-the-target-triple.md @@ -428,6 +428,13 @@ other's rows. | `riscv32-none-elf` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | | `aarch64-none-elf` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | | `x86_64-none-elf` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv6m-none-eabi` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv7m-none-eabi` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv7em-none-eabi` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv7em-none-eabihf` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv8m.base-none-eabi` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv8m.main-none-eabi` | verified | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | +| `thumbv8m.main-none-eabihf` | preview | `llvm@22.1.8` | ✅ payload | ✅ payload | ✅ payload | ✅ payload | `✅ payload` a toolchain payload here produces it · `⚙ graph` no payload, but a dependency can supply the system · `✅ system` located on the machine, not From be5242e453839c05367de7d3beba7d253395f5d4 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:29:40 +0800 Subject: [PATCH 3/8] test(e2e): register qemu-arm in KNOWN_CAPS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The harness validates every `# requires:` token against a master list before running anything, so a capability that is only added to the Linux detection branch aborts every suite on every platform. CI caught it: the msvc job failed with "declares unknown capability 'qemu-arm'" while the bare-metal job — the one that has the emulator — passed. That is the guard working. A token absent from this list would otherwise match nothing and the test would skip everywhere while looking supported. --- tests/e2e/run_all.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/run_all.sh b/tests/e2e/run_all.sh index 38a26edc..9a6eb8f0 100755 --- a/tests/e2e/run_all.sh +++ b/tests/e2e/run_all.sh @@ -221,8 +221,8 @@ echo "Detected capabilities: ${CAPS[*]:-}" # CAPS+=() calls above by tests/e2e/README or by reading them -- keep it in # sync when adding a capability. KNOWN_CAPS=(elf fresh-sandbox gcc import-std-libcxx jq llvm macos mingw - mingw-cross msvc musl nasm no-msvc pack patchelf python3 qemu-riscv - scan-deps symlink unix-shell windows wine xlings-msvc) + mingw-cross msvc musl nasm no-msvc pack patchelf python3 qemu-arm + qemu-riscv scan-deps symlink unix-shell windows wine xlings-msvc) bad_tokens=0 for tf in "$HERE"/[0-9]*.sh; do From b1b2e1bc5a71eafe0f6d8d9f33f2508b8fb0a5aa Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 4 Sep 2026 01:40:33 +0800 Subject: [PATCH 4/8] =?UTF-8?q?docs(plan):=20=E5=95=86=E4=B8=9A=E7=BA=A7?= =?UTF-8?q?=E8=A3=B8=E6=9C=BA/=E5=B5=8C=E5=85=A5=E5=BC=8F=E6=80=BB?= =?UTF-8?q?=E4=BD=93=E6=96=B9=E6=A1=88,=E5=B9=B6=E5=9B=9E=E5=A1=AB=20P0=20?= =?UTF-8?q?=E7=9A=84=E5=AE=9E=E6=96=BD=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 方案文档此前只存在于工作副本里,没有进入 PR —— 而它是本次改动的依据,读者要靠 它才能知道七个目标行与 --gc-sections 为什么是同一批。 §10 记下实施推翻方案的两处:extra 列承载的是 -mfpu=none 而不是方案写的 -mfloat-abi(triple 已经承载了浮点 ABI),以及行数是七不是六。 --- ...ommercial-grade-baremetal-embedded-plan.md | 473 ++++++++++++++++++ 1 file changed, 473 insertions(+) create mode 100644 .agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md diff --git a/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md b/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md new file mode 100644 index 00000000..95cf9a0d --- /dev/null +++ b/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md @@ -0,0 +1,473 @@ +# 商业级可用:mcpp × xlings 的裸机与嵌入式总体方案 + +2026-09-04 · 多仓库总体方案 · **v4:P0 引擎切片已实施**(PR #550);其余批次待实施 + +前置讨论: +[`2026-08-21-baremetal-ecosystem-assessment.md`](2026-08-21-baremetal-ecosystem-assessment.md)(七角度评估) · +[`2026-08-20-baremetal-user-facing-scenarios.md`](2026-08-20-baremetal-user-facing-scenarios.md)(场景) · +[`2026-07-24-embedded-platform-support-design.md`](2026-07-24-embedded-platform-support-design.md)(issue #276) · +[`2026-08-27-openkal-ecosystem-design-plan.md`](2026-08-27-openkal-ecosystem-design-plan.md)(生态接口判据) + +> ⭐ **只想看结论的读者直接去 §8**:十三条决定,每条附判据与它推翻了上一版的哪一句。 +> §2 是唯一的新设计,其余各章是数据与归属。 + +--- + +## 0. 一句话,与本文的骨架 + +> **裸机与嵌入式的"极致体验"不是功能的总和,是「一件事只说一次,并且说在唯一知道 +> 它的那一层」。** + +所以本文不是一张功能清单,而是一张**归属表**。每一条待办先过同一个三问,再决定它 +落在哪个仓库。 + +### 0.1 三侧划分的判据 + +| 侧 | 它回答的问题 | 判别问法 | 契约形式 | +|---|---|---|---| +| **mcpp 引擎** | 对**所有**工程都一样的决定,或**只有引擎同时知道两边的地址** | 「换一块板、换一个厂商,这条会变吗?」**会变 ⇒ 不在这侧** | 表 + 单一读点 | +| **build.mcpp 插件(包)** | 属于**某块板 / 某个库**的决定,消费者不该重述 | 「谁知道这件事?」**板知道 ⇒ 在这侧** | directive 协议 + `import mcpp` 类型化 API | +| **xlings 生态** | 一个**要被装到机器上的东西** | 「它是个产物,还是个决定?」**产物 ⇒ 这侧** | xim 描述符 + `xpm.<平台>.deps` | + +⭐ **这三条不是新发明的,是把仓库里已经在用的三条规则合成一句。** + +* `docs/13`:*location is a target fact, selection is a board fact* +* `docs/14`:五层 target side × 四种来源(`payload` / `prebuilt` / `graph` / 缺席) +* openkal SPEC clause 10 的基数(openarch 一个 · openkal 每接口一个 · openhal 多个) + +⚠️ **判据的反面同样承重,方案里每一条都要过它**:**一个决定被两侧同时表达,不会在 +加进去的那天失败,会在加下一条语义时变成构建失败。** 这个代价这个仓库付过四次 +(#233 / #240 / #242 / #344)。凡是本文里出现「引擎也知道一点、包也知道一点」的条目, +都必须重写到只剩一侧。 + +### 0.2 「商业级可用」的六个轴 + +不给判据的「商业级」是口号。本文把它拆成六条,每条都能被证伪: + +| 轴 | 判据(一句话) | 今天 | +|---|---|---| +| **A 覆盖** | 目标机器能不能编出正确的目标文件 | 裸机四行,**Cortex-M 缺席** | +| **B 可信** | 判据是否产生在能观察到该现象的环境里 | **全部来自 QEMU**,零真机 | +| **C 闭环** | 从 `new` 到烧录、调试、看串口是不是一条命令链 | **烧录/调试/串口三项完全缺席** | +| **D 可复现** | 同一份输入,两次、两地、两台机器得到同一产物 | ⚠️ **`mcpp.lock` 自己的头注释:"does not yet pin future builds"** —— 它是记录不是钉 | +| **E 可交付** | 企业采购会问的:SBOM、许可、离线内网、支持窗口 | **近乎空白**(`sbom`/`spdx` 全仓零命中) | +| **F 可扩展** | 第三方加一块板 / 一个厂商,引擎 diff 为零 | ⭐ **今天最强的一条,方案要保护它** | + +--- + +## 1. 资产负债表(实测,2026-09-04) + +### 1.1 已经就位,且比预期多 + +| | 实测 | +|---|---| +| 引擎已模块化 | `modules/` 九个包(`buildmcpp` `manifest` `toolchain-model` `platform` `dyndep` `libs` `log` `source-kind` `versioning`) | +| 目标侧模型 | `docs/14` 五层 × 四来源;`hosted-standard-library` 之后**裸机可以有完整 C++**(异常、RTTI、`import std`、`filesystem`) | +| 插件面 | 15 条 directive × 7 种 scope(`PackagePrivate` `LinkGlobal` `SourceSet` `RunGlobal` `RerunKey` `Advisory` `GraphNode`)+ 类型化 `import mcpp` + `action{}` 图节点 + `dep_bin` 宿主工具 | +| 模拟器 | ⭐ **`xim:qemu-arm@9.2.4-1` 已带 `qemu-system-arm`**,机器表实测覆盖 M0(`microbit`)/ M3(`mps2-an385`)/ M4(`mps2-an386`,`b-l475e-iot01a`)/ M7(`mps2-an500`)/ M33(`mps2-an505`,双核 `an521`)/ M55(`mps3-an547`)。**已发布、已装机、一次未用** | +| triple 解析 | 已认识 `eabi`/`eabihf` 且带 `envExplicit`;`normalize_arch` 直通,`cfg(arch=…)` 无封闭词表 | +| ISA 表的列 | `extra` 列(x86_64 为 `-mno-red-zone` 加的)**正好是 `-mfloat-abi`/`-mfpu` 的形状** | +| 运行槽 | `Slot::Runner` + `Scope::RunGlobal`:板级包逐 token 拼 argv,工程可覆盖且被报告 | +| 离线 | `--offline` → `MCPP_OFFLINE` | + +### 1.2 缺口,按侧分 + +| 侧 | 缺什么 | +|---|---| +| **引擎** | Cortex-M 目标行族;`flash`/`monitor`/`debug` 三个槽;**runner 的独占语义**(§2.3);**freestanding 链接无 `--gc-sections`**(实测全仓零命中,§3.1.1);lock 的钉住语义;SBOM 输出 | +| **插件(包)** | ARM 板级包零;真机板级包零 | +| **xlings** | ⚠️ **187 个包里 `openocd`/`probe-rs`/`pyocd`/`gdb`/`dfu-util`/`esptool`/`jlink` 零命中** —— 整个真机工具层不存在。(ARM 的 C 库**不走这一侧**,见 §3.1.1) | +| **判据** | 真机 CI 不存在;裸机 + `import std` 在 mcpp 侧只有 unit test,e2e 在生态仓 | +| **文档** | `docs/13` 停在 `2026.8.21.3`,其「异常关闭 / `import std` 不可用」两条已被 `2026.8.28.2` 推翻 | + +--- + +## 2. C 轴是重心 —— 但比 v1 想的小得多 + +这是全文唯一需要**新设计**的地方。v1 把它写成"三个新槽 + 三种进程语义",自审后 +两条判断把它缩小了一半。 + +### 2.1 ⭐⭐ 真机上的 `mcpp run` / `mcpp test` 今天就不需要引擎改动 + +`probe-rs run --chip ` 一个进程做完:烧录、复位、接 RTT/半主机、**把固件 +经半主机 `SYS_EXIT` 报的退出码作为自己的退出码**。这与 `qemu-system-* -semihosting` +的形状**逐字段相同** —— 都是「一次性、读退出码」。 + +⇒ 真机板级包只需把 `runner` 槽填成 `probe-rs run --chip STM32L475VGTx {}`, +**`mcpp run` 就在真机上工作**,引擎零 diff;`mcpp test` 也一样,**前提是一次只跑一个** +—— 那个前提今天要工程自己记住,§2.3 把它交给板级包。 + +⚠️ **这条推翻了 v1 的分批**:v1 把"引擎槽表"放在真机之前。实际上通往真机 +`mcpp test` 的关键路径是 **xlings 的 `probe-rs` 包 + 一个真机板级包**,槽表是 +**附加**的(`flash` 只烧不跑、`monitor`、`debug`),不在关键路径上。 + +### 2.2 三个新槽,两种语义 —— 不是三种 + +v1 给 `debug` 发明了第三种语义 `Paired`(server + 客户端,交互式,tty 透传)。 +自审:**那是把 IDE 的活揽进 mcpp**。`probe-rs gdb` / `openocd` 起的是一个 **server**; +连上去的 gdb / DAP 客户端属于 VS Code、CLion、用户的终端 —— mcpp 已经有一条给 IDE +的机器接口(`docs/11`),边界就画在那里。 + +| 槽 | 语义 | 引擎做的事 | +|---|---|---| +| `run` | `OneShot` | (已有)读退出码 | +| `flash` | `OneShot` | 读退出码;**读回校验是工具的事,退出码承载它** | +| `monitor` | `LongLived` | 起进程、透传 stdio、把 Ctrl-C 交给它、**不把"永不退出"当失败** | +| `debug` | `LongLived` | 同上,并在机器输出通道上发一行 `{ "gdb_port": …, "elf": … }` 供 IDE 连接 | + +⇒ **`Semantics ∈ {OneShot, LongLived}`,两个值**。`debug` 在两值语义下与 `monitor` +同形,**移回 P1**;v1 把它推到 P2' 的理由(可能要会话协议)随边界一起消失。 + +### 2.3 ⚠️ 真机带来一列模拟器从来不需要的:独占 + +实测 `execute.cppm:2022`:`mcpp test` 用一个 **`workers` 大小的线程池**并发跑测试 +二进制。QEMU 可以开 N 个实例;**一块物理板是一把互斥锁** —— 两个 `probe-rs` 抢一个 +探针,结果不是失败而是**乱**。 + +⇒ 板级包发一条 `mcpp:runner-exclusive=1`(`Scope::RunGlobal`,布尔),引擎在运行 +阶段把 workers 钳到 1。**板知道自己是独占的,工程不该记得 `-j1`**。这是 x86_64 +长出 `extra` 列的同一形状:第一块真板挖出的不是缺陷,是表少一列。 + +### 2.4 线协议:三行,不是一种新语法 + +v1 写了 `mcpp:device-action=:`。自审:那是在 directive 表旁边**发明第二 +套语法**。`runner` 当初就是表里**一行**,新槽照做: + +``` +表: {"flash", …, Slot::Flash, Scope::RunGlobal, …} 语义 OneShot + {"monitor", …, Slot::Monitor, Scope::RunGlobal, …} 语义 LongLived + {"debug", …, Slot::Debug, Scope::RunGlobal, …} 语义 LongLived + {"runner-exclusive", …, Scope::RunGlobal, …} 布尔 +类型化: mcpp::flash(tok) · mcpp::monitor(tok) · mcpp::debug(tok) · mcpp::runner_exclusive() +覆盖: [target.].flash = [...] 与 runner 同轴、同一句 note +命令: mcpp flash · mcpp monitor · mcpp debug 同一个读点 +``` + +⚠️ 每加一条 directive 要改的位置是九处(`build-mcpp-extensibility-architecture` +记的),这是已知代价,不是新代价。 + +### 2.5 三侧分工 + +| 侧 | 负责 | 例 | +|---|---|---| +| **引擎** | 四行表、语义列、单一读点、覆盖与 note、命令族、产物集(`.elf` `.bin` **`.hex`** `.map`) | | +| **板级包** | 每个槽的 argv 与条件降级 | `probe-rs run --chip STM32L475VGTx {}` · `probe-rs download --chip … --verify {}` · `probe-rs gdb --chip …` | +| **xlings** | 把工具装到机器上 | `xim:probe-rs`(P0')· `xim:openocd`(P2)· `xim:arm-none-eabi-gdb` | + +⚠️ **今天第三列一个都没有** ⇒ xlings 侧**先行**,不是并行。 + +--- + +## 3. 逐轴方案 + +### 3.1 A 覆盖:Cortex-M 目标行族 + +| 侧 | 事项 | 备注 | +|---|---|---| +| 引擎 | `kKnownTargets` + `freestanding::kTable` 加 **7** 行:`thumbv6m-eabi` `thumbv7m-eabi` `thumbv7em-eabi` `thumbv7em-eabihf` `thumbv8m.base-eabi` `thumbv8m.main-eabi` `thumbv8m.main-eabihf`(均 `-none-`) | `lldEmulation` **留空**(clang 有 arm 的 BareMetal 工具链);`mcmodel` **留空**;`-mfloat-abi`/`-mfpu` 走已存在的 `extra` 列;`mabi = "aapcs"` 同 aarch64 行 | +| 引擎 | 表形状:**7 行,不引入 `-mcpu` 第二轴** | 判据在表自己的注释里:「让 `--target ` 单独足够产出正确的目标文件」。`thumbv6m` 与 `thumbv7em` 的目标文件互不兼容 ⇒ 是行不是偏好 | +| 引擎 | ⚠️ **`-mfpu` 的默认值要实测不要类推**:`thumbv7em-none-eabihf` 不给 `-mfpu` 时 clang 选什么,用 `-###` 问机器 | aarch64 行 `lp64`→`aapcs` 的教训:表按类比填,会错在类比最强的那格 | +| 引擎 | `libdir` 与 `sysroot` 列:**7 行全部留空** | 图来源没有多库目录;列只在 sysroot 存在时被读(§3.1.1) | +| 引擎 | ⭐ **freestanding 编译加 `-ffunction-sections -fdata-sections`,链接加 `--gc-sections`** | 实测全仓零命中。依赖的目标文件**无条件进链接**(`docs/13:423`),没有它图来源的 C 库会把整份 picolibc 放进每个 MCU 镜像 | +| 引擎 | 分级:**2 行 `verified`**(`v6m-eabi`、`v7em-eabihf`,各有板)**5 行 `preview`** | 与 aarch64/x86_64 落地时相同 | +| **mcpp-index** | ⭐ **`mcpplibs/picolibc`:源码包**,`provides = ["mcpp:c-abi=picolibc"]`;**不建 `xim:picolibc-arm`** | §3.1.1。上游 zip(实测 API:`picolibc-arm-none-eabi-1.8.12-15.3.rel1.zip`,Arm GNU 15.3/GCC 编)无论如何不可用 —— `docs/14` 规则拒绝 llvm 下的 libgcc;而"自建 7 个多库"是 prebuilt 来源自带的成本 | +| **mcpp-index** | `mcpplibs/compiler-rt-builtins`:源码包,`provides = ["mcpp:compiler-runtime=compiler-rt"]`;`picolibc` 依赖它 | 只 vendor `compiler-rt/lib/builtins`,目标无关。M0 无硬件除法(`__aeabi_idiv`)、软浮点全套、ryu 的 128 位移位全在这里 | + +**工作量**:引擎侧 7 行数据 + 两个 flag。C 库是源码包,主要工作是 vendor + 生成配置 +(`openkal-musl` 的 `musl-generated/`、`openkal-llvm-runtime` 的 `llvm-generated/` 是现成先例)。 + +#### 3.1.1 ⭐ 为什么 C 库走 mcpp-index 而不是 xim 载荷(review 第一轮的追问,已决定) + +`docs/14` 原文:*"Moving a layer from a prebuilt payload into the dependency graph +removes engine work rather than adding it. This is the mechanism by which one source +reaches several platforms without an engine change."* + +v2 初稿为 prebuilt 来源列出的工作,逐条对照: + +| prebuilt 来源要做的 | 图来源 | +|---|---| +| 7 个多库各建一次 | **没有多库**:C 库用与程序完全相同的 `compile_flags` 编译,`-mfpu`/浮点 ABI 一致是**按构造**成立的 | +| `libdir` 列与包目录逐字节对上(#481 的形状) | **列为空** | +| builtins 与 C 库同包,否则第一次 printf 挂 | 两个包,`picolibc` 依赖 `compiler-rt-builtins`,由五层规则「配置于其下的层」表达 | +| 五宿主镜像、`.sha256` 侧文件、CDN 传播等待 | 一个源码 tarball,宿主无关 | +| 版本钉在目标表里,**在 lock 之外** | 进 `mcpp.lock`(D 轴) | +| 每个架构一个包(`picolibc-riscv/aarch64/x86` 已是三个) | **一个包服务全部 11 行裸机目标** | + +代价与前提: + +* ⚠️ **首次构建付编译一次 picolibc 的墙钟**。全局依赖缓存(`docs/05 §2.10`, + `$MCPP_HOME/build-cache/v1/`,跨工程、按工具链 × profile × 版本键)使它是**每台机器 + 每个目标档一次**。第一天的探针里量它。 +* ⚠️⚠️ **前提是 `--gc-sections`**。依赖的目标文件无条件进链接(`docs/13:423`),而全仓 + 没有任何 `gc-sections`/`function-sections`(实测零命中)。hosted 目标不在乎,QEMU 的 + 128 MB RAM 也不在乎;**64 KB flash 的 MCU 放不下整份 picolibc**。这是图来源的 C 库在 + MCU 上暴露的第一条引擎缺口,也是任何商业 MCU 工具链的标配。 + ⚠️ 随之而来的:向量表没有任何引用,`--gc-sections` 会把它删掉 —— 板级链接脚本必须 + `KEEP(*(.vectors))`;判据是镜像能启动,不是链接退 0。 +* 板级包不再 `link_lib("c")`,改为**依赖** `picolibc`;链接脚本走 picolibc 的 `build.mcpp` + 发 `link-search`(LinkGlobal)+ 板级 `board.ld` 里 `INCLUDE picolibc.ld` —— picolibc + 上游设计的用法,零新 API。 +* `sysroot_dir()` 对图来源返回空;板级包按 `aarch64-virt-rt` 已有的"缺席"分支处理。 +* ⚠️ 这与 `docs/13` 记的「板级包不该绑定 libc」不冲突:那条针对的是 `[xlings] deps` + 里**按架构分的 prebuilt** 绑定;源码依赖是架构无关的。 + +⭐ **判据**(同时是三个既有 prebuilt 载荷要不要迁移的判据):同一份半主机 hello, +riscv64 上用 `mcpplibs/picolibc` 与用 `xim:picolibc-riscv` 各链一次,**`Size text` 相当且 +输出相同**。差得多 ⇒ gc-sections 没生效或配置生成错了;相当 ⇒ 三个 prebuilt 包之后 +可以各自退休。这也是 mcpp 自己的 e2e 里**第一个"图来源 c-abi 上裸机目标"**的用例 +(今天零个,实测 `tests/e2e` 无 `c-abi=`)。 + +### 3.2 板的选择(v1 的问题 2,已决定) + +判据:两块板要在**每一条会影响判据的轴上都不同**,并且其中一块要**同时存在于 QEMU +与真机** —— 那样同一个板级包有两个观察环境,「模拟器 vs 真机」这条差异本身成为可测的。 + +| | 板 A | 板 B | +|---|---|---| +| 板 | **B-L475E-IOT01A**(ST) | **Raspberry Pi Pico**(RP2040) | +| 核 / 行 | Cortex-M4F · `thumbv7em-none-eabihf` | Cortex-M0+ · `thumbv6m-none-eabi` | +| 浮点 / 除法 | 硬浮点、硬除法 | **软浮点、无除法指令** —— builtins 判据在这块板上 | +| 探针 | 板载 ST-LINK v2-1 | CMSIS-DAP(debugprobe / 第二块 Pico) | +| 启动 | flash 直接执行 | **ROM bootloader + 镜像内 256 字节 boot2**,另有 UF2 拖放 | +| 核数 | 单核 | 双核 | +| QEMU | ⭐ **有**(`b-l475e-iot01a`,QEMU 9.0+ 的 stm32l4x5 模型) | 无 | + +⇒ P0(仅 QEMU)用 **`b-l475e-iot01a`(M4F)+ `microbit`(M0,nRF51,QEMU 有)** +两台机器,覆盖两个浮点档;P1'(真机)用 **B-L475E-IOT01A + Pico**。`b-l475e-iot01a-rt` +一个包服务 P0 与 P1' 两行 —— 这是本方案里判据最强的一处。 + +⚠️ **第一天的探针**:stm32l4x5 的 QEMU 模型较新,先确认 +`qemu-system-arm -machine b-l475e-iot01a -semihosting -kernel hello.elf` 能打印。 +不能 ⇒ P0 回落到 `mps2-an385`(M3),板 A 的 QEMU 行留到模型修好。 + +⚠️ **不选 mps2-an385 做主板的理由**:它是 FPGA 原型板,没人拥有;一个只在 QEMU 里 +存在的板级包永远得不到真机那一行。 + +### 3.3 B 可信:真机判据 + +⭐ **第一条判据不是「测试通过」,是「产物真的进了 flash」**:`probe-rs download --verify` +读回比对,退出码承载。「烧录命令退 0」与「芯片里是这份镜像」是两件事(`.mcpp_ok` +只证进程退 0 的同形)。 + +⚠️ **HIL 会长成 flaky,而 flaky 判据比没有判据更糟**(它训练所有人重跑)。 +⇒ 真机 job 从第一天起分开报告「探针没找到」与「测试失败」,前者不染红 PR。 +`probe-rs list` 在跑测试前作分母。 + +### 3.4 D 可复现 —— 比 v1 写的更严重 + +`mcpp.lock` 头注释原文:*"It does not yet pin future builds: index dependencies are +re-resolved from their constraints each time."* ⇒ **今天没有任何东西钉住一次解析**; +`--locked` 不是"加一个 flag",是**先让 lock 成为钉,再有 flag 可以校验它**。 + +| 侧 | 事项 | 判据 | +|---|---|---| +| 引擎 | lock 从记录升级为钉:再次解析**先读 lock**,不符才走约束;`--locked` 时不符即失败 | 改 lock 后 `mcpp build` 用的版本随之变 | +| 引擎 | lock 覆盖**工具链与载荷**(今天 pin 在目标表里,不在 lock 里) | 换机器拿到同一个 llvm | +| 引擎 | `-ffile-prefix-map`,产物不嵌绝对路径与时间戳 | **两台机器两个目录,sha256 相同** | +| xlings | 索引可钉到 commit 而非 `latest` | 「消费者先发布,`latest` 才能动」的解药 | + +⚠️ 可复现的判据**必须跨机器**。同机两次相同只证明缓存工作。 + +### 3.5 E 可交付(v1 的问题 3,已决定:按依赖拆,不按阶段排) + +| 依赖 | 事项 | 放哪 | +|---|---|---| +| **零工程** | 支持窗口与破坏性变更通知期(书面) | **P0'**,与 P0 并行 | +| **零工程** | 传递闭包的许可清单(文档 + `mcpp explain` 已有的图) | P0' | +| **一条输出格式** | `mcpp sbom`(SPDX / CycloneDX):图与版本 mcpp 全知道 | P0' | +| **工程** | `--deny-license` 门 | P2 | +| **工程** | 整仓离线快照一条命令(索引 + 载荷 + 包 → 可拷进内网的目录) | P2 | +| **文档** | 回填 `docs/13` 两条失效限制 | **P0**,本周 | + +v1 按工程依赖把 E 整个排到 P2。自审:**其中一半没有工程依赖**,排后面只是惯性。 + +### 3.6 F 可扩展:保护今天最强的一条 + +⭐ **加一块板 = 加一个包,引擎 diff 为零。** 本方案的判据:P0 与 P1' 的三个板级包 +(`b-l475e-iot01a-rt` / `microbit-rt` / `pico-rt`)**每一个**都必须在 mcpp 引擎零 diff +的前提下落地(§2 的四行表除外 —— 那是一次性的,且三个包共用)。 + +写成 CI 可检查的:板级包仓库的 PR **不得**要求新版 mcpp(除非显式声明); +`if constexpr (requires { mcpp::x(…) })` 对不存在的限定名是**硬错误**,所以降级靠 +引擎侧那条「编译失败且错误含『不是 `mcpp` 的成员』则追加升级提示」的补偿,新增的 +`flash`/`monitor`/`debug` 也靠它。 + +--- + +## 4. 多仓库分工总表 + +| 仓库 | 角色 | 主要条目 | +|---|---|---| +| **mcpp** | 表、槽、单一读点、命令族、lock 语义、SBOM | 7 行 thumb;freestanding `--gc-sections`;`flash`/`monitor`/`debug`/`runner-exclusive` 四条 directive + 语义列;`.hex`;lock 成钉 + `--locked`;`mcpp sbom`;`docs/13` 回填;裸机 `import std` 的 mcpp 侧 e2e | +| **openxlings/xim-pkgindex** | 工具 | **`probe-rs`**;`arm-none-eabi-gdb`;`openocd`(P2);离线快照 | +| **mcpplibs/mcpp-index** | 登记与模板 + **目标侧源码包** | 三个板级包描述符;**`picolibc`、`compiler-rt-builtins` 两个新源码包**(§3.1.1);模板随包走 | +| **新建 ×3:板级包** | 槽 + 链接脚本 + 启动 | `b-l475e-iot01a-rt`(QEMU + 真机)· `microbit-rt`(QEMU)· `pico-rt`(真机,含 boot2) | +| **mcpplibs/openarch** | 机器机制层 | 第四后端 **aarch32(A/R)先于 Cortex-M**(§5 P3) | +| **openkal ***、`std-freestanding` | 语言与环境层 | 无新结构;随目标增加回归 | +| **文档** | `docs/13` 回填、`docs/18-devices`(新)、支持窗口声明 | | + +--- + +## 5. 分批清单 + +每批带**判据**,判据不成立则该批不算完成。 + +### 第一天的探针(任何批开始前) + +1. `qemu-system-arm -machine b-l475e-iot01a -semihosting` 能否打印(决定 §3.2 的回落) +2. `clang --target=thumbv7em-none-eabihf -###` 的默认 `-mfpu`(填表) +3. `probe-rs` 发布二进制的 `LD_TRACE_LOADED_OBJECTS=1` 闭包(决定它是不是"自带库载荷") +4. picolibc 从源码经 mcpp 编译一遍的墙钟,以及**第二个工程**是否命中全局缓存(决定首次 + 构建体验;判据是第二个工程的 `Cached` 行与墙钟,不是第一个的) + +### P0 —— 能编 Cortex-M + +1. 引擎:7 行(2 verified + 5 preview);**freestanding `-ffunction-sections -fdata-sections` + + `--gc-sections`**;`docs/13` 回填;裸机 `import std` 的 mcpp 侧 e2e +2. mcpp-index:`mcpplibs/picolibc` + `mcpplibs/compiler-rt-builtins` 源码包(§3.1.1) +3. 包:`b-l475e-iot01a-rt`、`microbit-rt` +4. **判据**:两个模板 `mcpp new && mcpp run` 在 CI 的 qemu 里打印;断言 PASS 行; + **microbit 上一次浮点 printf + 一次整数除法**(M4F 那行对 builtins 缺口是假绿); + **riscv64 上图来源与 prebuilt 的 `Size text` 相当、输出相同**(§3.1.1 的判据) + +### P0' —— 与 P0 并行 + +5. xlings:`xim:probe-rs`(五宿主、双镜像、`.sha256` 侧文件) +6. E 轴零工程项:支持窗口声明、许可清单、`mcpp sbom` +7. **判据**:干净机器 `xlings install probe-rs -y` 后 `probe-rs list` 有输出且闭包不越界 + +### P1 —— 真机(关键路径:§2.1) + +8. 包:`pico-rt`;`b-l475e-iot01a-rt` 加真机 `runner`(`probe-rs run`) +9. 引擎:`runner-exclusive`(§2.3)—— **唯一在真机关键路径上的引擎改动** +10. 自托管 runner + 两块板;探针缺席与测试失败分开报告 +11. **判据**:`mcpp test` 在两块板上读到退出码;`probe-rs download --verify` 读回通过 + +### P1' —— 闭环槽 + +12. 引擎:`flash` / `monitor` / `debug` 三行 + 语义列;`.hex` +13. 包:三个板级包填三个槽 +14. **判据**:`mcpp debug` 发出的 `{gdb_port, elf}` 能被一个 gdb 连上并停在 `main` + +### P2 —— 可交付与第二实现 + +15. lock 成钉 + `--locked`;跨机器 sha256 判据 +16. `--deny-license`;离线快照 +17. xlings:`xim:openocd` —— **同一板级包契约的第二个实现**,这本身是槽抽象的判据 + +### P3 —— openarch 第四后端 + +18. **aarch32(Cortex-A/R 32 位)优先**:14 个 ABI 函数一个不缺(CP15 的 + `TPIDRPRW`/`TPIDRURW` 恰是两个指针槽;真 MMU;`VBAR`;DMB/DSB/ISB),且它是第一台 + 32 位机器,会挖出 `arch_pte_make_leaf` 返回 `arch_u64` 这条没被问过的宽度假设 +19. **Cortex-M 后端在其后**,因为要先回答**规范问题**:openarch 的可行性闸是「上下文 + 切换 + 页表项」两件,M-profile 没有 MMU、没有页表项,只满足一件。14 个函数里 + 5 个可写、4 个不可能(`pte_*`)、5 个语义改变(`trap_set_handler`;`percpu`/`tls` + 四个 —— 无 TPIDR 类寄存器,退化成全局变量后「两个槽是不同的」变成恒真)。 + ⇒ 先决定**接口允不允许部分后端** + +### P3' —— 嵌入式 Linux(issue #276) + +20. `[target.*].sysroot` 接受外部路径(今天走 `parse_xpkg_ref`);pkg-config +21. 硬边界不变:target 侧必须支持 C++23 modules(GCC ≥ 15);方案是 config② + (mcpp 自带匹配 libc 的交叉编译器 + 只经 sysroot 取库) + +--- + +## 6. 会踩的坑(取自本仓库已付过的代价) + +| # | 坑 | 本方案哪一条会踩 | +|---|---|---| +| 1 | **判据必须放在能观察到该现象的环境里,矩阵的每一行是一个独立观察环境** | 真机 job、qemu job:按**行**穷举 | +| 2 | **`# requires:` 的 e2e 在 shard 上可能一次都没跑过**,`run_all.sh` 跳过退 0 | 守卫住在 job 里,断言 PASS 行 | +| 3 | **判据的「否」与「没测成」同读数** | 烧录校验、闭包检查:带**分母**(`probe-rs list`) | +| 4 | **声明 ≠ 安装**,`[xlings] deps` 不是安装触发器 | probe-rs 的安装边写在索引描述符 `xpm.<平台>.deps`;判据「拿走再装回来」 | +| 5 | **带 `build.mcpp` 的包必须 Form A** | 三个板级包 | +| 6 | **索引陈旧有五层**,第五层是 xlings 消费 `artifact:`;判据是 `Publish Index Artifact` 在那个 commit 上绿 | 发布后等 ~10 分钟再跑依赖它的 CI | +| 7 | **发布形态 ≠ 开发形态**:生态 CI 全用工作树替换 ⇒ 冲突不存在 | 沙箱解析**已发布**包是唯一验发布物的路径 | +| 8 | **消费者先发布,`latest` 才能动** | `picolibc-arm` / 板级包的发布顺序 | +| 9 | **裸名工具经 PATH 解析到属于另一个 home 的 shim** | 所有槽的 argv 绝对路径 | +| 10 | **自带库载荷 + 声明 loader 依赖 ⇒ elfpatch 整条替换 RPATH,装完即坏** | `openocd`(libusb/hidapi/ftdi)是这一类;`probe-rs` 第一天的探针就是为了确认它不是 | +| 11 | ⚠️ **Windows 上 `GenerateConsoleCtrlEvent` 打到整个控制台**,job object 才是机制 | `LongLived` 槽的 Ctrl-C 归属 —— `monitor`/`debug` 在 Windows 上的第一个缺陷会是这个 | +| 12 | **`pipefail` + `grep -q` 把匹配成功读成 141** | 新脚本用 `grep -c` | +| 13 | **依赖缓存曾经"同进程命中、跨进程付全价"**,`Cached` 一行不是判据 | picolibc 的首次构建体验:判据是**第二个工程**的墙钟 | +| 14 | **`--gc-sections` 会删掉没被引用的向量表** | 板级链接脚本 `KEEP(*(.vectors))`;判据是镜像能启动 | + +--- + +## 7. 明确不做的 + +* **L4 build backend**(mcpp 作 Yocto/Buildroot 后端)—— 2026-07-24 维护者决策,不改 +* **消费厂商预编译 C++ 库** —— 「C++ 只源码,二进制只 C-ABI + 封装」 +* **mcpp 驱动 gdb / DAP 客户端** —— 那是 IDE 的边界(§2.2) +* **ESP32 / Xtensa** —— 非 LLVM 上游后端 +* **Cortex-M 的 openarch 后端在规范决定之前落地**(§5 P3) + +--- + +## 8. 十三条决定(自审十条 + review 第一轮三条) + +| # | 决定 | 判据 | 推翻了 v1 的哪一句 | +|---|---|---|---| +| D1 | **真机 `mcpp run`/`mcpp test` 走现有 `runner` 槽,引擎零 diff** | `probe-rs run` = 烧录 + 复位 + 半主机退出码,一个进程,与 qemu `-semihosting` 同形 | "P1 引擎槽表在真机之前" | +| D2 | **语义列两个值 `{OneShot, LongLived}`,不是三个** | `debug` 只起 server,客户端归 IDE(`docs/11` 已有那条边界) | "`debug` 是 `Paired`,可能要会话协议" | +| D3 | **`debug` 回到 P1'**,与 `monitor` 同批 | D2 之后它与 `monitor` 同形 | "`debug` 推到 P2'" | +| D4 | **线协议 = directive 表加四行**,不发明 `device-action=:` | `runner` 当初就是一行;第二套语法是第二个解析器 | v1 §2.1 的语法 | +| D5 | **新增 `runner-exclusive`** | 实测 `execute.cppm:2022` 线程池并发跑测试;板是互斥锁 | v1 没有它 —— 第一块真板挖出的那一列 | +| D6 | **`probe-rs` 先于 `openocd`**,后者 P2 作第二实现 | 单二进制、五宿主预编译、内置芯片库、`run` 一步到位;openocd 是"自带库载荷"形状 | "openocd / probe-rs 并列" | +| D7 | **C 库进 mcpp-index 作源码包(`picolibc` + `compiler-rt-builtins`),不建 `xim:picolibc-arm`** | `docs/14`:层从 prebuilt 移进图是**减**引擎工作;多库、`libdir`、五宿主镜像全是 prebuilt 来源自带的成本;上游 zip(GCC 15.3 编)无论如何不可用 | v2 "自建 7 个多库"(review 追问后改判) | +| D8 | **板 = B-L475E-IOT01A + Pico;QEMU 行 = `b-l475e-iot01a` + `microbit`** | 每条轴都不同(核/浮点/探针/启动/核数),且板 A 同时在 QEMU 与真机 | "mps2-an385 开路" | +| D9 | **E 轴按依赖拆**:零工程项进 P0',工程项留 P2 | 支持窗口/许可清单/`sbom` 没有工程前置 | "E 整体在 P2" | +| D10 | **D 轴升级严重度**:lock 先成钉,再有 `--locked` | lock 头注释原文 "does not yet pin future builds" | "缺的是 `--locked` flag" | +| D11 | **freestanding 加 `-ffunction-sections -fdata-sections` + `--gc-sections`**,作 D7 的前提 | 依赖目标文件无条件进链接(`docs/13:423`)+ 全仓无 gc-sections(实测)⇒ 图来源 C 库会撑爆 MCU flash | v1/v2 都没有 —— 图来源在 MCU 上暴露的第一条引擎缺口 | +| D12 | 自托管 runner **归板级包仓**,mcpp 仓只留模拟器行 | 判据跟着包走;`openkal-llvm-runtime` 自跑 riscv64 qemu 的先例 | (v2 §9 问题 1,维护者已定) | +| D13 | **不做 `.uf2`**;`pico-rt` 的 `flash` 走 `probe-rs download` | RP2040 一家的格式 | (v2 §9 问题 2,维护者已定) | + +## 9. 待决项 + +无。v2 §9 的两个问题已定为 D12、D13;D7 经追问改判,前提为 D11。 + +--- + +## 10. 实施状态(2026-09-04,PR #550) + +### 已落地:P0 的引擎切片 + +| 条目 | 状态 | +|---|---| +| Cortex-M 目标行 | ✅ **七行**,`kKnownTargets` 与 `freestanding::kTable` 各七行 | +| D11 死代码段消除 | ✅ `-ffunction-sections -fdata-sections` + `--gc-sections`(驱动路径与直接 lld 路径) | +| `docs/13` 回填两条失效限制 | ✅ 中英双份 | +| 判据 | ✅ e2e 332(四行在 QEMU 中启动)+ 三条单测;CI `bare-metal e2e` 作业已绿 | + +### ⚠️ 实施推翻的两处方案原文 + +**① `extra` 列不承载浮点 ABI —— triple 已经承载了它。** +方案 §3.1 写「`-mfloat-abi`/`-mfpu` 走已存在的 `extra` 列」。实测 llvm 22.1.8: +clang 从 `eabi`/`eabihf` 后缀**自行导出** `-mfloat-abi`,无需引擎发任何标志。 + +⭐ **但同一次实测挖出方案没有预见的一条**:浮点 ABI 只约束浮点值如何跨越函数边界, +**不约束函数内部发什么指令**。`thumbv7em` 架构蕴含 FPv4-SP,于是软浮点 ABI 下 +clang 对一次 float 乘法仍发出 `vmul.f32` —— 在没有 FPU 的 Cortex-M4 上于运行期 +触发异常,而编译与链接都是干净的。`extra` 列因此确实被用上了,承载的是 `-mfpu=none`, +不是方案写的那两个标志。 + +**② 七行不是六行。** 方案 §3.1 列了六行(三个 arch × eabi/eabihf 的子集);实际是 +七行 —— `thumbv8m.base` 没有 `eabihf` 变体(M23 无 FPU),而 `thumbv7em` 与 +`thumbv8m.main` 各有两个。 + +### ⚠️ 实施中发现的、方案没有的一条纪律 + +`-mfpu=none` 第一版只加在**实测到非零 FPU 指令数**的行上。那让表记录的是一次 +**测量**而不是一条**保证**,并且各行的差异没有任何读者能看出来。对所有行量化的 +单元测试报出了它。⇒ **一行陈述它保证的性质,而不是从一个可以改变的默认值继承它。** + +### 未实施(按方案原顺序) + +`P0` 的 `mcpplibs/picolibc` + `compiler-rt-builtins` 源码包(§3.1.1)· 板级包三个 · +`P0'` 的 `xim:probe-rs` 与 E 轴零工程项 · `P1`/`P1'` 真机 · `P1'` 槽表 · +`P2` lock 成钉与 SBOM · `P3` openarch 第四后端 · `P3'` 嵌入式 Linux。 + +⚠️ **软浮点行在没有 builtins 时链接不了浮点代码**(实测:`undefined symbol: +__aeabi_fmul`),这正是 §3.1.1 把 `compiler-rt-builtins` 与 C 库并列为 P0 的理由。 +整数程序不受影响 —— e2e 332 的四行启动用例即为整数程序。 From df1c1f40c19a1ce66ed49d2b02f0f824b4ffac97 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 4 Sep 2026 02:15:40 +0800 Subject: [PATCH 5/8] revert(freestanding): drop dead-section elimination from this change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠️ MEASURED TO BREAK A JOB THAT IS GREEN ON main, BY A MECHANISM THAT CONTRADICTS THE CODE, WHICH IS REASON ENOUGH NOT TO SHIP IT. `--gc-sections` was correct on this machine and in the bare-metal CI job. It broke the openkal cross-build on a WINDOWS host: lld: error: unable to find library -lntdll (and four more) Three-way control, not reasoning: main, via workflow_dispatch build 3 targets on windows ✅ this PR, with gc-sections (three runs) ❌ bisect/no-gc-sections, that change alone reverted ✅ And the result contradicts the model. Both call sites sit inside `isFreestandingTarget`, which `x86_64-windows-gnu` does not satisfy; the same cross-build succeeds on a Linux host; and the `-Wl,--gc-sections` visible in the failing link line was traced to openkal-windows' own manifest rather than to the engine. The actual symptom is a MISSING `-L` for the directory holding the import libraries that package generates. So the mechanism is not understood, and an unexplained change does not belong in a release. The flags are removed here and the finding is recorded in `.agents/docs/2026-09-04-…-plan.md` §10.1 with the control that establishes it, because dead-section elimination remains a prerequisite for a C library supplied from the dependency graph. The Cortex-M rows do not depend on it: they build, link and boot without it, and the e2e still asserts the vector table is present and the float ABI is honoured on both sides. Only the two dead-code assertions are withdrawn. --- ...ommercial-grade-baremetal-embedded-plan.md | 33 ++++++- CHANGELOG.md | 9 +- docs/13-baremetal.md | 89 ------------------- docs/zh/13-baremetal.md | 76 ---------------- src/freestanding/linkline.cppm | 8 -- src/freestanding/target.cppm | 24 ----- tests/e2e/332_cortex_m_builds_and_boots.sh | 30 ++----- tests/unit/test_freestanding.cpp | 16 ---- 8 files changed, 42 insertions(+), 243 deletions(-) diff --git a/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md b/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md index 95cf9a0d..b4b0971a 100644 --- a/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md +++ b/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md @@ -436,10 +436,39 @@ v1 按工程依赖把 E 整个排到 P2。自审:**其中一半没有工程依 | 条目 | 状态 | |---|---| | Cortex-M 目标行 | ✅ **七行**,`kKnownTargets` 与 `freestanding::kTable` 各七行 | -| D11 死代码段消除 | ✅ `-ffunction-sections -fdata-sections` + `--gc-sections`(驱动路径与直接 lld 路径) | +| D11 死代码段消除 | ❌ **撤出本 PR** —— 实测打断 Windows 宿主上的 openkal 交叉构建,见 §10.1 | | `docs/13` 回填两条失效限制 | ✅ 中英双份 | | 判据 | ✅ e2e 332(四行在 QEMU 中启动)+ 三条单测;CI `bare-metal e2e` 作业已绿 | +### ⚠️⚠️ 10.1 D11 被实测挡住,而挡住它的机制尚未查清 + +`--gc-sections` 在本机(Linux 宿主)与 CI 的裸机作业上都正确;它打断的是 +**openkal 在 Windows 宿主上的交叉构建**: + +``` +lld: error: unable to find library -lntdll +lld: error: unable to find library -lkernel32 (等五条) +``` + +判据是三次对照,不是推理: + +| 分支 | `build 3 targets on windows` | +|---|---| +| `main`(workflow_dispatch 对照) | ✅ | +| 本 PR(含 gc-sections,三次) | ❌ | +| `bisect/no-gc-sections`(只去掉 gc-sections) | ✅ | + +⚠️ **而这条与我的机制模型矛盾**:两处 `gc-sections` 都在 `isFreestandingTarget` +之内,`x86_64-windows-gnu` 不该进那个分支;本机 Linux→Windows 交叉**能成功**,且 +失败的链接行里的 `-Wl,--gc-sections` 经核对来自 **openkal-windows 自己的清单** +而不是引擎。失败的真正现象是链接行**缺了 `-L`** —— +那个目录里正是它生成的 `libntdll.a` 等导入库。 + +⇒ **测量胜过模型**:一个我解释不了的改动不该合入。D11 退回待办,并带上这条判据 —— +它同时是「图来源 C 库」那条路线必须先解决的前置。⭐ 附带发现:openkal 的包**已经 +在自己的清单里用 `-ffunction-sections` 与 `-Wl,--gc-sections`**,所以引擎侧是否 +需要再发一份,本身也要重新论证。 + ### ⚠️ 实施推翻的两处方案原文 **① `extra` 列不承载浮点 ABI —— triple 已经承载了它。** @@ -464,6 +493,8 @@ clang 对一次 float 乘法仍发出 `vmul.f32` —— 在没有 FPU 的 Cortex ### 未实施(按方案原顺序) +**D11 死代码段消除**(见 §10.1,已从本 PR 撤出,带判据)· + `P0` 的 `mcpplibs/picolibc` + `compiler-rt-builtins` 源码包(§3.1.1)· 板级包三个 · `P0'` 的 `xim:probe-rs` 与 E 轴零工程项 · `P1`/`P1'` 真机 · `P1'` 槽表 · `P2` lock 成钉与 SBOM · `P3` openarch 第四后端 · `P3'` 嵌入式 Linux。 diff --git a/CHANGELOG.md b/CHANGELOG.md index d242dd51..eb7e4811 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ## [2026.9.4.1] — 2026-09-04 -Cortex-M 落地为七个目标行,freestanding 链接开启死代码段消除。 +Cortex-M 落地为七个目标行。 裸机目标表从四行增至十一行。M-profile 是七行而不是一行:为 `thumbv7em` 构建的 目标文件使用 Cortex-M0 没有的指令,两种拼写产出互不兼容的目标文件,而表存在的 @@ -23,13 +23,6 @@ target = "thumbv7em-none-eabihf" 因此携带 `-mfpu=none`,包括架构本来就没有 FPU 的那几行 —— 一行陈述它保证的性质, 而不是从一个可以改变的默认值继承它。 -freestanding 编译加 `-ffunction-sections -fdata-sections`、链接加 `--gc-sections`。 -依赖的目标文件无条件进入链接(不像归档成员那样按未定义符号拉取),当 C 库改由 -依赖图提供时,没有死代码段消除的镜像会装进整份 C 库,而 Cortex-M 器件只有几十 KB。 - -⚠️ **链接脚本因此以新的方式承重**:中断向量表不被任何东西引用,`--gc-sections` -会回收它,板级脚本必须写 `KEEP(*(.vectors))`。 - 同时回填了 `docs/13` 中两条已被 2026.8.28.2 推翻的限制:当图中有包提供 `hosted-standard-library` 时,裸机目标上的异常、RTTI 与 `import std` 均可用。 diff --git a/docs/13-baremetal.md b/docs/13-baremetal.md index 4c44ae5c..d64acc1a 100644 --- a/docs/13-baremetal.md +++ b/docs/13-baremetal.md @@ -59,95 +59,6 @@ Cortex-M needs no `lldEmulation` column entry: clang has a *BareMetal* toolchain for arm, so these triples reach `ld.lld` through the driver as the RISC-V and aarch64 rows do. 32-bit ARM has no `-mcmodel` axis, so that column is empty too. -### Dead-section elimination - -Freestanding builds compile with `-ffunction-sections -fdata-sections` and link -with `--gc-sections`. Both halves belong to the engine rather than to a project -because a dependency's translation units must carry them, and a project cannot -reach those. - -The flags became necessary rather than merely economical when a C library began -arriving from the dependency graph. A dependency's object files enter the link -unconditionally, unlike an archive member, which is pulled only while its symbol -is undefined. That costs nothing when the C library is a prebuilt archive and -the target has megabytes; a Cortex-M part has kilobytes, and without dead-section -elimination every image would carry the whole of the C library. - -⚠️ **A linker script becomes load-bearing in a new way.** An interrupt vector -table is referenced by nothing — the hardware reads it by address — so -`--gc-sections` collects it. A board's script must say `KEEP(*(.vectors))`. -Measured: with the `KEEP` present, a function nothing calls is dropped, the -table survives, and the image boots. - -⚠️ The last two rows default to no C library, and that is a statement rather -than an omission: the first consumer of both rows — the `openarch` layer of -machine mechanism — references no C library symbol, and if no row defaulted to -this tier there would be nothing demonstrating the tier works. -⭐ **A build for those rows is declarable, not absent** (mcpp 2026.8.21.3+). -`xim:picolibc-aarch64` and `xim:picolibc-x86` are in the index; a project that -wants one names it the same way it would choose a different one: - -```toml -[target.aarch64-none-elf] -sysroot = "xim:picolibc-aarch64@1.8.12" -``` An empty column means exactly what -`[target.].sysroot = ""` means in a manifest, so a project targeting one -begins on the zero-libc tier without asking. A project that wants a C library on -those targets declares one, which is also how it would choose a different one. - -Such a target needs no per-host cross toolchain. clang and lld are -cross-compilers by construction — one binary emits every target it was built -with — so the target table pins `llvm@22.1.8` on every host, and any machine -that can install the LLVM payload can produce an image for any of the four. - -### The x86_64 row is not four strings - -⚠️ **A target row is normally an entry in two tables and nothing else. This one -needed engine code, and the reason is a property of clang rather than of the -instruction set.** - -clang selects a toolchain from the triple. It has a *BareMetal* toolchain for -arm, aarch64 and riscv, which links with `ld.lld` directly; it has none for -x86_64, so every spelling of a bare x86_64 triple falls through to the generic -GCC toolchain — whose linker is the **host's `g++`**: - -``` -g++: error: unrecognized command-line option '-fuse-ld=/…/llvm/22.1.8/bin/ld.lld' -``` - -Measured for `x86_64-none-elf`, `x86_64-unknown-none-elf`, `x86_64-unknown-none`, -`x86_64-elf`, `x86_64-none-none` and `x86_64-unknown-unknown`, and unchanged by -`-fuse-ld=lld`, `--ld-path=`, `--gcc-toolchain=` or `-B`. The one thing that -does change it is putting `linux` in the OS position, which makes clang link -directly and adds eight host `-L` paths to a bare-metal link. - -Neither outcome is acceptable: routing through a host `g++` makes the row work -on a Linux host and nowhere else, and host search paths on a freestanding link -are the hermeticity this engine exists to keep. So the row carries a fifth -column, `lldEmulation`, and when it is set the engine drives the link with -`ld.lld` itself. The flag vocabulary changes with the tool — `-Map=` rather than -`-Wl,-Map=`, `-m elf_x86_64` rather than `--target=` — and the driver-only flags -(`-nostdlib++`, the loader tag) are dropped rather than translated. - -The column is empty for the riscv and aarch64 rows. Their driver already reaches -lld, and changing a working link to make three rows look alike is how a -regression is introduced. - -### `-mno-red-zone` is part of the target, not a preference - -The System V x86-64 ABI reserves 128 bytes below `rsp` that a leaf function may -use without adjusting the stack pointer, because on a hosted system nothing else -writes there. On bare metal the processor pushes an interrupt frame at `rsp` — -into the red zone — and the interrupted leaf resumes to find its locals -overwritten. There is no fault and no diagnostic, and it happens only when an -interrupt arrives inside a leaf. - -There is no bare-metal x86_64 program for which the red zone is safe, so the -flag is a property of the row rather than something a project remembers. It -reaches the command line through a new `extra` column in the ISA-profile table, -which exists because `-march`/`-mabi`/`-mcmodel` could not express it. RISC-V -and aarch64 have no equivalent, which is why the column did not exist before. - Three things a bare-metal build requires are not properties of the ISA, and mcpp does not attempt to derive them: which startup object and libraries to select, which linker script describes the machine's memory, and how to execute diff --git a/docs/zh/13-baremetal.md b/docs/zh/13-baremetal.md index ee57def5..47c6e5ec 100644 --- a/docs/zh/13-baremetal.md +++ b/docs/zh/13-baremetal.md @@ -52,82 +52,6 @@ Cortex-M 不需要 `lldEmulation` 列:clang 有面向 arm 的 *BareMetal* 工具 triple 与 RISC-V、aarch64 各行一样经驱动到达 `ld.lld`。32 位 ARM 没有 `-mcmodel` 这个轴,所以该列同样为空。 -### 死代码段消除 - -freestanding 构建以 `-ffunction-sections -fdata-sections` 编译,以 `--gc-sections` -链接。两半都属于引擎而不属于工程,因为依赖的翻译单元也必须带上它们,而工程够不到 -那些单元。 - -这两个标志从「划算」变成「必需」,发生在 C 库开始由依赖图提供的时候。依赖的目标 -文件是**无条件**进入链接的,不像归档成员那样只在符号仍未定义时才被拉入。当 C 库是 -预编译归档、目标又有若干兆字节时,这不花什么代价;而 Cortex-M 器件只有几十 KB, -没有死代码段消除,每个镜像都会装进整份 C 库。 - -⚠️ **链接脚本因此以一种新的方式承重。** 中断向量表不被任何东西引用 —— 硬件按地址 -读取它 —— 所以 `--gc-sections` 会把它回收。板级脚本必须写 `KEEP(*(.vectors))`。 -实测:有这条 `KEEP` 时,无人调用的函数被丢弃、向量表被保留、镜像能够启动。 - -⚠️ 后两行**默认**没有 C 库,这是声明而非遗漏:这两行的第一个消费者 —— 机器机制层 -`openarch` —— 一个 C 库符号都不引用,而**如果四行里没有一行默认在这一层,就没有 -任何东西在证明这一层可用**。空列在这里的含义与清单里 -`[target.].sysroot = ""` 完全一致。 - -⭐ **这两行的 C 库是可声明的,不是不存在的**(mcpp 2026.8.21.3+)。 -`xim:picolibc-aarch64` 与 `xim:picolibc-x86` 已在索引里;想要它的工程自行声明, -而那与它换用另一份 C 库是同一个动作: - -```toml -[target.aarch64-none-elf] -sysroot = "xim:picolibc-aarch64@1.8.12" -``` - -这类目标不需要逐宿主的交叉工具链。clang 与 lld 在构造上就是交叉编译器 —— -一个二进制发射它构建时支持的全部目标 —— 因此目标表在每个宿主上都钉 -`llvm@22.1.8`,任何能安装 LLVM 载荷的机器都能为这四个中的任何一个产出镜像。 - -### x86_64 这一行不止是四个字符串 - -⚠️ **一个目标行通常就是两张表里的两条记录,不含引擎代码。这一行需要引擎代码, -而原因是 clang 的属性而非指令集的属性。** - -clang 由 triple 选择工具链。它为 arm、aarch64、riscv 备有 *BareMetal* 工具链, -直接以 `ld.lld` 链接;它没有 x86_64 的,于是裸 x86_64 triple 的每一种写法都落到 -通用 GCC 工具链上 —— 而后者的链接器是**宿主的 `g++`**: - -``` -g++: error: unrecognized command-line option '-fuse-ld=/…/llvm/22.1.8/bin/ld.lld' -``` - -对 `x86_64-none-elf`、`x86_64-unknown-none-elf`、`x86_64-unknown-none`、 -`x86_64-elf`、`x86_64-none-none`、`x86_64-unknown-unknown` 逐一实测,结果一致; -`-fuse-ld=lld`、`--ld-path=`、`--gcc-toolchain=`、`-B` 逐一实测,均不改变结果。 -唯一能改变它的是把 `linux` 放进 OS 位,而那会给一次裸机链接带来八条宿主 `-L`。 - -两种结果都不可接受:经宿主 `g++` 会让这一行只在 Linux 宿主上成立;宿主搜索路径 -出现在 freestanding 链接上,正是本引擎存在的意义所要守住的封闭性。因此该行带了 -第五列 `lldEmulation`,置位时引擎直接用 `ld.lld` 驱动链接。标志的词汇随工具一同 -改变 —— `-Map=` 而非 `-Wl,-Map=`,`-m elf_x86_64` 而非 `--target=` —— 而仅属于 -驱动的标志(`-nostdlib++`、加载器标签)是被丢弃而不是被翻译。 - -riscv 与 aarch64 两行的该列为空。它们的驱动本就到得了 lld,为了让三行看起来一致 -而改动一条可用的链接,正是引入回归的方式。 - -### `-mno-red-zone` 是目标的属性,不是偏好 - -System V x86-64 ABI 在 `rsp` 之下保留 128 字节,叶函数可以不调整栈指针直接使用, -因为在有操作系统的机器上没有别的东西会写那里。裸机上不存在做这件事的一方:处理器 -在 `rsp` 处压入中断帧 —— 正压进红区 —— 被中断的叶函数恢复后发现自己的局部变量已被 -覆盖。既不触发异常也没有诊断,而且只在中断恰好落在叶函数内部时发生。 - -不存在红区安全的裸机 x86_64 程序,所以这个标志是行的属性,而不是工程需要记住的 -事情。它经 ISA 档表新增的 `extra` 列到达命令行 —— 该列之所以存在,是因为 -`-march`/`-mabi`/`-mcmodel` 表达不了它。RISC-V 与 aarch64 没有对应物,这也正是 -此前不需要这一列的原因。 - -裸机构建需要的三样东西并不是 ISA 的属性,mcpp 也不试图推导它们:选哪个启动对象 -与哪些库、哪份链接脚本描述这台机器的内存、以及如何执行产出的镜像。这三样随 -**板级支持包**一起提供,而板级支持包是一个普通依赖。由此得到的结果是:换板子是 -一次依赖变更,而不是一次构建系统变更。 ## 从零到一个可运行的镜像 diff --git a/src/freestanding/linkline.cppm b/src/freestanding/linkline.cppm index d0c873c8..913a9c09 100644 --- a/src/freestanding/linkline.cppm +++ b/src/freestanding/linkline.cppm @@ -89,9 +89,6 @@ inline std::string link_flags(const Spec& s, const LinkInputs& in, // suggesting it does. for (auto const& f : compile_flags(s)) { out += ' '; out += f; } out += " -nostdlib -nostartfiles -static"; - // The link half of the pair set in mcpp.freestanding.target's - // `compile_flags`. Routed through the driver, hence `-Wl,`. - out += " -Wl,--gc-sections"; if (!in.lld.empty()) out += " -fuse-ld=" + esc(in.lld); // ⚠️ BEFORE the libraries the board selects, and it has to be on THIS line @@ -132,11 +129,6 @@ inline std::string link_flags_direct(const Spec& s, const LinkInputs& in, // No loader exists, so an image must not name one. The driver line reaches // this through `-static`; here it is said to the linker. out += " --no-dynamic-linker"; - // Said to the linker directly here, as `-Wl,--gc-sections` is said through - // the driver on the line above. The two paths must agree: a target whose - // link is driven by `ld.lld` itself would otherwise keep every section its - // dependencies emit, and the difference would show only as size. - out += " --gc-sections"; if (!in.sysrootLib.empty()) out += " -L" + esc(in.sysrootLib); if (!in.linkerScript.empty()) diff --git a/src/freestanding/target.cppm b/src/freestanding/target.cppm index b545194e..8e3bc2ec 100644 --- a/src/freestanding/target.cppm +++ b/src/freestanding/target.cppm @@ -291,30 +291,6 @@ inline std::vector compile_flags(const Spec& s, // `-ffreestanding` so the ordering of this function's output stays a // function of the table rather than of the row. for (auto flag : s.extra) out.emplace_back(flag); - // ⭐⭐ ONE SECTION PER FUNCTION, SO THE LINKER CAN DROP WHAT NOTHING CALLS. - // - // These two flags do nothing on their own; they are the half of - // `--gc-sections` that has to happen at compile time, and the link half is - // in mcpp.freestanding.linkline. Both halves are here rather than left to a - // project because a dependency's translation units must carry them too, and - // a project cannot reach those. - // - // ⚠️ WHY THIS BECAME NECESSARY RATHER THAN MERELY NICE. A dependency's - // object files enter the link unconditionally (docs/13), unlike an archive - // member, which is pulled only while its symbol is undefined. That costs - // nothing when the C library is a prebuilt archive and the target has - // megabytes — the arrangement every bare-metal row had until now. A C - // library that arrives from the dependency graph is object files, and a - // Cortex-M part has kilobytes: without this, every image carries the whole - // of the C library whether or not it calls into it. - // - // ⚠️ AND IT MAKES A LINKER SCRIPT LOAD-BEARING IN A NEW WAY: an interrupt - // vector table is referenced by nothing — the hardware reads it by address - // — so `--gc-sections` collects it. A board's script must say - // `KEEP(*(.vectors))`. Measured: with the KEEP present, a dead function is - // dropped and the table survives; the image boots. - out.emplace_back("-ffunction-sections"); - out.emplace_back("-fdata-sections"); // ⭐ AND `-ffreestanding` ITSELF IS ONE OF THE THINGS THE GRAPH DECIDES. // // The paragraph above this function names what the flag changes: "no `main` diff --git a/tests/e2e/332_cortex_m_builds_and_boots.sh b/tests/e2e/332_cortex_m_builds_and_boots.sh index 6535d107..2080fe70 100755 --- a/tests/e2e/332_cortex_m_builds_and_boots.sh +++ b/tests/e2e/332_cortex_m_builds_and_boots.sh @@ -1,16 +1,9 @@ #!/usr/bin/env bash # requires: llvm unix-shell qemu-arm -# Cortex-M: the M-profile rows build, boot, and collect what nothing calls. +# Cortex-M: the M-profile rows build and boot, and honour the float ABI. # -# ⚠️ THE ROWS THAT MATTER HERE ARE THE ONES A REASONED TABLE WOULD HAVE GOT -# WRONG. Two properties are asserted that a build alone cannot show: -# -# * `--gc-sections` reaches a bare-metal link. Without it a C library that -# arrives from the dependency graph — object files, which enter a link -# unconditionally — puts its whole self into every image, and a Cortex-M -# part has kilobytes. The assertion is that a function nothing calls is -# ABSENT from the image while the interrupt vector table, which nothing -# references either, SURVIVES because the script says KEEP. +# ⚠️ THE PROPERTY THAT MATTERS HERE IS THE ONE A REASONED TABLE WOULD HAVE GOT +# WRONG: # # * the soft-float rows do not emit FPU instructions. `thumbv7em`'s # architecture implies an FPU, so clang emits `vmul.f32` for a float @@ -70,17 +63,14 @@ inline void sh(int op, const void* a) { } extern "C" unsigned __stack_top; -// Referenced by nothing. The image must not contain it. -extern "C" void collected_because_nothing_calls_it() { sh(0x04, (void*)"UNREACHABLE\n"); } - extern "C" void Reset_Handler() { sh(0x04, (void*)"cortex-m ok\n"); sh(0x18, (void*)0x20026); // semihosting SYS_EXIT, ADP_Stopped_ApplicationExit for (;;) {} } -// Referenced by nothing either — the hardware reads it by address. It survives -// only because the linker script says KEEP. +// Referenced by nothing — the hardware reads it by address, which is why the +// linker script says KEEP. extern "C" __attribute__((section(".vectors"), used)) void* const vectors[] = { (void*)&__stack_top, (void*)Reset_Handler }; CPP @@ -119,12 +109,10 @@ boot_row() { # triple machine cpuflag flash_org flash_len ram_org r elf=$(find target -type f -name mcu | head -1) [ -n "$elf" ] || { echo "FAIL: $triple produced no artefact"; exit 1; } - # gc-sections: the dead function is gone, the vector table is not. - if [ "$("$NM" "$elf" | grep -c collected_because_nothing_calls_it)" != "0" ]; then - echo "FAIL: $triple kept a function nothing calls (--gc-sections not applied)"; exit 1 - fi + # The vector table is present: it is what the machine fetches at reset, and + # an image without it boots into nothing. if [ "$("$NM" "$elf" | grep -c vectors)" != "1" ]; then - echo "FAIL: $triple lost the vector table (KEEP not honoured)"; exit 1 + echo "FAIL: $triple has no vector table"; exit 1 fi local out @@ -196,4 +184,4 @@ case "$soft_out" in echo "$soft_out" | tail -5 | sed 's/^/ /'; exit 1 ;; esac -echo "PASS: cortex-m rows build, boot, collect dead code and honour the float ABI" +echo "PASS: cortex-m rows build, boot and honour the float ABI" diff --git a/tests/unit/test_freestanding.cpp b/tests/unit/test_freestanding.cpp index 7d2cdcba..ca84d72b 100644 --- a/tests/unit/test_freestanding.cpp +++ b/tests/unit/test_freestanding.cpp @@ -529,19 +529,3 @@ TEST(FreestandingTarget, MProfileRowsNeedNoCodeModelAndNoDirectLldDriving) { } EXPECT_GT(rows, 0); } - -// The compile half of `--gc-sections`, which has to hold for EVERY freestanding -// target rather than only the M-profile ones: a dependency's translation units -// carry these flags too, and a project cannot reach those. -TEST(FreestandingTarget, EveryRowCompilesWithPerFunctionSections) { - int rows = 0; - for (const auto& spec : mcpp::freestanding::known()) { - ++rows; - auto flags = mcpp::freestanding::compile_flags(spec); - EXPECT_NE(std::find(flags.begin(), flags.end(), "-ffunction-sections"), - flags.end()) << spec.triple; - EXPECT_NE(std::find(flags.begin(), flags.end(), "-fdata-sections"), - flags.end()) << spec.triple; - } - EXPECT_GT(rows, 0); -} From 097954ed995c61999903538f204efcad9c2f5780 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 4 Sep 2026 02:39:45 +0800 Subject: [PATCH 6/8] test(matrix): the support matrix declares the seven M-profile rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⭐ AND THE SCAN'S OWN PREDICATE WAS A SPELLING RATHER THAN A PROPERTY, IN TWO PLACES. `scan.sh` decided "is this a bare-metal target" with `grep -- '-none-elf$'`. That is the environment field of four particular rows, not the question being asked. Cortex-M spells the same OS `-none-eabi` and `-none-eabihf`, so both copies of the rule fell through: * the probe source picked the hosted branch and emitted `#include ` for a target with no C library, producing a column of `build-failed` that described the probe rather than the target — precisely the defect the comment three lines above it already records; * the graph-mode skip did not skip, so fourteen cells were measured for a combination the same file declares out of scope. Both now ask the OS field. This is the shape recorded as "one rule, two copies: change one and you will miss the other", and it is why the rows are added by MEASURING with the fixed scan rather than by writing them out. The 42 payload rows are the live scan's output, verified by `compare.sh` for both modes. Graph mode contributes none, which is the declaration the file already made for every other bare-metal row. --- tests/matrix/expected.tsv | 178 +++++++++++++++++++++++--------------- tests/matrix/scan.sh | 20 ++++- 2 files changed, 128 insertions(+), 70 deletions(-) diff --git a/tests/matrix/expected.tsv b/tests/matrix/expected.tsv index 58344101..4260de3e 100644 --- a/tests/matrix/expected.tsv +++ b/tests/matrix/expected.tsv @@ -40,39 +40,106 @@ # linux-aarch64 16 只有 musl-gcc —— llvm 在非 x86_64 Linux 上被显式延缓 # macos-arm64 20 只有 llvm × 12 目标(payload 12 / graph 8) # windows-x86_64 40 llvm + msvc@system +graph linux-aarch64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-aarch64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-aarch64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-aarch64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported host-cannot-serve +graph linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve +graph linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +graph linux-x86_64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-linux-gnu gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +graph linux-x86_64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported layer-requirement +graph linux-x86_64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin +graph linux-x86_64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +graph macos-arm64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 aarch64-macos llvm@22.1.8 arm64-apple-macos14.0 host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +graph macos-arm64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +graph macos-arm64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph macos-arm64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +graph windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +graph windows-x86_64 riscv64-linux-musl msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned +graph windows-x86_64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 x86_64-windows-msvc llvm@22.1.8 - - - - - unsupported other +graph windows-x86_64 x86_64-windows-msvc msvc@system - - - - - unsupported host-tool-toolchain +graph windows-x86_64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none +graph windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin payload linux-aarch64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned payload linux-aarch64 aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none payload linux-aarch64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned payload linux-aarch64 riscv64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv6m-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv7em-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv7em-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv7m-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv8m.base-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv8m.main-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-aarch64 thumbv8m.main-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported host-cannot-serve payload linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned payload linux-aarch64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve payload linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none -payload linux-x86_64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 riscv64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 x86_64-linux-gnu gcc@16.1.0 x86_64-unknown-linux-gnu subos gnu(payload) libstdc++(payload) - ok none -payload linux-x86_64 x86_64-linux-musl gcc@16.1.0 x86_64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none -payload linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -payload linux-x86_64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin -payload linux-x86_64 x86_64-windows-gnu gcc@16.1.0 x86_64-w64-windows-gnu none gnu(payload) libstdc++(payload) - ok none -payload linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned +payload linux-x86_64 aarch64-linux-musl gcc@16.1.0 aarch64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none payload linux-x86_64 aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux-x86_64 aarch64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 aarch64-none-elf llvm@22.1.8 aarch64-none-elf payload - - - ok none +payload linux-x86_64 riscv32-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 riscv32-none-elf llvm@22.1.8 riscv32-none-elf payload - - - ok none +payload linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned payload linux-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned +payload linux-x86_64 riscv64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 riscv64-none-elf llvm@22.1.8 riscv64-none-elf payload - - - ok none +payload linux-x86_64 thumbv6m-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv6m-none-eabi llvm@22.1.8 thumbv6m-none-eabi payload - - - ok none +payload linux-x86_64 thumbv7em-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv7em-none-eabi llvm@22.1.8 thumbv7em-none-eabi payload - - - ok none +payload linux-x86_64 thumbv7em-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv7em-none-eabihf llvm@22.1.8 thumbv7em-none-eabihf payload - - - ok none +payload linux-x86_64 thumbv7m-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv7m-none-eabi llvm@22.1.8 thumbv7m-none-eabi payload - - - ok none +payload linux-x86_64 thumbv8m.base-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv8m.base-none-eabi llvm@22.1.8 thumbv8m.base-none-eabi payload - - - ok none +payload linux-x86_64 thumbv8m.main-none-eabi gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv8m.main-none-eabi llvm@22.1.8 thumbv8m.main-none-eabi payload - - - ok none +payload linux-x86_64 thumbv8m.main-none-eabihf gcc@16.1.0 - - - - - unsupported capability-pin +payload linux-x86_64 thumbv8m.main-none-eabihf llvm@22.1.8 thumbv8m.main-none-eabihf payload - - - ok none +payload linux-x86_64 x86_64-linux-gnu gcc@16.1.0 x86_64-unknown-linux-gnu subos gnu(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload gnu(payload) libc++(payload) - ok none +payload linux-x86_64 x86_64-linux-musl gcc@16.1.0 x86_64-unknown-linux-musl payload musl(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned payload linux-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned +payload linux-x86_64 x86_64-none-elf gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 x86_64-none-elf llvm@22.1.8 x86_64-none-elf payload - - - ok none +payload linux-x86_64 x86_64-windows-gnu gcc@16.1.0 x86_64-w64-windows-gnu none gnu(payload) libstdc++(payload) - ok none payload linux-x86_64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced +payload linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin payload linux-x86_64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve payload macos-arm64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned payload macos-arm64 aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced @@ -81,78 +148,53 @@ payload macos-arm64 aarch64-none-elf llvm@22.1.8 aarch64-none-elf host - - - ok payload macos-arm64 riscv32-none-elf llvm@22.1.8 riscv32-none-elf host - - - ok none payload macos-arm64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned payload macos-arm64 riscv64-none-elf llvm@22.1.8 riscv64-none-elf host - - - ok none +payload macos-arm64 thumbv6m-none-eabi llvm@22.1.8 thumbv6m-none-eabi host - - - ok none +payload macos-arm64 thumbv7em-none-eabi llvm@22.1.8 thumbv7em-none-eabi host - - - ok none +payload macos-arm64 thumbv7em-none-eabihf llvm@22.1.8 thumbv7em-none-eabihf host - - - ok none +payload macos-arm64 thumbv7m-none-eabi llvm@22.1.8 thumbv7m-none-eabi host - - - ok none +payload macos-arm64 thumbv8m.base-none-eabi llvm@22.1.8 thumbv8m.base-none-eabi host - - - ok none +payload macos-arm64 thumbv8m.main-none-eabi llvm@22.1.8 thumbv8m.main-none-eabi host - - - ok none +payload macos-arm64 thumbv8m.main-none-eabihf llvm@22.1.8 thumbv8m.main-none-eabihf host - - - ok none payload macos-arm64 x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced payload macos-arm64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned payload macos-arm64 x86_64-none-elf llvm@22.1.8 x86_64-none-elf host - - - ok none payload macos-arm64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced payload macos-arm64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve payload windows-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced -payload windows-x86_64 aarch64-none-elf llvm@22.1.8 aarch64-none-elf none - - - ok none -payload windows-x86_64 riscv32-none-elf llvm@22.1.8 riscv32-none-elf none - - - ok none -payload windows-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 riscv64-none-elf llvm@22.1.8 riscv64-none-elf none - - - ok none -payload windows-x86_64 x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced -payload windows-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned -payload windows-x86_64 x86_64-none-elf llvm@22.1.8 - - - - - unsupported lld-required-absent -payload windows-x86_64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced -payload windows-x86_64 x86_64-windows-msvc llvm@22.1.8 x86_64-pc-windows-msvc none msvc(payload) msvc-stl(payload) - ok none -payload windows-x86_64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve payload windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 aarch64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced payload windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported convention-unreplaced +payload windows-x86_64 aarch64-none-elf llvm@22.1.8 aarch64-none-elf none - - - ok none payload windows-x86_64 aarch64-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 riscv32-none-elf llvm@22.1.8 riscv32-none-elf none - - - ok none payload windows-x86_64 riscv32-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned payload windows-x86_64 riscv64-linux-musl msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 riscv64-none-elf llvm@22.1.8 riscv64-none-elf none - - - ok none payload windows-x86_64 riscv64-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv6m-none-eabi llvm@22.1.8 thumbv6m-none-eabi none - - - ok none +payload windows-x86_64 thumbv6m-none-eabi msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv7em-none-eabi llvm@22.1.8 thumbv7em-none-eabi none - - - ok none +payload windows-x86_64 thumbv7em-none-eabi msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv7em-none-eabihf llvm@22.1.8 thumbv7em-none-eabihf none - - - ok none +payload windows-x86_64 thumbv7em-none-eabihf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv7m-none-eabi llvm@22.1.8 thumbv7m-none-eabi none - - - ok none +payload windows-x86_64 thumbv7m-none-eabi msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv8m.base-none-eabi llvm@22.1.8 thumbv8m.base-none-eabi none - - - ok none +payload windows-x86_64 thumbv8m.base-none-eabi msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv8m.main-none-eabi llvm@22.1.8 thumbv8m.main-none-eabi none - - - ok none +payload windows-x86_64 thumbv8m.main-none-eabi msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 thumbv8m.main-none-eabihf llvm@22.1.8 thumbv8m.main-none-eabihf none - - - ok none +payload windows-x86_64 thumbv8m.main-none-eabihf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 x86_64-linux-musl llvm@22.1.8 - - - - - unsupported convention-unreplaced payload windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported convention-unreplaced +payload windows-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned payload windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned +payload windows-x86_64 x86_64-none-elf llvm@22.1.8 - - - - - unsupported lld-required-absent payload windows-x86_64 x86_64-none-elf msvc@system - - - - - unsupported capability-pin +payload windows-x86_64 x86_64-windows-gnu llvm@22.1.8 - - - - - unsupported convention-unreplaced payload windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported convention-unreplaced +payload windows-x86_64 x86_64-windows-msvc llvm@22.1.8 x86_64-pc-windows-msvc none msvc(payload) msvc-stl(payload) - ok none payload windows-x86_64 x86_64-windows-msvc msvc@system x86_64-pc-windows-msvc none msvc(payload) (payload) - ok none +payload windows-x86_64 x86_64-windows-musl llvm@22.1.8 - - - - - unsupported host-cannot-serve payload windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin -graph linux-aarch64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-aarch64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux-aarch64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-aarch64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported host-cannot-serve -graph linux-aarch64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-aarch64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported host-cannot-serve -graph linux-x86_64 aarch64-linux-gnu gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 aarch64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux-x86_64 riscv64-linux-musl gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 x86_64-linux-gnu gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux-x86_64 x86_64-linux-musl gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux-x86_64 x86_64-macos gcc@16.1.0 - - - - - unsupported tier-planned -graph linux-x86_64 x86_64-windows-gnu gcc@16.1.0 - - - - - unsupported layer-requirement -graph linux-x86_64 x86_64-windows-musl gcc@16.1.0 - - - - - unsupported capability-pin -graph linux-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned -graph linux-x86_64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned -graph linux-x86_64 x86_64-linux-gnu llvm@22.1.8 x86_64-unknown-linux-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux-x86_64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned -graph linux-x86_64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph linux-x86_64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu payload musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned -graph macos-arm64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 aarch64-macos llvm@22.1.8 arm64-apple-macos14.0 host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned -graph macos-arm64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned -graph macos-arm64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph macos-arm64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu host musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph windows-x86_64 aarch64-linux-gnu llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 aarch64-linux-musl llvm@22.1.8 aarch64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph windows-x86_64 riscv64-linux-musl llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-linux-musl llvm@22.1.8 x86_64-unknown-linux-musl none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph windows-x86_64 x86_64-macos llvm@22.1.8 - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-windows-gnu llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph windows-x86_64 x86_64-windows-msvc llvm@22.1.8 - - - - - unsupported other -graph windows-x86_64 x86_64-windows-musl llvm@22.1.8 x86_64-w64-windows-gnu none musl(graph) libc++(graph) openkal-llvm-runtime@0.1.3 ok none -graph windows-x86_64 aarch64-linux-gnu msvc@system - - - - - unsupported tier-planned -graph windows-x86_64 aarch64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 riscv64-linux-musl msvc@system - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-linux-musl msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 x86_64-macos msvc@system - - - - - unsupported tier-planned -graph windows-x86_64 x86_64-windows-gnu msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 x86_64-windows-msvc msvc@system - - - - - unsupported host-tool-toolchain -graph windows-x86_64 x86_64-windows-musl msvc@system - - - - - unsupported capability-pin diff --git a/tests/matrix/scan.sh b/tests/matrix/scan.sh index c53f98c2..771d9592 100755 --- a/tests/matrix/scan.sh +++ b/tests/matrix/scan.sh @@ -154,7 +154,14 @@ for tc in $(compilers); do # `openkal-opensbi`(RISC-V 的监管者接口)并编译失败。 # # 裸机 × 图由 135/136/292 用正确的包覆盖。这里跳过并说出来。 - if [ "$MODE" = graph ] && printf '%s' "$t" | grep -q -- '-none-elf$'; then + # + # ⚠️ THE SAME PREDICATE AS THE PROBE BELOW, AND IT HAD THE SAME DEFECT. + # Written as `-none-elf$`, this skipped only the four targets that spell + # their environment `elf`; the Cortex-M rows spell it `eabi`/`eabihf`, so + # they were NOT skipped and produced fourteen graph cells describing a + # combination this paragraph says is out of scope. One rule, two copies — + # changing one and missing the other is how it failed. + if [ "$MODE" = graph ] && printf '%s' "$t" | grep -qE -- '-none-[a-z0-9]+$'; then echo "scan: 略过 graph × $t —— 裸机行不在这套依赖的论域内" >&2 continue fi @@ -170,7 +177,16 @@ for tc in $(compilers); do # 的失败 —— 零 libc 的目标本来就没有 ``,是探针问错了问题。 # # ⭐ 一个自己就编不过的探针,产出的整列都是关于探针的。 - if [ "$MODE" != graph ] && printf '%s' "$t" | grep -q -- '-none-elf$'; then + # + # ⚠️ AND THE PREDICATE IS THE OS FIELD, NOT A FILENAME SUFFIX. This read + # `-none-elf$` until 2026-09-04, which is a spelling rather than a property: + # the Cortex-M rows are `…-none-eabi` and `…-none-eabihf`, so they fell into + # the hosted branch, got `#include ` — and produced a whole column of + # `build-failed` describing the probe rather than the target. Exactly the + # defect the paragraph above records, reintroduced by matching a substring. + # + # A freestanding triple is `arch-none-env`; the OS field is what says so. + if [ "$MODE" != graph ] && printf '%s' "$t" | grep -qE -- '-none-[a-z0-9]+$'; then printf 'extern "C" void kmain() { for (volatile int i = 0; i < 1; ++i) {} }\n' > src/main.cpp printf '\n[target.%s]\nsysroot = ""\n' "$t" >> mcpp.toml else From a4ded89d7695c15a8e0421c15cdbf4c8a9f5f985 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:34:37 +0800 Subject: [PATCH 7/8] Revert "revert(freestanding): drop dead-section elimination" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The removal rested on a false signal. Reverting gc-sections made one CI run green, and that was taken as cause; the full control matrix says otherwise: main ×2 ✅ main + the seven rows ✅ main + the version bump ✅ this PR minus the version bump ✅ this PR, with AND without gc-sections, ×5 ❌ a branch holding BYTE-IDENTICAL code to the PR ✅ ← the decisive one The last row rules out every code-level explanation. The cause is GitHub Actions cache scoping: a branch reads its own cache before falling back to the default branch's, and this branch's first run — aborted by the unregistered `qemu-arm` capability — saved a half-populated sandbox that every later run on it then restored. Fresh bisect branches had no cache of their own and fell back to main's, which is why each of them passed and none of them exonerated anything. ⭐ Recorded in the plan document: when every single factor passes in isolation and the combination still fails, the next question is whether the criterion is being decided by the environment — not which factor to split next. --- ...ommercial-grade-baremetal-embedded-plan.md | 33 +------ CHANGELOG.md | 9 +- docs/13-baremetal.md | 89 +++++++++++++++++++ docs/zh/13-baremetal.md | 76 ++++++++++++++++ src/freestanding/linkline.cppm | 8 ++ src/freestanding/target.cppm | 24 +++++ tests/e2e/332_cortex_m_builds_and_boots.sh | 30 +++++-- tests/unit/test_freestanding.cpp | 16 ++++ 8 files changed, 243 insertions(+), 42 deletions(-) diff --git a/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md b/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md index b4b0971a..95cf9a0d 100644 --- a/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md +++ b/.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md @@ -436,39 +436,10 @@ v1 按工程依赖把 E 整个排到 P2。自审:**其中一半没有工程依 | 条目 | 状态 | |---|---| | Cortex-M 目标行 | ✅ **七行**,`kKnownTargets` 与 `freestanding::kTable` 各七行 | -| D11 死代码段消除 | ❌ **撤出本 PR** —— 实测打断 Windows 宿主上的 openkal 交叉构建,见 §10.1 | +| D11 死代码段消除 | ✅ `-ffunction-sections -fdata-sections` + `--gc-sections`(驱动路径与直接 lld 路径) | | `docs/13` 回填两条失效限制 | ✅ 中英双份 | | 判据 | ✅ e2e 332(四行在 QEMU 中启动)+ 三条单测;CI `bare-metal e2e` 作业已绿 | -### ⚠️⚠️ 10.1 D11 被实测挡住,而挡住它的机制尚未查清 - -`--gc-sections` 在本机(Linux 宿主)与 CI 的裸机作业上都正确;它打断的是 -**openkal 在 Windows 宿主上的交叉构建**: - -``` -lld: error: unable to find library -lntdll -lld: error: unable to find library -lkernel32 (等五条) -``` - -判据是三次对照,不是推理: - -| 分支 | `build 3 targets on windows` | -|---|---| -| `main`(workflow_dispatch 对照) | ✅ | -| 本 PR(含 gc-sections,三次) | ❌ | -| `bisect/no-gc-sections`(只去掉 gc-sections) | ✅ | - -⚠️ **而这条与我的机制模型矛盾**:两处 `gc-sections` 都在 `isFreestandingTarget` -之内,`x86_64-windows-gnu` 不该进那个分支;本机 Linux→Windows 交叉**能成功**,且 -失败的链接行里的 `-Wl,--gc-sections` 经核对来自 **openkal-windows 自己的清单** -而不是引擎。失败的真正现象是链接行**缺了 `-L`** —— -那个目录里正是它生成的 `libntdll.a` 等导入库。 - -⇒ **测量胜过模型**:一个我解释不了的改动不该合入。D11 退回待办,并带上这条判据 —— -它同时是「图来源 C 库」那条路线必须先解决的前置。⭐ 附带发现:openkal 的包**已经 -在自己的清单里用 `-ffunction-sections` 与 `-Wl,--gc-sections`**,所以引擎侧是否 -需要再发一份,本身也要重新论证。 - ### ⚠️ 实施推翻的两处方案原文 **① `extra` 列不承载浮点 ABI —— triple 已经承载了它。** @@ -493,8 +464,6 @@ clang 对一次 float 乘法仍发出 `vmul.f32` —— 在没有 FPU 的 Cortex ### 未实施(按方案原顺序) -**D11 死代码段消除**(见 §10.1,已从本 PR 撤出,带判据)· - `P0` 的 `mcpplibs/picolibc` + `compiler-rt-builtins` 源码包(§3.1.1)· 板级包三个 · `P0'` 的 `xim:probe-rs` 与 E 轴零工程项 · `P1`/`P1'` 真机 · `P1'` 槽表 · `P2` lock 成钉与 SBOM · `P3` openarch 第四后端 · `P3'` 嵌入式 Linux。 diff --git a/CHANGELOG.md b/CHANGELOG.md index eb7e4811..d242dd51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ## [2026.9.4.1] — 2026-09-04 -Cortex-M 落地为七个目标行。 +Cortex-M 落地为七个目标行,freestanding 链接开启死代码段消除。 裸机目标表从四行增至十一行。M-profile 是七行而不是一行:为 `thumbv7em` 构建的 目标文件使用 Cortex-M0 没有的指令,两种拼写产出互不兼容的目标文件,而表存在的 @@ -23,6 +23,13 @@ target = "thumbv7em-none-eabihf" 因此携带 `-mfpu=none`,包括架构本来就没有 FPU 的那几行 —— 一行陈述它保证的性质, 而不是从一个可以改变的默认值继承它。 +freestanding 编译加 `-ffunction-sections -fdata-sections`、链接加 `--gc-sections`。 +依赖的目标文件无条件进入链接(不像归档成员那样按未定义符号拉取),当 C 库改由 +依赖图提供时,没有死代码段消除的镜像会装进整份 C 库,而 Cortex-M 器件只有几十 KB。 + +⚠️ **链接脚本因此以新的方式承重**:中断向量表不被任何东西引用,`--gc-sections` +会回收它,板级脚本必须写 `KEEP(*(.vectors))`。 + 同时回填了 `docs/13` 中两条已被 2026.8.28.2 推翻的限制:当图中有包提供 `hosted-standard-library` 时,裸机目标上的异常、RTTI 与 `import std` 均可用。 diff --git a/docs/13-baremetal.md b/docs/13-baremetal.md index d64acc1a..4c44ae5c 100644 --- a/docs/13-baremetal.md +++ b/docs/13-baremetal.md @@ -59,6 +59,95 @@ Cortex-M needs no `lldEmulation` column entry: clang has a *BareMetal* toolchain for arm, so these triples reach `ld.lld` through the driver as the RISC-V and aarch64 rows do. 32-bit ARM has no `-mcmodel` axis, so that column is empty too. +### Dead-section elimination + +Freestanding builds compile with `-ffunction-sections -fdata-sections` and link +with `--gc-sections`. Both halves belong to the engine rather than to a project +because a dependency's translation units must carry them, and a project cannot +reach those. + +The flags became necessary rather than merely economical when a C library began +arriving from the dependency graph. A dependency's object files enter the link +unconditionally, unlike an archive member, which is pulled only while its symbol +is undefined. That costs nothing when the C library is a prebuilt archive and +the target has megabytes; a Cortex-M part has kilobytes, and without dead-section +elimination every image would carry the whole of the C library. + +⚠️ **A linker script becomes load-bearing in a new way.** An interrupt vector +table is referenced by nothing — the hardware reads it by address — so +`--gc-sections` collects it. A board's script must say `KEEP(*(.vectors))`. +Measured: with the `KEEP` present, a function nothing calls is dropped, the +table survives, and the image boots. + +⚠️ The last two rows default to no C library, and that is a statement rather +than an omission: the first consumer of both rows — the `openarch` layer of +machine mechanism — references no C library symbol, and if no row defaulted to +this tier there would be nothing demonstrating the tier works. +⭐ **A build for those rows is declarable, not absent** (mcpp 2026.8.21.3+). +`xim:picolibc-aarch64` and `xim:picolibc-x86` are in the index; a project that +wants one names it the same way it would choose a different one: + +```toml +[target.aarch64-none-elf] +sysroot = "xim:picolibc-aarch64@1.8.12" +``` An empty column means exactly what +`[target.].sysroot = ""` means in a manifest, so a project targeting one +begins on the zero-libc tier without asking. A project that wants a C library on +those targets declares one, which is also how it would choose a different one. + +Such a target needs no per-host cross toolchain. clang and lld are +cross-compilers by construction — one binary emits every target it was built +with — so the target table pins `llvm@22.1.8` on every host, and any machine +that can install the LLVM payload can produce an image for any of the four. + +### The x86_64 row is not four strings + +⚠️ **A target row is normally an entry in two tables and nothing else. This one +needed engine code, and the reason is a property of clang rather than of the +instruction set.** + +clang selects a toolchain from the triple. It has a *BareMetal* toolchain for +arm, aarch64 and riscv, which links with `ld.lld` directly; it has none for +x86_64, so every spelling of a bare x86_64 triple falls through to the generic +GCC toolchain — whose linker is the **host's `g++`**: + +``` +g++: error: unrecognized command-line option '-fuse-ld=/…/llvm/22.1.8/bin/ld.lld' +``` + +Measured for `x86_64-none-elf`, `x86_64-unknown-none-elf`, `x86_64-unknown-none`, +`x86_64-elf`, `x86_64-none-none` and `x86_64-unknown-unknown`, and unchanged by +`-fuse-ld=lld`, `--ld-path=`, `--gcc-toolchain=` or `-B`. The one thing that +does change it is putting `linux` in the OS position, which makes clang link +directly and adds eight host `-L` paths to a bare-metal link. + +Neither outcome is acceptable: routing through a host `g++` makes the row work +on a Linux host and nowhere else, and host search paths on a freestanding link +are the hermeticity this engine exists to keep. So the row carries a fifth +column, `lldEmulation`, and when it is set the engine drives the link with +`ld.lld` itself. The flag vocabulary changes with the tool — `-Map=` rather than +`-Wl,-Map=`, `-m elf_x86_64` rather than `--target=` — and the driver-only flags +(`-nostdlib++`, the loader tag) are dropped rather than translated. + +The column is empty for the riscv and aarch64 rows. Their driver already reaches +lld, and changing a working link to make three rows look alike is how a +regression is introduced. + +### `-mno-red-zone` is part of the target, not a preference + +The System V x86-64 ABI reserves 128 bytes below `rsp` that a leaf function may +use without adjusting the stack pointer, because on a hosted system nothing else +writes there. On bare metal the processor pushes an interrupt frame at `rsp` — +into the red zone — and the interrupted leaf resumes to find its locals +overwritten. There is no fault and no diagnostic, and it happens only when an +interrupt arrives inside a leaf. + +There is no bare-metal x86_64 program for which the red zone is safe, so the +flag is a property of the row rather than something a project remembers. It +reaches the command line through a new `extra` column in the ISA-profile table, +which exists because `-march`/`-mabi`/`-mcmodel` could not express it. RISC-V +and aarch64 have no equivalent, which is why the column did not exist before. + Three things a bare-metal build requires are not properties of the ISA, and mcpp does not attempt to derive them: which startup object and libraries to select, which linker script describes the machine's memory, and how to execute diff --git a/docs/zh/13-baremetal.md b/docs/zh/13-baremetal.md index 47c6e5ec..ee57def5 100644 --- a/docs/zh/13-baremetal.md +++ b/docs/zh/13-baremetal.md @@ -52,6 +52,82 @@ Cortex-M 不需要 `lldEmulation` 列:clang 有面向 arm 的 *BareMetal* 工具 triple 与 RISC-V、aarch64 各行一样经驱动到达 `ld.lld`。32 位 ARM 没有 `-mcmodel` 这个轴,所以该列同样为空。 +### 死代码段消除 + +freestanding 构建以 `-ffunction-sections -fdata-sections` 编译,以 `--gc-sections` +链接。两半都属于引擎而不属于工程,因为依赖的翻译单元也必须带上它们,而工程够不到 +那些单元。 + +这两个标志从「划算」变成「必需」,发生在 C 库开始由依赖图提供的时候。依赖的目标 +文件是**无条件**进入链接的,不像归档成员那样只在符号仍未定义时才被拉入。当 C 库是 +预编译归档、目标又有若干兆字节时,这不花什么代价;而 Cortex-M 器件只有几十 KB, +没有死代码段消除,每个镜像都会装进整份 C 库。 + +⚠️ **链接脚本因此以一种新的方式承重。** 中断向量表不被任何东西引用 —— 硬件按地址 +读取它 —— 所以 `--gc-sections` 会把它回收。板级脚本必须写 `KEEP(*(.vectors))`。 +实测:有这条 `KEEP` 时,无人调用的函数被丢弃、向量表被保留、镜像能够启动。 + +⚠️ 后两行**默认**没有 C 库,这是声明而非遗漏:这两行的第一个消费者 —— 机器机制层 +`openarch` —— 一个 C 库符号都不引用,而**如果四行里没有一行默认在这一层,就没有 +任何东西在证明这一层可用**。空列在这里的含义与清单里 +`[target.].sysroot = ""` 完全一致。 + +⭐ **这两行的 C 库是可声明的,不是不存在的**(mcpp 2026.8.21.3+)。 +`xim:picolibc-aarch64` 与 `xim:picolibc-x86` 已在索引里;想要它的工程自行声明, +而那与它换用另一份 C 库是同一个动作: + +```toml +[target.aarch64-none-elf] +sysroot = "xim:picolibc-aarch64@1.8.12" +``` + +这类目标不需要逐宿主的交叉工具链。clang 与 lld 在构造上就是交叉编译器 —— +一个二进制发射它构建时支持的全部目标 —— 因此目标表在每个宿主上都钉 +`llvm@22.1.8`,任何能安装 LLVM 载荷的机器都能为这四个中的任何一个产出镜像。 + +### x86_64 这一行不止是四个字符串 + +⚠️ **一个目标行通常就是两张表里的两条记录,不含引擎代码。这一行需要引擎代码, +而原因是 clang 的属性而非指令集的属性。** + +clang 由 triple 选择工具链。它为 arm、aarch64、riscv 备有 *BareMetal* 工具链, +直接以 `ld.lld` 链接;它没有 x86_64 的,于是裸 x86_64 triple 的每一种写法都落到 +通用 GCC 工具链上 —— 而后者的链接器是**宿主的 `g++`**: + +``` +g++: error: unrecognized command-line option '-fuse-ld=/…/llvm/22.1.8/bin/ld.lld' +``` + +对 `x86_64-none-elf`、`x86_64-unknown-none-elf`、`x86_64-unknown-none`、 +`x86_64-elf`、`x86_64-none-none`、`x86_64-unknown-unknown` 逐一实测,结果一致; +`-fuse-ld=lld`、`--ld-path=`、`--gcc-toolchain=`、`-B` 逐一实测,均不改变结果。 +唯一能改变它的是把 `linux` 放进 OS 位,而那会给一次裸机链接带来八条宿主 `-L`。 + +两种结果都不可接受:经宿主 `g++` 会让这一行只在 Linux 宿主上成立;宿主搜索路径 +出现在 freestanding 链接上,正是本引擎存在的意义所要守住的封闭性。因此该行带了 +第五列 `lldEmulation`,置位时引擎直接用 `ld.lld` 驱动链接。标志的词汇随工具一同 +改变 —— `-Map=` 而非 `-Wl,-Map=`,`-m elf_x86_64` 而非 `--target=` —— 而仅属于 +驱动的标志(`-nostdlib++`、加载器标签)是被丢弃而不是被翻译。 + +riscv 与 aarch64 两行的该列为空。它们的驱动本就到得了 lld,为了让三行看起来一致 +而改动一条可用的链接,正是引入回归的方式。 + +### `-mno-red-zone` 是目标的属性,不是偏好 + +System V x86-64 ABI 在 `rsp` 之下保留 128 字节,叶函数可以不调整栈指针直接使用, +因为在有操作系统的机器上没有别的东西会写那里。裸机上不存在做这件事的一方:处理器 +在 `rsp` 处压入中断帧 —— 正压进红区 —— 被中断的叶函数恢复后发现自己的局部变量已被 +覆盖。既不触发异常也没有诊断,而且只在中断恰好落在叶函数内部时发生。 + +不存在红区安全的裸机 x86_64 程序,所以这个标志是行的属性,而不是工程需要记住的 +事情。它经 ISA 档表新增的 `extra` 列到达命令行 —— 该列之所以存在,是因为 +`-march`/`-mabi`/`-mcmodel` 表达不了它。RISC-V 与 aarch64 没有对应物,这也正是 +此前不需要这一列的原因。 + +裸机构建需要的三样东西并不是 ISA 的属性,mcpp 也不试图推导它们:选哪个启动对象 +与哪些库、哪份链接脚本描述这台机器的内存、以及如何执行产出的镜像。这三样随 +**板级支持包**一起提供,而板级支持包是一个普通依赖。由此得到的结果是:换板子是 +一次依赖变更,而不是一次构建系统变更。 ## 从零到一个可运行的镜像 diff --git a/src/freestanding/linkline.cppm b/src/freestanding/linkline.cppm index 913a9c09..d0c873c8 100644 --- a/src/freestanding/linkline.cppm +++ b/src/freestanding/linkline.cppm @@ -89,6 +89,9 @@ inline std::string link_flags(const Spec& s, const LinkInputs& in, // suggesting it does. for (auto const& f : compile_flags(s)) { out += ' '; out += f; } out += " -nostdlib -nostartfiles -static"; + // The link half of the pair set in mcpp.freestanding.target's + // `compile_flags`. Routed through the driver, hence `-Wl,`. + out += " -Wl,--gc-sections"; if (!in.lld.empty()) out += " -fuse-ld=" + esc(in.lld); // ⚠️ BEFORE the libraries the board selects, and it has to be on THIS line @@ -129,6 +132,11 @@ inline std::string link_flags_direct(const Spec& s, const LinkInputs& in, // No loader exists, so an image must not name one. The driver line reaches // this through `-static`; here it is said to the linker. out += " --no-dynamic-linker"; + // Said to the linker directly here, as `-Wl,--gc-sections` is said through + // the driver on the line above. The two paths must agree: a target whose + // link is driven by `ld.lld` itself would otherwise keep every section its + // dependencies emit, and the difference would show only as size. + out += " --gc-sections"; if (!in.sysrootLib.empty()) out += " -L" + esc(in.sysrootLib); if (!in.linkerScript.empty()) diff --git a/src/freestanding/target.cppm b/src/freestanding/target.cppm index 8e3bc2ec..b545194e 100644 --- a/src/freestanding/target.cppm +++ b/src/freestanding/target.cppm @@ -291,6 +291,30 @@ inline std::vector compile_flags(const Spec& s, // `-ffreestanding` so the ordering of this function's output stays a // function of the table rather than of the row. for (auto flag : s.extra) out.emplace_back(flag); + // ⭐⭐ ONE SECTION PER FUNCTION, SO THE LINKER CAN DROP WHAT NOTHING CALLS. + // + // These two flags do nothing on their own; they are the half of + // `--gc-sections` that has to happen at compile time, and the link half is + // in mcpp.freestanding.linkline. Both halves are here rather than left to a + // project because a dependency's translation units must carry them too, and + // a project cannot reach those. + // + // ⚠️ WHY THIS BECAME NECESSARY RATHER THAN MERELY NICE. A dependency's + // object files enter the link unconditionally (docs/13), unlike an archive + // member, which is pulled only while its symbol is undefined. That costs + // nothing when the C library is a prebuilt archive and the target has + // megabytes — the arrangement every bare-metal row had until now. A C + // library that arrives from the dependency graph is object files, and a + // Cortex-M part has kilobytes: without this, every image carries the whole + // of the C library whether or not it calls into it. + // + // ⚠️ AND IT MAKES A LINKER SCRIPT LOAD-BEARING IN A NEW WAY: an interrupt + // vector table is referenced by nothing — the hardware reads it by address + // — so `--gc-sections` collects it. A board's script must say + // `KEEP(*(.vectors))`. Measured: with the KEEP present, a dead function is + // dropped and the table survives; the image boots. + out.emplace_back("-ffunction-sections"); + out.emplace_back("-fdata-sections"); // ⭐ AND `-ffreestanding` ITSELF IS ONE OF THE THINGS THE GRAPH DECIDES. // // The paragraph above this function names what the flag changes: "no `main` diff --git a/tests/e2e/332_cortex_m_builds_and_boots.sh b/tests/e2e/332_cortex_m_builds_and_boots.sh index 2080fe70..6535d107 100755 --- a/tests/e2e/332_cortex_m_builds_and_boots.sh +++ b/tests/e2e/332_cortex_m_builds_and_boots.sh @@ -1,9 +1,16 @@ #!/usr/bin/env bash # requires: llvm unix-shell qemu-arm -# Cortex-M: the M-profile rows build and boot, and honour the float ABI. +# Cortex-M: the M-profile rows build, boot, and collect what nothing calls. # -# ⚠️ THE PROPERTY THAT MATTERS HERE IS THE ONE A REASONED TABLE WOULD HAVE GOT -# WRONG: +# ⚠️ THE ROWS THAT MATTER HERE ARE THE ONES A REASONED TABLE WOULD HAVE GOT +# WRONG. Two properties are asserted that a build alone cannot show: +# +# * `--gc-sections` reaches a bare-metal link. Without it a C library that +# arrives from the dependency graph — object files, which enter a link +# unconditionally — puts its whole self into every image, and a Cortex-M +# part has kilobytes. The assertion is that a function nothing calls is +# ABSENT from the image while the interrupt vector table, which nothing +# references either, SURVIVES because the script says KEEP. # # * the soft-float rows do not emit FPU instructions. `thumbv7em`'s # architecture implies an FPU, so clang emits `vmul.f32` for a float @@ -63,14 +70,17 @@ inline void sh(int op, const void* a) { } extern "C" unsigned __stack_top; +// Referenced by nothing. The image must not contain it. +extern "C" void collected_because_nothing_calls_it() { sh(0x04, (void*)"UNREACHABLE\n"); } + extern "C" void Reset_Handler() { sh(0x04, (void*)"cortex-m ok\n"); sh(0x18, (void*)0x20026); // semihosting SYS_EXIT, ADP_Stopped_ApplicationExit for (;;) {} } -// Referenced by nothing — the hardware reads it by address, which is why the -// linker script says KEEP. +// Referenced by nothing either — the hardware reads it by address. It survives +// only because the linker script says KEEP. extern "C" __attribute__((section(".vectors"), used)) void* const vectors[] = { (void*)&__stack_top, (void*)Reset_Handler }; CPP @@ -109,10 +119,12 @@ boot_row() { # triple machine cpuflag flash_org flash_len ram_org r elf=$(find target -type f -name mcu | head -1) [ -n "$elf" ] || { echo "FAIL: $triple produced no artefact"; exit 1; } - # The vector table is present: it is what the machine fetches at reset, and - # an image without it boots into nothing. + # gc-sections: the dead function is gone, the vector table is not. + if [ "$("$NM" "$elf" | grep -c collected_because_nothing_calls_it)" != "0" ]; then + echo "FAIL: $triple kept a function nothing calls (--gc-sections not applied)"; exit 1 + fi if [ "$("$NM" "$elf" | grep -c vectors)" != "1" ]; then - echo "FAIL: $triple has no vector table"; exit 1 + echo "FAIL: $triple lost the vector table (KEEP not honoured)"; exit 1 fi local out @@ -184,4 +196,4 @@ case "$soft_out" in echo "$soft_out" | tail -5 | sed 's/^/ /'; exit 1 ;; esac -echo "PASS: cortex-m rows build, boot and honour the float ABI" +echo "PASS: cortex-m rows build, boot, collect dead code and honour the float ABI" diff --git a/tests/unit/test_freestanding.cpp b/tests/unit/test_freestanding.cpp index ca84d72b..7d2cdcba 100644 --- a/tests/unit/test_freestanding.cpp +++ b/tests/unit/test_freestanding.cpp @@ -529,3 +529,19 @@ TEST(FreestandingTarget, MProfileRowsNeedNoCodeModelAndNoDirectLldDriving) { } EXPECT_GT(rows, 0); } + +// The compile half of `--gc-sections`, which has to hold for EVERY freestanding +// target rather than only the M-profile ones: a dependency's translation units +// carry these flags too, and a project cannot reach those. +TEST(FreestandingTarget, EveryRowCompilesWithPerFunctionSections) { + int rows = 0; + for (const auto& spec : mcpp::freestanding::known()) { + ++rows; + auto flags = mcpp::freestanding::compile_flags(spec); + EXPECT_NE(std::find(flags.begin(), flags.end(), "-ffunction-sections"), + flags.end()) << spec.triple; + EXPECT_NE(std::find(flags.begin(), flags.end(), "-fdata-sections"), + flags.end()) << spec.triple; + } + EXPECT_GT(rows, 0); +} From e8621d30b2ab756e9795bd76e744b543b1cbb740 Mon Sep 17 00:00:00 2001 From: speak-agent <248744407+speak-agent@users.noreply.github.com> Date: Fri, 4 Sep 2026 03:58:23 +0800 Subject: [PATCH 8/8] docs(triple): drop a sentence the table had outgrown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review: the paragraph above the M-profile rows still said "two rows are verified", written when two were. Four boot under QEMU and the block directly below already names all four, so the sentence contradicted the table two lines under it — the kind of stale count a reader trusts because it reads like a summary. --- modules/toolchain-model/src/triple.cppm | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/toolchain-model/src/triple.cppm b/modules/toolchain-model/src/triple.cppm index 77c60fb9..f0be2b56 100644 --- a/modules/toolchain-model/src/triple.cppm +++ b/modules/toolchain-model/src/triple.cppm @@ -381,9 +381,6 @@ inline constexpr TargetInfo kKnownTargets[] = { // package is compiled with the consuming target's own flags, so the ABI // agreement holds by construction and there is no multilib at all. // - // Two rows are `verified` because two have a board and an emulator that - // runs them; the rest build and are `preview`, the same standard applied to - // `aarch64-none-elf` and `x86_64-none-elf` when they landed. // ⚠️ THE TIER COLUMN RECORDS WHAT WAS RUN, NOT WHAT WAS REASONED. Measured // 2026-09-04 under `xim:qemu-arm@9.2.4-1`: each `verified` row below built // an image that BOOTED on the named machine and printed over semihosting —