Hotdry.

Article

从链接器脚本到段布局:可执行文件字节级优化实战

探索可执行文件字节级优化技术,从链接器脚本到段布局的实战策略,提供可落地的编译参数与监控要点。

2026-06-04systems

在资源受限的嵌入式环境或需要快速分发的场景中,每一个字节都可能成为关键瓶颈。可执行文件的膨胀往往并非源于业务代码本身,而是链接过程中的隐形成本:过度的段对齐、未使用的库函数、冗余的调试信息。本文从链接器层面切入,提供一套可落地的字节级优化策略。

链接器脚本:掌控内存布局的第一道防线

链接器脚本(Linker Script)是控制 ELF 文件结构的直接手段。默认配置往往采用保守的对齐策略,导致段与段之间存在大量填充字节。通过自定义链接器脚本,可以精确控制各段的内存布局,消除不必要的对齐间隙。

关键优化点包括:

精确内存区域定义:在 MEMORY 指令中严格限定 ROM 和 RAM 的起始地址与长度,避免链接器过度分配。例如,将只读数据段(.rodata)与代码段(.text)紧密排列,而非按默认的页面边界对齐。

段合并策略:通过 SECTIONS 指令将频繁访问的小段合并,减少段头表(Section Header Table)的开销。对于嵌入式目标,可将初始化数据(.data)与零初始化数据(.bss)的布局优化,确保初始化值在 Flash 中连续存储。

对齐参数调优:链接器默认可能使用 4KB 或 64KB 的页面对齐,这在小型固件中极其浪费。通过 -Wl,-z,max-page-size=0x1000 或更小的值(如 0x100),可以显著减少段间填充。

编译器标志与死代码消除

链接器层面的优化需要编译器的配合。现代工具链提供了细粒度的段控制机制:

函数级与数据级分段:使用 -ffunction-sections-fdata-sections 编译选项,使每个函数和全局变量进入独立的段(如 .text.function_name)。这为链接器的垃圾回收创造了条件。

段垃圾回收:链接时添加 --gc-sections 标志,链接器将自动剔除未被引用的段。配合 --print-gc-sections 可以查看被移除的内容,验证优化效果。

链接时优化(LTO):启用 -flto 后,编译器在链接阶段获得全局视图,能够跨编译单元内联函数、消除重复代码、折叠相同常量。LTO 对于模板密集型 C++ 代码尤为有效,可显著减少因模板实例化导致的代码膨胀。

运行时特性裁剪:对于不需要异常处理和 RTTI 的嵌入式场景,添加 -fno-exceptions-fno-rtti 可移除相关支持代码。使用 -nostdlib-nodefaultlibs 配合精简的 C 运行时库,能进一步削减体积。

分析与测量:数据驱动的优化

优化必须建立在测量之上。工具链提供了多层次的体积分析手段:

基础体积统计size 工具可快速查看 text、data、bss 段的占用,但需注意 data 段的初始化值实际也存储在 Flash 中。更准确的计算应为 Flash = text + data,RAM = data + bss。

符号级分析nm --print-size --size-sort --radix=d 按大小排序列出所有符号,快速定位体积大户。对于 ARM 目标,arm-none-eabi-nm 提供相同功能。

可视化工具:Puncover 提供层次化的代码体积视图,支持按模块、文件、函数分析,并展示调用关系。Google 的 Bloaty 则适合 CI 集成,可对比两个 ELF 文件的体积差异,精确定位增长来源。

链接器内存使用报告:添加 -Wl,--print-memory-usage 可在链接完成后输出各内存区域的使用率,便于快速验证链接器脚本配置。

实战参数清单

以下参数组合适用于 GCC/Clang 工具链的固件体积优化:

# 编译阶段
CFLAGS="-Os -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -flto -g"

# 链接阶段
LDFLAGS="-Wl,--gc-sections -Wl,--print-gc-sections -Wl,--print-memory-usage \
         -Wl,-z,max-page-size=0x1000 -T custom_linker.ld -nostartfiles"

# 后处理:符号剥离
strip --strip-debug output.elf

对于需要保留调试信息的场景,使用 --only-keep-debug 将符号分离到独立文件,主 ELF 保持精简。

风险与注意事项

字节级优化并非没有代价。过度激进的优化可能引入以下风险:

初始化顺序依赖:段重排可能影响 C++ 全局对象的构造顺序,需验证启动流程。

动态链接副作用:若使用动态库,过度裁剪可能导致运行时符号缺失。静态链接场景下此风险较低。

调试信息丢失:符号剥离后,现场调试能力受限。建议保留带符号的构建产物用于事后分析。

对齐违规:过小的对齐参数可能在某些架构上触发性能惩罚或硬件异常,需在目标平台验证。

结语

可执行文件优化是一个系统工程,需要从编译、链接到后处理的全链路配合。链接器脚本提供了最底层的布局控制权,配合编译器的段级优化和垃圾回收,可以在不修改业务代码的前提下实现显著瘦身。关键在于建立测量 - 分析 - 优化的闭环,用数据指导每一次调整。


参考来源

  • Ostrovskiy Alexander, "Shrinking Your Executable Size", 2025
  • François Baldassari, "Tools for Firmware Code Size Optimization", Memfault Interrupt, 2019

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com