4、文件系统裁剪(下):移除不必要的文档和locale、使用sstrip/arm-strip进一步减小体积、合并重复的库文件、使用uclibc/musl替代glibc

上一节我们聊了文件系统裁剪的基础思路,比如删掉没用的二进制、清理缓存文件。今天咱们继续深入,把裁剪做到极致。说白了,嵌入式设备的存储空间就是寸土寸金,能省一点是一点。

我个人习惯把裁剪分成两个阶段:第一阶段是「粗剪」,去掉明显没用的东西;第二阶段是「精修」,用各种工具和技巧把每一字节都榨干。今天讲的就是精修阶段的核心操作。

移除不必要的文档和locale

你想想看,一个嵌入式设备跑在板子上,谁会去读man手册?谁会去切换系统语言?但默认的Buildroot或Yocto构建出来的文件系统,往往会带上大量文档和locale数据。这些东西加起来,轻松吃掉几MB甚至几十MB的空间。

我曾在项目中遇到过一件事:一个原本设计为8MB Flash的产品,固件死活塞不进去。排查了半天,发现光是/usr/share/locale目录就占了2.3MB。删掉之后,空间问题直接解决。

具体怎么做?我一般这么处理:

  • 文档清理:删除/usr/share/doc、/usr/share/man、/usr/share/info等目录。这些对运行时毫无用处。
  • locale清理:只保留目标语言(比如zh_CN或en_US),其余全部删除。或者干脆只保留C.UTF-8,这是最保险的做法。
  • 其他无用数据:比如/usr/share/zoneinfo里只保留需要的时区,/usr/share/misc里的magic文件也可以删掉。

经验之谈:在Buildroot中,可以通过配置BR2_TARGET_ROOTFS_TAR和BR2_PACKAGE_LOCALE相关选项来控制locale的生成。我建议直接关闭BR2_ENABLE_LOCALE_PURGE,然后手动指定保留的locale列表。

使用sstrip/arm-strip进一步减小体积

strip工具大家应该不陌生,它能去掉ELF文件中的符号表和调试信息。但普通的strip还不够狠。这里我要介绍两个更猛的家伙:sstrip和arm-strip。

sstrip(super strip)是专门针对嵌入式场景设计的。它不仅能去掉符号表,还能把ELF文件中的节头表(section headers)也干掉。节头表在运行时根本不需要,只对链接器和调试器有用。去掉之后,文件体积能再缩小10%~20%。

举个例子,一个原本用strip处理过的busybox二进制是500KB,再用sstrip处理一遍,可能变成420KB。别小看这80KB,在嵌入式世界里,这就是巨大的胜利。

# 使用sstrip处理单个文件
sstrip /path/to/binary

# 批量处理整个目录
find /target/usr/bin -type f -executable -exec sstrip {} \;

注意:sstrip处理过的文件不能再被gdb调试,也不能再被strip。所以一定要在最终发布前做这一步。我曾经因为顺序搞反,导致调试阶段无法定位问题,折腾了一整天。

对于ARM平台,还有arm-strip工具。它是专门针对ARM架构优化的strip版本,能更好地处理ARM特有的ELF格式。如果你用的是ARM Cortex-A系列芯片,强烈建议用arm-strip代替通用的strip。

合并重复的库文件

你有没有注意过,文件系统里经常出现同一个库的多个版本?比如libcrypto.so.1.0.0和libcrypto.so.1.1同时存在。这是因为不同的应用程序依赖了不同版本的库。

但说实话,很多情况下这些库的ABI是兼容的。我们可以通过符号链接或硬链接来合并它们。更激进的做法是:用librsync或hardlink工具扫描整个文件系统,找出内容完全相同的文件,然后用硬链接代替。

# 使用hardlink工具合并重复文件
hardlink -t /target/usr/lib   # 先预览哪些文件可以合并
hardlink /target/usr/lib      # 执行合并

我曾在项目中用这个技巧,把/usr/lib目录从12MB压缩到了9MB。省出来的3MB,刚好够放一个新的功能模块。

小技巧:合并库文件时,一定要检查它们的SONAME是否一致。如果SONAME不同,说明ABI不兼容,强行合并会导致运行时崩溃。这个坑我踩过,教训深刻。

使用uclibc/musl替代glibc

glibc功能强大,但体积也大。一个完整的glibc加上locale数据,轻松超过2MB。对于嵌入式设备来说,这太奢侈了。

这时候,uclibc和musl就派上用场了。它们都是轻量级的C标准库实现,体积只有glibc的几分之一,同时保持了良好的兼容性。

特性 glibc uclibc musl
典型体积 ~2MB ~400KB ~600KB
POSIX兼容性 完全 大部分 完全
动态链接支持 完善 有限 完善
适用场景 桌面/服务器 深度嵌入式 通用嵌入式

我个人更倾向于musl。为什么?因为它的代码质量高,安全审计严格,而且对标准C的兼容性比uclibc好得多。我在一个基于ARM Cortex-A7的项目中,把glibc换成musl后,文件系统整体体积减少了35%,而且所有应用程序都运行正常。

当然,切换C库不是小事。你需要重新编译整个工具链和所有应用程序。在Buildroot中,只需要修改配置:

# 在Buildroot中启用musl
BR2_TOOLCHAIN_BUILDROOT_MUSL=y

避坑指南:我曾经在一个项目里直接替换了glibc为musl,结果发现某个第三方库用了glibc特有的__progname变量,导致段错误。所以,切换前一定要做完整的兼容性测试,特别是那些闭源的二进制库。

知识体系总览

下面这张图总结了文件系统裁剪的核心逻辑,你可以把它当作操作指南:

文件系统裁剪核心流程 原始文件系统 第一层:粗剪(删除无用文件) 第二层:精修(文档/locale清理) 第三层:工具优化(sstrip/arm-strip/合并库) 第四层:替换C库(uclibc/musl) 极致精简的文件系统

这张图展示了裁剪的四个层次。每一层都能带来可观的体积缩减,但越往后操作越复杂,风险也越高。我的建议是:先做前两层,如果空间还不够,再考虑第三层和第四层。

好了,关于文件系统裁剪的内容就讲到这里。这些技巧我在多个项目中验证过,效果都很不错。你可以在自己的开发板上试试,看看能省出多少空间。

可可精品资料库