8.6 KiB
ELF 瘦身可行性分析
2026-09-20 · 环境:WSL Ubuntu-22.04 +
/opt/ps5-payload-sdk+ LLD 18.1.8 基线产物web-file-mgr-v1.9.1.elf= 1,034,328 B 复现脚本:.build/_sizeprobe.sh、.build/_probe3.sh、.build/_slimtest.sh、.build/_slimtest2.sh、.build/_modsize.sh
结论
✅ 4.1 + 4.2 已落地(2026-09-20) —
src/demangle_stub.c已加入COMMON_SRCS,LDFLAGS已加-Wl,--icf=all。产物:870,488 B · sha256
24392aff6ddcca4dc0ea969cce356bd693ac52efe8a117d61ee1c814aa43cd07· e_machine0x003e落地后复核:
__cxa_demangle本体 11 B、itanium_demangle符号 0、__cxa_throw/_Unwind_Resume等异常符号齐全、sevenz_extract/rar_extract/zipx_extract/MHD_start_daemon_va全部存在。 section 变化:.text637,616 → 538,336、.rela.dyn54,816 → 25,800、.eh_frame58,904 → 43,972、.rodata162,016 → 153,664。4.3(RELR)与 4.4 未启用 —— 待真机验证 / 权衡后再决定。
能瘦,而且有一个"白捡"的 15.8%。
| 方案 | 结果 | 降幅 | 风险 |
|---|---|---|---|
| 瘦身前 | 1,034,328 B | — | — |
+ __cxa_demangle 桩 |
886,872 B | −147 KB | 极低 |
| + ICF 代码折叠(当前产物) | 870,488 B | −164 KB (−15.8%) | 极低 |
| 再 + RELR 重定位压缩 | 854,176 B | −180 KB (−17.4%) | 需真机验证 |
第三方改 -Oz(单列) |
969,504 B | −65 KB | 可能降速 |
前两项不损失任何功能,也不影响解压速度 —— 去掉的是一段永远不会被执行的代码。
一、现状构成
数据源:size -A / nm --size-sort --print-size / 未 strip 重链接。
section 级
| section | 大小 | 备注 |
|---|---|---|
.text |
637,616 | 代码主体 |
.rodata |
162,016 | 前端资源(gzip)+ 字符串常量 |
.eh_frame + .eh_frame_hdr |
71,444 | C++ 异常展开表 |
.gcc_except_table |
8,500 | C++ 异常处理器表 |
.rela.dyn |
54,816 | 2,119 × R_X86_64_RELATIVE + 165 × GLOB_DAT |
.data.rel.ro |
20,192 | 含指针的只读数据 |
.dynsym + .dynstr + .gnu.hash |
23,863 | 动态符号表(PIE 必需) |
.text$LZMADECOPT |
4,719 | LZMA 汇编解码器(提速 1.26×,保留) |
.bss |
56,416 | 不占文件体积 |
模块级(按目标文件归属,text+data)
| 模块 | 文件数 | text | data |
|---|---|---|---|
| unrar7(RAR 引擎) | 48 | 314,528 | 658 |
| libc++ / libc++abi / PS5 运行时 | — | ~161,900 | 20,738 |
| C++ 异常机制 | — | 151,288 | — |
| zlib | 8 | 65,211 | 336 |
| 7z SDK + 自研链 | 30 | 85,406 | 56 |
| minizip-ng | 8 | 39,382 | 384 |
自有代码 src/ |
14 | 60,653 | 472 |
| libmicrohttpd | — | 44,647 | — |
| 前端资源(gzip 后) | 13 | 40,278 | 104 |
二、最大的单一发现:151 KB 的 C++ 异常机制
nm 统计出 607 个 itanium_demangle::* 符号,合计 105,431 B —— 这是 libc++abi 的
C++ 名字还原器(__cxa_demangle),单独一块就占了整个 ELF 的 10.2%,比 zlib 整个库还大。
它是怎么被拉进来的:
third_party/unrar7/dll.cpp用了catch (RAR_EXIT)/catch (std::bad_alloc&)(unpack.cpp/model.cpp里也有throw std::bad_alloc())- 只要 C++ 异常运行时存在,libc++abi 的
__cxa_throw链路就会引用__cxa_demangle(用于打印未捕获异常的类型名) - 链接器于是把整个
cxa_demangle.cpp拉进来 —— 一个深度内联的模板解析器, 展开成 607 个函数
连带被拖进来的还有 libunwind(21,953 B,栈回溯)和异常胶水(23,904 B),
以及散落在每个 C++ 目标文件里的 .eh_frame(58,904 B)/ .gcc_except_table(8,500 B)。
但这个 demangler 只在「未捕获异常」的诊断路径上才会被调用。
我们的 unrar7 走 DLL 模式,所有异常都在 dll.cpp 内部被 catch 掉,
程序逻辑永远走不到那条路径。
三、实测(同一 WSL、同一份源码、同一个链接器)
| # | 变更 | 结果 | 差值 |
|---|---|---|---|
| E0 | 重新链接(校验基线) | 1,034,328 | 0 |
| E1 | -Wl,--icf=all |
1,017,944 | −16,384 |
| E2 | 注入 __cxa_demangle 桩 |
886,872 | −147,456 |
| E3 | 桩 + --icf=all |
870,488 | −163,840 |
| E4 | -Wl,-z,pack-relative-relocs |
985,248 | −49,080 |
| E5 | -Wl,-z,noseparate-code |
1,034,328 | 0(无效) |
| E7 | 桩 + ICF + RELR | 854,176 | −180,152 |
| E8 | 第三方全部改 -Oz |
969,504 | −64,824 |
E4 单独能省 49 KB,但与 ICF 组合后只剩 16 KB —— ICF 已经合并掉了一批重定位。
四、落地方案
4.1 立即可用:__cxa_demangle 桩(−147 KB)
新增 src/demangle_stub.c:
/* 只提供 __cxa_demangle 的桩,让 libc++abi 里 105 KB 的名字还原器
* 不被链接进来。该函数只在打印「未捕获异常的类型名」时被调用;
* 返回 NULL 时调用方退回打印 mangled 名,不影响任何业务流程。 */
#include <stddef.h>
char *__cxa_demangle(const char *mangled, char *buf, size_t *len, int *status)
{
(void)mangled; (void)buf; (void)len;
if (status) *status = -1;
return NULL;
}
Makefile 里把它加进 COMMON_SRCS(PS5 与 Linux 两条链路都受益):
COMMON_SRCS := src/main.c src/websrv.c ... src/sevenz_mt.c src/demangle_stub.c
原理:链接器解析 __cxa_demangle 引用时,命令行上的 .o 优先于归档成员,
所以 libc++abi.a 里的 cxa_demangle.o 压根不会被取出。
安全性(已实测验证):
| 检查项 | 结果 |
|---|---|
__cxa_demangle 本体大小 |
14 B(我们的桩) |
itanium_demangle::* 符号残留 |
0 |
__cxa_throw |
存在 |
__cxa_begin_catch / __cxa_end_catch |
存在 |
_Unwind_Resume / __gxx_personality_v0 |
存在 |
唯一的行为变化:万一真的出现未捕获异常,std::terminate 打印的是 mangled 名
而不是可读名。解压逻辑、错误码、进度上报、HTTP 服务一概不受影响。
4.2 立即可用:ICF 代码折叠(−16 KB)
LDFLAGS := -Wl,--gc-sections -Wl,--icf=all
lld 的 identical code folding,合并字节完全相同的函数。工具链已确认为 LLVM LLD 18。
建议只加在 PS5 的 LDFLAGS,不动 Linux 链路(GNU ld 的 --icf 支持不完整)。
4.3 需真机验证:RELR 重定位压缩(−16 KB)
LDFLAGS += -Wl,-z,pack-relative-relocs
把 2,119 条 R_X86_64_RELATIVE(24 B/条)压成 RELR 位图格式(8 B/条)。
风险:需要 PS5 的 ELF 加载器认得 .relr.dyn。如果加载器只处理 .rela.dyn,
重定位根本不会执行 —— 表现是启动即崩。先在一台机器上验证再推广。
4.4 不建议作为默认项
| 项 | 收益 | 为什么不默认开 |
|---|---|---|
第三方改 -Oz |
−65 KB | 作用于 LZMA / Deflate / RAR 的热循环,解压速度有下降风险。要用先跑 tests/bench_driver.py 对比 |
关掉 PPMd(-DZ7_PPMD_SUPPORT) |
−10 KB | PPMd 压缩的 7z 就解不开了 —— 违背"功能完整" |
去掉 zlib deflate(只留 inflate) |
−18 KB | mz_strm_zlib_write 引用了它,需要桩或改库,收益/风险不划算 |
五、还能挖的(未实测,仅估算)
| 项 | 预估 | 代价 |
|---|---|---|
unrar7 去掉 C++ 异常(throw/catch 改错误码 + -fno-exceptions -fno-rtti) |
−100~110 KB | 改 vendored 代码,需回归 163 checks。可回收 .eh_frame 59 KB + .gcc_except_table 8.5 KB + libunwind 22 KB + 异常胶水 |
LTO(-flto=thin) |
−30~60 KB | 全量重编,第三方 .o 需统一编译选项;有一定概率反而提速 |
| 前端资源改 LZMA 压缩(复用已有解码器,替代 gzip) | ~−10 KB | 改 gen-asset-module.py + asset.c |
理论极限在 700 KB 上下(−32%),但边际成本递增:4.1 + 4.2 用 20 行代码换 164 KB, 而再往下 100 KB 要动 vendored 源码或验证加载器行为。
六、推荐执行顺序
落 4.1 + 4.2 → 构建✅ 已完成(2026-09-20),产物 870,488 B- 跑 163 checks(
tests/run-tests.sh+tests/run-sevenz-tests.sh)确认无回归 - PS5 真机跑一轮 ZIP / RAR / 7z(含分卷)解压,确认行为不变
- 真机验证 4.3(RELR)后再决定是否加入
- 有需要再评估第五节的三项
本次改动只动链接期(新增一个 TU + 一个 lld 参数),未触碰任何解压逻辑, 因此 163 checks 的预期是"逐条不变"。