Skip to content

fix: 产物必须加载它链接的那一份库 —— 链接行顺序成为声明,共享库不再劫持运行时 (2026.8.11.3) - #414

Merged
Sunrisepeak merged 1 commit into
mainfrom
fix/origin-precedence-and-shared-cxx-runtime
Aug 11, 2026
Merged

fix: 产物必须加载它链接的那一份库 —— 链接行顺序成为声明,共享库不再劫持运行时 (2026.8.11.3)#414
Sunrisepeak merged 1 commit into
mainfrom
fix/origin-precedence-and-shared-cxx-runtime

Conversation

@Sunrisepeak

Copy link
Copy Markdown
Member

fix: 产物必须加载它链接的那一份库 —— 链接行顺序成为声明,共享库不再劫持运行时 (2026.8.11.3)

mcpp run 一个 imgui/GLFW 工程,链接 rc=0,运行即死:

undefined symbol: _ZNKSt13runtime_error4whatEv

缺陷一:$ORIGIN 被 SubOS 库视图遮蔽

2026.8.11.2(#413)首次把 SubOS 库视图(farm)写进产物 DT_RPATH,但它落在
$ORIGIN 之前。而这两个目录在本生态里天然装着同名 SONAME —— mcpp 从
compat.x11 源码构建 libX11.so 部署到产物目录,xlings 又在 farm 里有
xim:libX11。于是链接期用 A,运行期加载 B

真因不是"放错位置",而是一条链接命令行的顺序由两个互不知情的生产者用 +=
决定
:flags.cppm 把 farm 拼进全局 ldflags(注释还写着 "so it is LAST"),
plan.cppm$ORIGIN 拼进 per-unit,而链接规则渲染的是
$ldflags $unit_ldflags。三处各自都对,合起来是错的。

新增 mcpp.build.link_line:把 per-unit 尾部声明成具名槽位
(dependencies → cxxRuntime → runtimeFallback → loaderTag),相对顺序写在类型
里、由单测钉死。新增一个生产者必须先选一个槽 —— "选"正是"在产物自己的目录之前
还是之后"这个问题被提出来的地方。格式中立:槽位按职责命名,PE 的两个槽天然为空,
Mach-O 的 dependencies 装 @loader_path,没有任何 if (platform)

缺陷二:共享库把自己的 C++ 运行时导出给了别人(ELF)

SharedLibrary 与可执行文件共用 Distributable 角色,于是拿到同一份
self-contained 契约:-static-libstdc++。ELF 上这不是"私有一份" —— 只有一个全局
符号命名空间,共享对象导出它定义的每一个全局符号。一个纯 C 的 compat 包因此
导出了 777 个 GLOBAL 标准库定义(libXau.so:39KB 的 Xau + 9.5MB 的 libstdc++)。

可执行文件链接时 -lX11 排在驱动的 -lstdc++ 之前,ld 用它满足了
runtime_error::what(),归档成员从不拉入 —— exe 的 -static-libstdc++ 变成
空操作,它的 C++ 运行时事实上是那个 .so
。缺陷一之所以致命,根源在这里。

共享库默认契约改为按目标格式分档:

ELF toolchain-coupled 一个全局命名空间,先加载的定义胜出
Mach-O self-contained 机制本就是 -load_hidden,dyld 不归一;
且 toolchain-coupled 在 macOS 是死路(#202)
PE self-contained 没有全局命名空间,导入按 DLL 逐个按名解析

只有 ELF 的行为变了,而它正是有缺陷的那个;Mach-O/PE 产物字节不变。
显式 cxx_runtime = { shared = "self-contained" } 仍可选回自包含,此时自动补
-Wl,--exclude-libs(实测:按归档基名匹配,与 -l 还是完整路径无关),
让内嵌的运行时留在动态符号表之外 —— 逃生舱不会重新打开这个洞。

顺带修掉的架构债

dist::default_contract 自称"the role -> contract policy, in one place",实际
没有任何生产调用方 —— 真正的策略在 flags.cppm 被第二次推导。这正是
distribution.cppm 开篇声讨的那类债("derived independently in five places")
换个位置复发。现在它是唯一真源。

cxx_runtime 补进 [build] 已知键白名单:它此前会打印 "unsupported key
(ignored)" —— 而 "ignored" 是假的,--strict 还会直接拒绝 manifest。整个特性的
唯一入口不能一边工作一边说自己被忽略了。

测试

  • 新增 test_link_line(6):槽位顺序、空槽不产生多余分隔、顺序与赋值序无关
  • 新增 NinjaBackend.SubosFarmRpathFollowsTheArtifactsOwnDirectory:断言在
    合成后的链接行上 $ORIGIN 早于 farm(只看其中一个变量,正是原缺陷隐形的原因)
  • test_distribution +8:(Role × Format) 全表、--exclude-libs 只给共享库
  • test_manifest +3:shared 键、未知角色键报错、标量拼写仍覆盖所有角色
  • e2e 219 断言由「farm 是最后一个绝对路径条目」收紧为「字面最后一项」,
    并补一条行为不变量(LD_DEBUG=libs 实测同名 SONAME 解析到 $ORIGIN)。
    旧断言把 $ORIGIN 过滤掉了,对坏顺序与好顺序给出同一个结论;测试工程也从
    int main() 换成消费依赖共享库 —— 否则它连 $ORIGIN 都不产生
  • 新增 e2e 222:共享库不得导出 GLOBAL 标准库符号、必须 NEEDED libstdc++.so.6、
    exe 无未定义 std 符号、显式自包含时护栏仍在

红测(两条新 e2e 对已发布 2026.8.11.2):219 报「farm is not the last entry」,
222 报「exports 713 GLOBAL standard-library symbols」—— 均以正确理由失败。

⚠️ 行为不变量不得依赖崩溃:缺陷二修好后崩溃会消失,依赖崩溃的断言会立刻假绿。
219 的不变量 4 因此测的是加载器的搜索过程,不是程序的退出码。

实测(helloegui:imgui + GLFW + X11)

判据 修复前 修复后
DT_RPATH 尾部 … : <subos>/lib : $ORIGIN … : $ORIGIN : <subos>/lib
libX11.so.6 解析到 farm 里的 xim:libX11 1.8.10 $ORIGIN(farm 未被试到)
bin/libX11.so 导出 std 符号 2931(777 GLOBAL) 0
bin/libXau.so 9 557 936 B 39 368 B
exe 未定义 runtime_error::what
运行 symbol lookup error GUI 正常启动

分析:.agents/docs/2026-08-11-runtime-search-origin-precedence-analysis.md
计划:.agents/docs/2026-08-11-origin-precedence-implementation-plan.md

`mcpp run` 一个 imgui/GLFW 工程,链接 rc=0,运行即死:

    undefined symbol: _ZNKSt13runtime_error4whatEv

## 缺陷一:`$ORIGIN` 被 SubOS 库视图遮蔽

2026.8.11.2(#413)首次把 SubOS 库视图(farm)写进产物 DT_RPATH,但它落在
`$ORIGIN` **之前**。而这两个目录在本生态里天然装着同名 SONAME —— mcpp 从
`compat.x11` 源码构建 `libX11.so` 部署到产物目录,xlings 又在 farm 里有
`xim:libX11`。于是**链接期用 A,运行期加载 B**。

真因不是"放错位置",而是**一条链接命令行的顺序由两个互不知情的生产者用 `+=`
决定**:`flags.cppm` 把 farm 拼进全局 ldflags(注释还写着 "so it is LAST"),
`plan.cppm` 把 `$ORIGIN` 拼进 per-unit,而链接规则渲染的是
`$ldflags $unit_ldflags`。三处各自都对,合起来是错的。

新增 `mcpp.build.link_line`:把 per-unit 尾部声明成**具名槽位**
(dependencies → cxxRuntime → runtimeFallback → loaderTag),相对顺序写在类型
里、由单测钉死。新增一个生产者必须先选一个槽 —— "选"正是"在产物自己的目录之前
还是之后"这个问题被提出来的地方。格式中立:槽位按职责命名,PE 的两个槽天然为空,
Mach-O 的 dependencies 装 `@loader_path`,没有任何 `if (platform)`。

## 缺陷二:共享库把自己的 C++ 运行时导出给了别人(ELF)

`SharedLibrary` 与可执行文件共用 `Distributable` 角色,于是拿到同一份
self-contained 契约:`-static-libstdc++`。ELF 上这不是"私有一份" —— 只有一个全局
符号命名空间,共享对象导出它定义的每一个全局符号。一个**纯 C** 的 compat 包因此
导出了 777 个 GLOBAL 标准库定义(`libXau.so`:39KB 的 Xau + 9.5MB 的 libstdc++)。

可执行文件链接时 `-lX11` 排在驱动的 `-lstdc++` 之前,ld 用它满足了
`runtime_error::what()`,归档成员从不拉入 —— **exe 的 `-static-libstdc++` 变成
空操作,它的 C++ 运行时事实上是那个 `.so`**。缺陷一之所以致命,根源在这里。

共享库默认契约改为**按目标格式分档**:

  ELF     toolchain-coupled   一个全局命名空间,先加载的定义胜出
  Mach-O  self-contained      机制本就是 -load_hidden,dyld 不归一;
                              且 toolchain-coupled 在 macOS 是死路(#202)
  PE      self-contained      没有全局命名空间,导入按 DLL 逐个按名解析

**只有 ELF 的行为变了,而它正是有缺陷的那个**;Mach-O/PE 产物字节不变。
显式 `cxx_runtime = { shared = "self-contained" }` 仍可选回自包含,此时自动补
`-Wl,--exclude-libs`(实测:按归档基名匹配,与 `-l` 还是完整路径无关),
让内嵌的运行时留在动态符号表之外 —— 逃生舱不会重新打开这个洞。

## 顺带修掉的架构债

`dist::default_contract` 自称"the role -> contract policy, in one place",实际
**没有任何生产调用方** —— 真正的策略在 `flags.cppm` 被第二次推导。这正是
`distribution.cppm` 开篇声讨的那类债("derived independently in five places")
换个位置复发。现在它是唯一真源。

`cxx_runtime` 补进 `[build]` 已知键白名单:它此前会打印 "unsupported key
(ignored)" —— 而 "ignored" 是假的,`--strict` 还会直接拒绝 manifest。整个特性的
唯一入口不能一边工作一边说自己被忽略了。

## 测试

- 新增 `test_link_line`(6):槽位顺序、空槽不产生多余分隔、顺序与赋值序无关
- 新增 `NinjaBackend.SubosFarmRpathFollowsTheArtifactsOwnDirectory`:断言在
  **合成后的**链接行上 `$ORIGIN` 早于 farm(只看其中一个变量,正是原缺陷隐形的原因)
- `test_distribution` +8:(Role × Format) 全表、`--exclude-libs` 只给共享库
- `test_manifest` +3:`shared` 键、未知角色键报错、标量拼写仍覆盖所有角色
- e2e 219 断言由「farm 是最后一个**绝对路径**条目」收紧为「**字面**最后一项」,
  并补一条**行为**不变量(`LD_DEBUG=libs` 实测同名 SONAME 解析到 `$ORIGIN`)。
  旧断言把 `$ORIGIN` 过滤掉了,对坏顺序与好顺序给出同一个结论;测试工程也从
  `int main()` 换成消费依赖共享库 —— 否则它连 `$ORIGIN` 都不产生
- 新增 e2e 222:共享库不得导出 GLOBAL 标准库符号、必须 NEEDED libstdc++.so.6、
  exe 无未定义 std 符号、显式自包含时护栏仍在

**红测**(两条新 e2e 对已发布 2026.8.11.2):219 报「farm is not the last entry」,
222 报「exports 713 GLOBAL standard-library symbols」—— 均以正确理由失败。

**⚠️ 行为不变量不得依赖崩溃**:缺陷二修好后崩溃会消失,依赖崩溃的断言会立刻假绿。
219 的不变量 4 因此测的是加载器的搜索过程,不是程序的退出码。

## 实测(helloegui:imgui + GLFW + X11)

| 判据 | 修复前 | 修复后 |
|---|---|---|
| DT_RPATH 尾部 | `… : <subos>/lib : $ORIGIN` | `… : $ORIGIN : <subos>/lib` |
| `libX11.so.6` 解析到 | farm 里的 xim:libX11 1.8.10 | `$ORIGIN`(farm 未被试到) |
| `bin/libX11.so` 导出 std 符号 | 2931(777 GLOBAL) | 0 |
| `bin/libXau.so` | 9 557 936 B | 39 368 B |
| exe 未定义 `runtime_error::what` | 有 | 无 |
| 运行 | symbol lookup error | GUI 正常启动 |

分析:`.agents/docs/2026-08-11-runtime-search-origin-precedence-analysis.md`
计划:`.agents/docs/2026-08-11-origin-precedence-implementation-plan.md`
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