feat(freestanding): Cortex-M 七个目标行 + 死代码段消除 (2026.9.4.1) - #550
Merged
Conversation
…limination (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.
… 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.
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.
方案文档此前只存在于工作副本里,没有进入 PR —— 而它是本次改动的依据,读者要靠 它才能知道七个目标行与 --gc-sections 为什么是同一批。 §10 记下实施推翻方案的两处:extra 列承载的是 -mfpu=none 而不是方案写的 -mfloat-abi(triple 已经承载了浮点 ABI),以及行数是七不是六。
⚠️ 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.
⭐ 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 <cstdio>`
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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
裸机目标表从四行增至十一行,freestanding 链接开启死代码段消除。方案见
.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md(本 PR 一并提交)。M-profile 是七行而不是一行
为
thumbv7em构建的目标文件使用 Cortex-M0 没有的指令 —— 两种拼写产出互不兼容的目标文件,而不是一种偏好。表存在的理由正是让
--target <triple>单独足以产出正确的目标文件。
clang 从
eabi/eabihf自行导出-mfloat-abi,但它约束的是浮点值如何跨越函数边界,不约束函数内部发什么指令 ——
thumbv7em蕴含 FPv4-SP。实测 llvm 22.1.8:在没有 FPU 的 Cortex-M4 上它于运行期触发异常,而编译与链接都是干净的。每个软浮点
行因此携带
-mfpu=none。⭐ 包括架构本来就没有 FPU 的行 —— 第一版只加在实测到非零指令数的行上,那让表记录的是测量而不是保证,由对所有行量化的单元测试报出。
死代码段消除
-ffunction-sections -fdata-sections+--gc-sections(驱动路径与直接 lld 路径)。两半都属于引擎:依赖的翻译单元也必须带上,而工程够不到那些单元。依赖的目标文件
无条件进入链接(不像归档成员按未定义符号拉取),Cortex-M 器件只有几十 KB。
--gc-sections会回收它,板级脚本必须
KEEP(*(.vectors))。档位记录的是「跑过什么」
四行在
xim:qemu-arm@9.2.4-1下启动并经半主机打印:thumbv6m→microbit、thumbv7m→mps2-an385、thumbv7em-eabihf→mps2-an386、thumbv8m.main-eabi→mps2-an505。其余三行构建并链接得出,标为preview。⭐ 四行第一次跑时有两行 hard-fault,因为我的通用链接脚本声称 128K RAM 而 nRF51 只有
16K —— 内存映射是板的事实,不是目标的事实。换成各板真实映射后四行全部打印。
判据
浮点 ABI 两侧测量(硬行的 FPU 指令计数 + 软行链接失败点名
__aeabi_fmul)。-mfpu=none后软行构建成功(76 字节),正是它要防的静默危险。
# requires: qemu-arm,无分片 runner 有它 ⇒ 由baremetaljob直接调用,要求 PASS 行与「启动了 4 行」的计数。
run_all.sh补上探测并注册进KNOWN_CAPS(CI 报出过这一条)。compare.sh两种模式均通过。⭐⭐ 一处值得单独读的:
scan.sh的判据是拼写而不是性质它用
grep -- '-none-elf$'判断「这是不是裸机目标」。那是四行的 env 字段,不是被问的问题 —— Cortex-M 把同一个 OS 拼作
-none-eabi/-none-eabihf,于是同一条规则的两份拷贝都漏了:探针源码走了 hosted 分支发
#include <cstdio>(产出一整列关于探针而非目标的
build-failed,正是它上面三行注释已经记过的缺陷),graph 模式的跳过也没跳过。两处都改为问 OS 字段。
CI 上 Windows 宿主的 openkal 交叉构建报
unable to find library -lntdll,而「只回退gc-sections」的分支变绿,于是 gc-sections 一度被撤出。那个判据是错的:
build 3 targets on windowsmain×2 ·main+七行 ·main+版本号 · 本 PR−版本号最后一行否掉全部代码性解释。真因是 GitHub Actions 缓存按分支隔离:分支优先读自己
的缓存,本 PR 第一次运行(因
qemu-arm未注册而中止)存下半成品沙箱,此后每次都恢复它;而每个新建的 bisect 分支没有自己的缓存,回落 main 的好缓存。已删除被污染的缓存,
gc-sections 已恢复。
⭐ 教训:单因子对照在每个因子上都通过而组合仍失败时,下一步不是继续拆因子,是问
「判据是不是被环境决定的」。
回归
全量单元 97 passed / 0 failed;freestanding e2e
130–139+332全绿(含riscv64 在 QEMU 中真跑);支持矩阵两模式通过。
顺带回填
docs/13两条限制在 2026.8.28.2 已被推翻却仍写着:图中有包提供hosted-standard-library时,裸机上的异常、RTTI 与import std均可用。中英双份,并同步 README 与
docs/16的目标表。