Skip to content

Commit 289ba6b

Browse files
committed
feat(freestanding): Cortex-M as seven target rows, and dead-section elimination (2026.9.4.1)
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 <triple>` 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.
1 parent dfa431f commit 289ba6b

12 files changed

Lines changed: 599 additions & 17 deletions

File tree

.github/workflows/ci-linux-e2e.yml

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ jobs:
114114
# test, and assert its PASS line appeared. A skip fails this job.
115115
# ──────────────────────────────────────────────────────────────────
116116
baremetal:
117-
name: bare-metal e2e (riscv64-none-elf, qemu)
117+
name: bare-metal e2e (riscv64-none-elf + cortex-m, qemu)
118118
runs-on: ubuntu-24.04
119119
timeout-minutes: 40
120120
env:
@@ -153,6 +153,18 @@ jobs:
153153
XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \
154154
"$XLINGS_BIN" install xim:picolibc-riscv -y
155155
test -d "${MCPP_HOME:-$HOME/.mcpp}/registry/data/xpkgs/xim-x-picolibc-riscv"
156+
# ⚠️ The M-profile emulator, in BOTH homes for the reason above.
157+
# `xim:qemu-arm` carries `qemu-system-arm` as well as
158+
# `qemu-system-aarch64`; test 332 addresses it by absolute path out of
159+
# the payload, so what matters is that the payload EXISTS in the home
160+
# mcpp uses rather than that a shim resolves.
161+
"$XLINGS_BIN" install xim:qemu-arm -y
162+
XLINGS_HOME="${MCPP_HOME:-$HOME/.mcpp}/registry" \
163+
"$XLINGS_BIN" install xim:qemu-arm -y
164+
# Reachable AND runnable before the tests, so that a missing emulator
165+
# fails this step rather than silently skipping test 332.
166+
ls "${MCPP_HOME:-$HOME/.mcpp}"/registry/data/xpkgs/xim-x-qemu-arm/*/bin/qemu-system-arm \
167+
| sort -V | tail -1 | xargs -I{} {} --version | head -1
156168
157169
- name: Bare-metal e2e
158170
timeout-minutes: 25
@@ -175,7 +187,8 @@ jobs:
175187
for t in tests/e2e/130_freestanding_riscv_build_and_run.sh \
176188
tests/e2e/131_freestanding_bsp_supplies_everything.sh \
177189
tests/e2e/132_freestanding_test_and_artifacts.sh \
178-
tests/e2e/133_freestanding_std_subset.sh; do
190+
tests/e2e/133_freestanding_std_subset.sh \
191+
tests/e2e/332_cortex_m_builds_and_boots.sh; do
179192
echo "=== $t ==="
180193
bash "$t" 2>&1 | tee "$(basename "$t").log"
181194
rc=${PIPESTATUS[0]}
@@ -196,6 +209,18 @@ jobs:
196209
grep -q 'PASS: the freestanding std subset' \
197210
133_freestanding_std_subset.sh.log || {
198211
echo "133 (std subset) skipped on the runner that must run it"; exit 1; }
212+
# ⚠️ 332 declares `# requires: qemu-arm`, which no sharded runner has
213+
# — so on the shards it exits 0 without running. This job is the only
214+
# place its PASS line can be demanded.
215+
grep -q 'PASS: cortex-m rows build, boot' \
216+
332_cortex_m_builds_and_boots.sh.log || {
217+
echo "332 (cortex-m) skipped on the runner that must run it"; exit 1; }
218+
# ⭐ And a count, because four `grep -q` calls that each matched say
219+
# nothing about how many rows the script actually booted: a fixture
220+
# that stopped iterating would still print its PASS line.
221+
booted=$(grep -c 'booted on ' 332_cortex_m_builds_and_boots.sh.log || true)
222+
[ "$booted" = "4" ] || {
223+
echo "332 booted $booted rows, expected 4"; exit 1; }
199224
200225
# ──────────────────────────────────────────────────────────────────
201226
# Hermetic (no host toolchain): the ONLY environment class that

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,36 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [2026.9.4.1] — 2026-09-04
7+
8+
Cortex-M 落地为七个目标行,freestanding 链接开启死代码段消除。
9+
10+
裸机目标表从四行增至十一行。M-profile 是七行而不是一行:为 `thumbv7em` 构建的
11+
目标文件使用 Cortex-M0 没有的指令,两种拼写产出互不兼容的目标文件,而表存在的
12+
理由正是让 `--target <triple>` 单独足以产出正确的目标文件。
13+
14+
```toml
15+
[build]
16+
target = "thumbv7em-none-eabihf"
17+
```
18+
19+
⚠️ **浮点 ABI 不决定 FPU 是否被使用。** `eabi`/`eabihf` 由 clang 从 triple 读出,
20+
它约束浮点值如何跨越函数边界,不约束函数内部发什么指令 —— 而 `thumbv7em` 架构
21+
蕴含 FPv4-SP。实测:软浮点 ABI 下 clang 对一次 float 乘法仍发出 `vmul.f32`,在
22+
没有 FPU 的 Cortex-M4 上于运行期触发异常,而编译与链接都是干净的。每个软浮点行
23+
因此携带 `-mfpu=none`,包括架构本来就没有 FPU 的那几行 —— 一行陈述它保证的性质,
24+
而不是从一个可以改变的默认值继承它。
25+
26+
freestanding 编译加 `-ffunction-sections -fdata-sections`、链接加 `--gc-sections`
27+
依赖的目标文件无条件进入链接(不像归档成员那样按未定义符号拉取),当 C 库改由
28+
依赖图提供时,没有死代码段消除的镜像会装进整份 C 库,而 Cortex-M 器件只有几十 KB。
29+
30+
⚠️ **链接脚本因此以新的方式承重**:中断向量表不被任何东西引用,`--gc-sections`
31+
会回收它,板级脚本必须写 `KEEP(*(.vectors))`
32+
33+
同时回填了 `docs/13` 中两条已被 2026.8.28.2 推翻的限制:当图中有包提供
34+
`hosted-standard-library` 时,裸机目标上的异常、RTTI 与 `import std` 均可用。
35+
636
## [2026.9.3.2] — 2026-09-03
737

838
`[xlings.workspace]`**推荐书写形态**定为命名空间在键上,官方包全部使用它;

docs/13-baremetal.md

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,71 @@ covers the hosted link model this chapter departs from.
1313
## Overview
1414

1515
A freestanding target is a target whose `os` field is `none`. The target table
16-
at `modules/toolchain-model/src/triple.cppm` carries four of them:
16+
at `modules/toolchain-model/src/triple.cppm` carries eleven of them:
1717

1818
| Triple | Tier | C library |
1919
|---|---|---|
2020
| `riscv64-none-elf` | verified | `xim:picolibc-riscv` |
2121
| `riscv32-none-elf` | verified | `xim:picolibc-riscv` |
2222
| `aarch64-none-elf` | preview | none by default — the zero-libc tier; `xim:picolibc-aarch64` is declarable |
2323
| `x86_64-none-elf` | preview | none by default — the zero-libc tier; `xim:picolibc-x86` is declarable |
24+
| `thumbv6m-none-eabi` | verified | none by default — Cortex-M0/M0+/M1 |
25+
| `thumbv7m-none-eabi` | verified | none by default — Cortex-M3 |
26+
| `thumbv7em-none-eabi` | preview | none by default — Cortex-M4/M7, soft float |
27+
| `thumbv7em-none-eabihf` | verified | none by default — Cortex-M4F/M7F, hard float |
28+
| `thumbv8m.base-none-eabi` | preview | none by default — Cortex-M23 |
29+
| `thumbv8m.main-none-eabi` | verified | none by default — Cortex-M33/M55, soft float |
30+
| `thumbv8m.main-none-eabihf` | preview | none by default — Cortex-M33F/M55F, hard float |
2431

2532
`verified` means an image has been built **and run** for the row. `preview`
26-
means it builds and has been observed to run, but is not yet covered by the
27-
engine's own emulator jobs.
33+
means it builds and links, and no emulator run has been recorded.
34+
35+
### M-profile is seven rows rather than one
36+
37+
Every other bare-metal family above is one row per architecture. Cortex-M is
38+
not. An object built for `thumbv7em` uses instructions a Cortex-M0 does not
39+
have, and the two spellings produce incompatible objects rather than expressing
40+
a preference. The table exists so that `--target <triple>` alone suffices to
41+
produce a correct object file; a single `arm-none-eabi` row plus an `-mcpu` that
42+
each project remembered would move a correctness decision out of the table and
43+
into every manifest.
44+
45+
The `eabi`/`eabihf` suffix is the float ABI, and clang derives it from the
46+
triple without help: measured on llvm 22.1.8, `thumbv7em-none-eabi` yields
47+
`-mfloat-abi soft` and `thumbv7em-none-eabihf` yields `hard`.
48+
49+
⚠️ **The float ABI does not settle whether the FPU is used.** It governs how
50+
floating-point values cross a function boundary, not what the compiler may emit
51+
inside one, and the `thumbv7em` architecture implies FPv4-SP. Measured: under
52+
the soft-float ABI clang still emits `vmul.f32` for a float multiply. On a
53+
Cortex-M4 without an FPU that instruction faults at run time, after a clean
54+
compile and a clean link. Every soft-float row therefore carries `-mfpu=none`,
55+
including the rows describing architectures that have no FPU at all — a row
56+
states the property it guarantees rather than inheriting it from a default.
57+
58+
Cortex-M needs no `lldEmulation` column entry: clang has a *BareMetal* toolchain
59+
for arm, so these triples reach `ld.lld` through the driver as the RISC-V and
60+
aarch64 rows do. 32-bit ARM has no `-mcmodel` axis, so that column is empty too.
61+
62+
### Dead-section elimination
63+
64+
Freestanding builds compile with `-ffunction-sections -fdata-sections` and link
65+
with `--gc-sections`. Both halves belong to the engine rather than to a project
66+
because a dependency's translation units must carry them, and a project cannot
67+
reach those.
68+
69+
The flags became necessary rather than merely economical when a C library began
70+
arriving from the dependency graph. A dependency's object files enter the link
71+
unconditionally, unlike an archive member, which is pulled only while its symbol
72+
is undefined. That costs nothing when the C library is a prebuilt archive and
73+
the target has megabytes; a Cortex-M part has kilobytes, and without dead-section
74+
elimination every image would carry the whole of the C library.
75+
76+
⚠️ **A linker script becomes load-bearing in a new way.** An interrupt vector
77+
table is referenced by nothing — the hardware reads it by address — so
78+
`--gc-sections` collects it. A board's script must say `KEEP(*(.vectors))`.
79+
Measured: with the `KEEP` present, a function nothing calls is dropped, the
80+
table survives, and the image boots.
2881

2982
⚠️ The last two rows default to no C library, and that is a statement rather
3083
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`.
190243
| 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`. |
191244
| ISA flags | `-march`, `-mabi` and `-mcmodel` come from one row per target in `src/freestanding/target.cppm`, so `--target <triple>` alone is sufficient to produce a correct object file. |
192245
| 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`). |
193-
| 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. |
194-
| `import std` | Unavailable, and rejected at configure time with a diagnostic rather than at link time. |
246+
| 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. |
247+
| `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. |
195248
| Entry point | `int main()` is available whenever something supplies a `crt0`. A board-support package normally does. |
196249
| Default linkage | Static, and not as a preference: there is no loader, so there is no other option. |
197250
@@ -716,7 +769,7 @@ targets, but that expectation is **not** covered by a test.
716769
| Limitation | Observed behaviour |
717770
|---|---|
718771
| `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. |
719-
| 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. |
772+
| 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. |
720773
| 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. |
721774
| C library substitution | Expressible since 2026.8.20.2 through `[target.<triple>].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. |
722775
| `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. |

0 commit comments

Comments
 (0)