Skip to content

Commit a4ded89

Browse files
committed
Revert "revert(freestanding): drop dead-section elimination"
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.
1 parent 097954e commit a4ded89

8 files changed

Lines changed: 243 additions & 42 deletions

File tree

.agents/docs/2026-09-04-commercial-grade-baremetal-embedded-plan.md

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -436,39 +436,10 @@ v1 按工程依赖把 E 整个排到 P2。自审:**其中一半没有工程依
436436
| 条目 | 状态 |
437437
|---|---|
438438
| Cortex-M 目标行 |**七行**,`kKnownTargets``freestanding::kTable` 各七行 |
439-
| D11 死代码段消除 | **撤出本 PR** —— 实测打断 Windows 宿主上的 openkal 交叉构建,见 §10.1 |
439+
| D11 死代码段消除 | `-ffunction-sections -fdata-sections` + `--gc-sections`(驱动路径与直接 lld 路径) |
440440
| `docs/13` 回填两条失效限制 | ✅ 中英双份 |
441441
| 判据 | ✅ e2e 332(四行在 QEMU 中启动)+ 三条单测;CI `bare-metal e2e` 作业已绿 |
442442

443-
### ⚠️⚠️ 10.1 D11 被实测挡住,而挡住它的机制尚未查清
444-
445-
`--gc-sections` 在本机(Linux 宿主)与 CI 的裸机作业上都正确;它打断的是
446-
**openkal 在 Windows 宿主上的交叉构建**:
447-
448-
```
449-
lld: error: unable to find library -lntdll
450-
lld: error: unable to find library -lkernel32 (等五条)
451-
```
452-
453-
判据是三次对照,不是推理:
454-
455-
| 分支 | `build 3 targets on windows` |
456-
|---|---|
457-
| `main`(workflow_dispatch 对照) ||
458-
| 本 PR(含 gc-sections,三次) ||
459-
| `bisect/no-gc-sections`(只去掉 gc-sections) ||
460-
461-
⚠️ **而这条与我的机制模型矛盾**:两处 `gc-sections` 都在 `isFreestandingTarget`
462-
之内,`x86_64-windows-gnu` 不该进那个分支;本机 Linux→Windows 交叉**能成功**,且
463-
失败的链接行里的 `-Wl,--gc-sections` 经核对来自 **openkal-windows 自己的清单**
464-
而不是引擎。失败的真正现象是链接行**缺了 `-L<openkal-windows 的 out 目录>`** ——
465-
那个目录里正是它生成的 `libntdll.a` 等导入库。
466-
467-
**测量胜过模型**:一个我解释不了的改动不该合入。D11 退回待办,并带上这条判据 ——
468-
它同时是「图来源 C 库」那条路线必须先解决的前置。⭐ 附带发现:openkal 的包**已经
469-
在自己的清单里用 `-ffunction-sections``-Wl,--gc-sections`**,所以引擎侧是否
470-
需要再发一份,本身也要重新论证。
471-
472443
### ⚠️ 实施推翻的两处方案原文
473444

474445
**`extra` 列不承载浮点 ABI —— triple 已经承载了它。**
@@ -493,8 +464,6 @@ clang 对一次 float 乘法仍发出 `vmul.f32` —— 在没有 FPU 的 Cortex
493464

494465
### 未实施(按方案原顺序)
495466

496-
**D11 死代码段消除**(见 §10.1,已从本 PR 撤出,带判据)·
497-
498467
`P0``mcpplibs/picolibc` + `compiler-rt-builtins` 源码包(§3.1.1)· 板级包三个 ·
499468
`P0'``xim:probe-rs` 与 E 轴零工程项 · `P1`/`P1'` 真机 · `P1'` 槽表 ·
500469
`P2` lock 成钉与 SBOM · `P3` openarch 第四后端 · `P3'` 嵌入式 Linux。

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
## [2026.9.4.1] — 2026-09-04
77

8-
Cortex-M 落地为七个目标行。
8+
Cortex-M 落地为七个目标行,freestanding 链接开启死代码段消除
99

1010
裸机目标表从四行增至十一行。M-profile 是七行而不是一行:为 `thumbv7em` 构建的
1111
目标文件使用 Cortex-M0 没有的指令,两种拼写产出互不兼容的目标文件,而表存在的
@@ -23,6 +23,13 @@ target = "thumbv7em-none-eabihf"
2323
因此携带 `-mfpu=none`,包括架构本来就没有 FPU 的那几行 —— 一行陈述它保证的性质,
2424
而不是从一个可以改变的默认值继承它。
2525

26+
freestanding 编译加 `-ffunction-sections -fdata-sections`、链接加 `--gc-sections`
27+
依赖的目标文件无条件进入链接(不像归档成员那样按未定义符号拉取),当 C 库改由
28+
依赖图提供时,没有死代码段消除的镜像会装进整份 C 库,而 Cortex-M 器件只有几十 KB。
29+
30+
⚠️ **链接脚本因此以新的方式承重**:中断向量表不被任何东西引用,`--gc-sections`
31+
会回收它,板级脚本必须写 `KEEP(*(.vectors))`
32+
2633
同时回填了 `docs/13` 中两条已被 2026.8.28.2 推翻的限制:当图中有包提供
2734
`hosted-standard-library` 时,裸机目标上的异常、RTTI 与 `import std` 均可用。
2835

docs/13-baremetal.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,95 @@ Cortex-M needs no `lldEmulation` column entry: clang has a *BareMetal* toolchain
5959
for arm, so these triples reach `ld.lld` through the driver as the RISC-V and
6060
aarch64 rows do. 32-bit ARM has no `-mcmodel` axis, so that column is empty too.
6161

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.
81+
82+
⚠️ The last two rows default to no C library, and that is a statement rather
83+
than an omission: the first consumer of both rows — the `openarch` layer of
84+
machine mechanism — references no C library symbol, and if no row defaulted to
85+
this tier there would be nothing demonstrating the tier works.
86+
**A build for those rows is declarable, not absent** (mcpp 2026.8.21.3+).
87+
`xim:picolibc-aarch64` and `xim:picolibc-x86` are in the index; a project that
88+
wants one names it the same way it would choose a different one:
89+
90+
```toml
91+
[target.aarch64-none-elf]
92+
sysroot = "xim:picolibc-aarch64@1.8.12"
93+
``` An empty column means exactly what
94+
`[target.<triple>].sysroot = ""` means in a manifest, so a project targeting one
95+
begins on the zero-libc tier without asking. A project that wants a C library on
96+
those targets declares one, which is also how it would choose a different one.
97+
98+
Such a target needs no per-host cross toolchain. clang and lld are
99+
cross-compilers by construction — one binary emits every target it was built
100+
with — so the target table pins `llvm@22.1.8` on every host, and any machine
101+
that can install the LLVM payload can produce an image for any of the four.
102+
103+
### The x86_64 row is not four strings
104+
105+
⚠️ **A target row is normally an entry in two tables and nothing else. This one
106+
needed engine code, and the reason is a property of clang rather than of the
107+
instruction set.**
108+
109+
clang selects a toolchain from the triple. It has a *BareMetal* toolchain for
110+
arm, aarch64 and riscv, which links with `ld.lld` directly; it has none for
111+
x86_64, so every spelling of a bare x86_64 triple falls through to the generic
112+
GCC toolchain — whose linker is the **host's `g++`**:
113+
114+
```
115+
g++: error: unrecognized command-line option '-fuse-ld=/…/llvm/22.1.8/bin/ld.lld'
116+
```
117+
118+
Measured for `x86_64-none-elf`, `x86_64-unknown-none-elf`, `x86_64-unknown-none`,
119+
`x86_64-elf`, `x86_64-none-none` and `x86_64-unknown-unknown`, and unchanged by
120+
`-fuse-ld=lld`, `--ld-path=`, `--gcc-toolchain=` or `-B`. The one thing that
121+
does change it is putting `linux` in the OS position, which makes clang link
122+
directly and adds eight host `-L` paths to a bare-metal link.
123+
124+
Neither outcome is acceptable: routing through a host `g++` makes the row work
125+
on a Linux host and nowhere else, and host search paths on a freestanding link
126+
are the hermeticity this engine exists to keep. So the row carries a fifth
127+
column, `lldEmulation`, and when it is set the engine drives the link with
128+
`ld.lld` itself. The flag vocabulary changes with the tool — `-Map=` rather than
129+
`-Wl,-Map=`, `-m elf_x86_64` rather than `--target=` — and the driver-only flags
130+
(`-nostdlib++`, the loader tag) are dropped rather than translated.
131+
132+
The column is empty for the riscv and aarch64 rows. Their driver already reaches
133+
lld, and changing a working link to make three rows look alike is how a
134+
regression is introduced.
135+
136+
### `-mno-red-zone` is part of the target, not a preference
137+
138+
The System V x86-64 ABI reserves 128 bytes below `rsp` that a leaf function may
139+
use without adjusting the stack pointer, because on a hosted system nothing else
140+
writes there. On bare metal the processor pushes an interrupt frame at `rsp` —
141+
into the red zone — and the interrupted leaf resumes to find its locals
142+
overwritten. There is no fault and no diagnostic, and it happens only when an
143+
interrupt arrives inside a leaf.
144+
145+
There is no bare-metal x86_64 program for which the red zone is safe, so the
146+
flag is a property of the row rather than something a project remembers. It
147+
reaches the command line through a new `extra` column in the ISA-profile table,
148+
which exists because `-march`/`-mabi`/`-mcmodel` could not express it. RISC-V
149+
and aarch64 have no equivalent, which is why the column did not exist before.
150+
62151
Three things a bare-metal build requires are not properties of the ISA, and
63152
mcpp does not attempt to derive them: which startup object and libraries to
64153
select, which linker script describes the machine's memory, and how to execute

docs/zh/13-baremetal.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,82 @@ Cortex-M 不需要 `lldEmulation` 列:clang 有面向 arm 的 *BareMetal* 工具
5252
triple 与 RISC-V、aarch64 各行一样经驱动到达 `ld.lld`。32 位 ARM 没有 `-mcmodel`
5353
这个轴,所以该列同样为空。
5454

55+
### 死代码段消除
56+
57+
freestanding 构建以 `-ffunction-sections -fdata-sections` 编译,以 `--gc-sections`
58+
链接。两半都属于引擎而不属于工程,因为依赖的翻译单元也必须带上它们,而工程够不到
59+
那些单元。
60+
61+
这两个标志从「划算」变成「必需」,发生在 C 库开始由依赖图提供的时候。依赖的目标
62+
文件是**无条件**进入链接的,不像归档成员那样只在符号仍未定义时才被拉入。当 C 库是
63+
预编译归档、目标又有若干兆字节时,这不花什么代价;而 Cortex-M 器件只有几十 KB,
64+
没有死代码段消除,每个镜像都会装进整份 C 库。
65+
66+
⚠️ **链接脚本因此以一种新的方式承重。** 中断向量表不被任何东西引用 —— 硬件按地址
67+
读取它 —— 所以 `--gc-sections` 会把它回收。板级脚本必须写 `KEEP(*(.vectors))`
68+
实测:有这条 `KEEP` 时,无人调用的函数被丢弃、向量表被保留、镜像能够启动。
69+
70+
⚠️ 后两行**默认**没有 C 库,这是声明而非遗漏:这两行的第一个消费者 —— 机器机制层
71+
`openarch` —— 一个 C 库符号都不引用,而**如果四行里没有一行默认在这一层,就没有
72+
任何东西在证明这一层可用**。空列在这里的含义与清单里
73+
`[target.<triple>].sysroot = ""` 完全一致。
74+
75+
**这两行的 C 库是可声明的,不是不存在的**(mcpp 2026.8.21.3+)。
76+
`xim:picolibc-aarch64``xim:picolibc-x86` 已在索引里;想要它的工程自行声明,
77+
而那与它换用另一份 C 库是同一个动作:
78+
79+
```toml
80+
[target.aarch64-none-elf]
81+
sysroot = "xim:picolibc-aarch64@1.8.12"
82+
```
83+
84+
这类目标不需要逐宿主的交叉工具链。clang 与 lld 在构造上就是交叉编译器 ——
85+
一个二进制发射它构建时支持的全部目标 —— 因此目标表在每个宿主上都钉
86+
`llvm@22.1.8`,任何能安装 LLVM 载荷的机器都能为这四个中的任何一个产出镜像。
87+
88+
### x86_64 这一行不止是四个字符串
89+
90+
⚠️ **一个目标行通常就是两张表里的两条记录,不含引擎代码。这一行需要引擎代码,
91+
而原因是 clang 的属性而非指令集的属性。**
92+
93+
clang 由 triple 选择工具链。它为 arm、aarch64、riscv 备有 *BareMetal* 工具链,
94+
直接以 `ld.lld` 链接;它没有 x86_64 的,于是裸 x86_64 triple 的每一种写法都落到
95+
通用 GCC 工具链上 —— 而后者的链接器是**宿主的 `g++`**:
96+
97+
```
98+
g++: error: unrecognized command-line option '-fuse-ld=/…/llvm/22.1.8/bin/ld.lld'
99+
```
100+
101+
`x86_64-none-elf``x86_64-unknown-none-elf``x86_64-unknown-none`
102+
`x86_64-elf``x86_64-none-none``x86_64-unknown-unknown` 逐一实测,结果一致;
103+
`-fuse-ld=lld``--ld-path=``--gcc-toolchain=``-B` 逐一实测,均不改变结果。
104+
唯一能改变它的是把 `linux` 放进 OS 位,而那会给一次裸机链接带来八条宿主 `-L`
105+
106+
两种结果都不可接受:经宿主 `g++` 会让这一行只在 Linux 宿主上成立;宿主搜索路径
107+
出现在 freestanding 链接上,正是本引擎存在的意义所要守住的封闭性。因此该行带了
108+
第五列 `lldEmulation`,置位时引擎直接用 `ld.lld` 驱动链接。标志的词汇随工具一同
109+
改变 —— `-Map=` 而非 `-Wl,-Map=`,`-m elf_x86_64` 而非 `--target=` —— 而仅属于
110+
驱动的标志(`-nostdlib++`、加载器标签)是被丢弃而不是被翻译。
111+
112+
riscv 与 aarch64 两行的该列为空。它们的驱动本就到得了 lld,为了让三行看起来一致
113+
而改动一条可用的链接,正是引入回归的方式。
114+
115+
### `-mno-red-zone` 是目标的属性,不是偏好
116+
117+
System V x86-64 ABI 在 `rsp` 之下保留 128 字节,叶函数可以不调整栈指针直接使用,
118+
因为在有操作系统的机器上没有别的东西会写那里。裸机上不存在做这件事的一方:处理器
119+
`rsp` 处压入中断帧 —— 正压进红区 —— 被中断的叶函数恢复后发现自己的局部变量已被
120+
覆盖。既不触发异常也没有诊断,而且只在中断恰好落在叶函数内部时发生。
121+
122+
不存在红区安全的裸机 x86_64 程序,所以这个标志是行的属性,而不是工程需要记住的
123+
事情。它经 ISA 档表新增的 `extra` 列到达命令行 —— 该列之所以存在,是因为
124+
`-march`/`-mabi`/`-mcmodel` 表达不了它。RISC-V 与 aarch64 没有对应物,这也正是
125+
此前不需要这一列的原因。
126+
127+
裸机构建需要的三样东西并不是 ISA 的属性,mcpp 也不试图推导它们:选哪个启动对象
128+
与哪些库、哪份链接脚本描述这台机器的内存、以及如何执行产出的镜像。这三样随
129+
**板级支持包**一起提供,而板级支持包是一个普通依赖。由此得到的结果是:换板子是
130+
一次依赖变更,而不是一次构建系统变更。
55131

56132
## 从零到一个可运行的镜像
57133

src/freestanding/linkline.cppm

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ inline std::string link_flags(const Spec& s, const LinkInputs& in,
8989
// suggesting it does.
9090
for (auto const& f : compile_flags(s)) { out += ' '; out += f; }
9191
out += " -nostdlib -nostartfiles -static";
92+
// The link half of the pair set in mcpp.freestanding.target's
93+
// `compile_flags`. Routed through the driver, hence `-Wl,`.
94+
out += " -Wl,--gc-sections";
9295
if (!in.lld.empty())
9396
out += " -fuse-ld=" + esc(in.lld);
9497
// ⚠️ 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,
129132
// No loader exists, so an image must not name one. The driver line reaches
130133
// this through `-static`; here it is said to the linker.
131134
out += " --no-dynamic-linker";
135+
// Said to the linker directly here, as `-Wl,--gc-sections` is said through
136+
// the driver on the line above. The two paths must agree: a target whose
137+
// link is driven by `ld.lld` itself would otherwise keep every section its
138+
// dependencies emit, and the difference would show only as size.
139+
out += " --gc-sections";
132140
if (!in.sysrootLib.empty())
133141
out += " -L" + esc(in.sysrootLib);
134142
if (!in.linkerScript.empty())

src/freestanding/target.cppm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,30 @@ inline std::vector<std::string> compile_flags(const Spec& s,
291291
// `-ffreestanding` so the ordering of this function's output stays a
292292
// function of the table rather than of the row.
293293
for (auto flag : s.extra) out.emplace_back(flag);
294+
// ⭐⭐ ONE SECTION PER FUNCTION, SO THE LINKER CAN DROP WHAT NOTHING CALLS.
295+
//
296+
// These two flags do nothing on their own; they are the half of
297+
// `--gc-sections` that has to happen at compile time, and the link half is
298+
// in mcpp.freestanding.linkline. Both halves are here rather than left to a
299+
// project because a dependency's translation units must carry them too, and
300+
// a project cannot reach those.
301+
//
302+
// ⚠️ WHY THIS BECAME NECESSARY RATHER THAN MERELY NICE. A dependency's
303+
// object files enter the link unconditionally (docs/13), unlike an archive
304+
// member, which is pulled only while its symbol is undefined. That costs
305+
// nothing when the C library is a prebuilt archive and the target has
306+
// megabytes — the arrangement every bare-metal row had until now. A C
307+
// library that arrives from the dependency graph is object files, and a
308+
// Cortex-M part has kilobytes: without this, every image carries the whole
309+
// of the C library whether or not it calls into it.
310+
//
311+
// ⚠️ AND IT MAKES A LINKER SCRIPT LOAD-BEARING IN A NEW WAY: an interrupt
312+
// vector table is referenced by nothing — the hardware reads it by address
313+
// — so `--gc-sections` collects it. A board's script must say
314+
// `KEEP(*(.vectors))`. Measured: with the KEEP present, a dead function is
315+
// dropped and the table survives; the image boots.
316+
out.emplace_back("-ffunction-sections");
317+
out.emplace_back("-fdata-sections");
294318
// ⭐ AND `-ffreestanding` ITSELF IS ONE OF THE THINGS THE GRAPH DECIDES.
295319
//
296320
// The paragraph above this function names what the flag changes: "no `main`

0 commit comments

Comments
 (0)