Skip to content

feat(freestanding): Cortex-M 七个目标行 + 死代码段消除 (2026.9.4.1) - #550

Merged
Sunrisepeak merged 8 commits into
mainfrom
feat/cortex-m-and-graph-libc
Sep 3, 2026
Merged

feat(freestanding): Cortex-M 七个目标行 + 死代码段消除 (2026.9.4.1)#550
Sunrisepeak merged 8 commits into
mainfrom
feat/cortex-m-and-graph-libc

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Sep 3, 2026

Copy link
Copy Markdown
Member

裸机目标表从四行增至十一行,freestanding 链接开启死代码段消除。方案见
.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md(本 PR 一并提交)。

M-profile 是七行而不是一行

thumbv7em 构建的目标文件使用 Cortex-M0 没有的指令 —— 两种拼写产出互不兼容
的目标文件,而不是一种偏好。表存在的理由正是让 --target <triple> 单独足以产出正确
的目标文件。

⚠️ 浮点 ABI 不决定 FPU 是否被使用

clang 从 eabi/eabihf 自行导出 -mfloat-abi,但它约束的是浮点值如何跨越函数
边界
,不约束函数内部发什么指令 —— thumbv7em 蕴含 FPv4-SP。实测 llvm 22.1.8:

clang --target=thumbv7em-none-eabi -O2 -S    (float a*b+1.0f)
    vmul.f32                                  ← 一条 FPU 指令

在没有 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启动并经半主机打印:thumbv6mmicrobit
thumbv7mmps2-an385thumbv7em-eabihfmps2-an386thumbv8m.main-eabi
mps2-an505。其余三行构建并链接得出,标为 preview

⭐ 四行第一次跑时有两行 hard-fault,因为我的通用链接脚本声称 128K RAM 而 nRF51 只有
16K —— 内存映射是板的事实,不是目标的事实。换成各板真实映射后四行全部打印。

判据

  • e2e 332:启动四个 verified 行;断言无人调用的函数缺席而向量表存活;
    浮点 ABI 两侧测量(硬行的 FPU 指令计数 + 软行链接失败点名 __aeabi_fmul)。
  • 两个守卫都确认能独立变红;退回 -mfpu=none 后软行构建成功(76 字节),
    正是它要防的静默危险。
  • 三条单元测试陈述 e2e 说不出的规则(对所有行量化,两侧都计数)。
  • ⚠️ 脚本声明 # requires: qemu-arm,无分片 runner 有它 ⇒ 由 baremetal job
    直接调用,要求 PASS 行与「启动了 4 行」的计数run_all.sh 补上探测并注册进
    KNOWN_CAPS(CI 报出过这一条)。
  • 支持矩阵:42 行由修好后的 scan 实测生成,compare.sh 两种模式均通过。

⭐⭐ 一处值得单独读的:scan.sh 的判据是拼写而不是性质

它用 grep -- '-none-elf$' 判断「这是不是裸机目标」。那是四行的 env 字段,不是被问
的问题 —— Cortex-M 把同一个 OS 拼作 -none-eabi/-none-eabihf,于是同一条规则的
两份拷贝都漏了
:探针源码走了 hosted 分支发 #include <cstdio>(产出一整列关于探针
而非目标的 build-failed,正是它上面三行注释已经记过的缺陷),graph 模式的跳过也没
跳过。两处都改为问 OS 字段。

⚠️⚠️ 一次假信号(记在方案 §10.1)

CI 上 Windows 宿主的 openkal 交叉构建报 unable to find library -lntdll,而「只回退
gc-sections」的分支变绿,于是 gc-sections 一度被撤出。那个判据是错的:

分支 build 3 targets on windows
main ×2 · main+七行 · main+版本号 · 本 PR−版本号
本 PR(含/不含 gc-sections)×5
与本 PR 逐字节相同的代码,只换分支名

最后一行否掉全部代码性解释。真因是 GitHub Actions 缓存按分支隔离:分支优先读自己
的缓存,本 PR 第一次运行(因 qemu-arm 未注册而中止)存下半成品沙箱,此后每次都恢复
它;而每个新建的 bisect 分支没有自己的缓存,回落 main 的好缓存。已删除被污染的缓存,
gc-sections 已恢复。

教训:单因子对照在每个因子上都通过而组合仍失败时,下一步不是继续拆因子,是问
「判据是不是被环境决定的」。

回归

全量单元 97 passed / 0 failed;freestanding e2e 130139 + 332 全绿(含
riscv64 在 QEMU 中真跑);支持矩阵两模式通过。

顺带回填

docs/13 两条限制在 2026.8.28.2 已被推翻却仍写着:图中有包提供
hosted-standard-library 时,裸机上的异常、RTTI 与 import std 均可用。中英双份,
并同步 README 与 docs/16 的目标表。

…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.
@Sunrisepeak Sunrisepeak changed the title feat(freestanding): Cortex-M 落地为七个目标行,以及死代码段消除 (2026.9.4.1) feat(freestanding): Cortex-M 落地为七个目标行 (2026.9.4.1) Sep 3, 2026
⭐ 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.
@Sunrisepeak Sunrisepeak changed the title feat(freestanding): Cortex-M 落地为七个目标行 (2026.9.4.1) feat(freestanding): Cortex-M 七个目标行 + 死代码段消除 (2026.9.4.1) Sep 3, 2026
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.
@Sunrisepeak
Sunrisepeak merged commit fd1a539 into main Sep 3, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants