【问题标题】:How can I analyze the size of my firmware image?如何分析固件映像的大小?
【发布时间】:2018-03-06 16:11:30
【问题描述】:

我们目前正在使用他们的 ESP-IDF 框架为基于流行的 ESP32 芯片的 IoT 产品开发固件,该框架使用名为 xtensa (https://github.com/icamgo/xtensa-toolchain) 的 GCC/G++ 工具链构建二进制文件。

最近,我注意到二进制文件的大小变得相当大(接近 1 MB),因此决定查看一下并尝试减小它。 NDEBUG 已定义,-Os 已启用,输出为 stripped。

基本上,工具链会生成一个.elf 文件,所以我看了一下它的内容:

nm -S -C --size-sort <my-app>.elf

六个最大的函数(大小为 6 kB-12 kB)是:

4011b24c 0000187b T __ssvfscanf_r
400f9f38 00001ffa T _svfiprintf_r
400f2aa4 000020fe T _vfiprintf_r
4012005c 000030d2 T _svfwprintf_r
400ef4d8 000030de T _svfprintf_r
400f50dc 000031e6 T _vfprintf_r

所以,我的固件映像中最大的函数是 vfprintf 和朋友,仅二进制大小就加起来大约 60 kB。它们为什么这么大?我怎样才能减小它们的大小或摆脱它们(我根本不需要 vfprintf,因为我在微控制器上没有文件系统)?

是否有任何进一步的技术可以减少二进制文件的大小?我将如何继续我的任务?

编辑(澄清优化原因):

有不同版本的 ESP32,具有高达 16 MB 的闪存。我们使用的有 4 MB。其中 1 MB 用于存储固定的服务器证书、受信任的 URL 配置选项。而且,由于我们需要 OTA 更新功能,因此我们需要保留与应用程序映像消耗的相同数量的闪存,以供新版本使用。这为我们的应用程序映像留下了 1.5 MB 的闪存,这与我们当前的 1 MB 相差不远。因此,我认为在这个问题完全阻止我们引入新功能之前考虑减小尺寸是合理的。

我确实意识到 60 kB 的不需要的 vfprintf() 函数只是 1 MB 的一小部分,但我们确实需要很多实际有用的库(用于加密的 mbedtls、完整的 IP 堆栈、瘦 Web 服务器、. ..)。我无法摆脱这些,所以我想通过删除我没有任何用处的函数来尽可能和可行地减小大小

【问题讨论】:

  • ??所以你有 12kiB 的不需要的代码,但你的固件还不到 1MiB? ~990kiB 刚刚“发生”在数千个微小的函数中,全部由你编写,或者我们应该如何理解它?您是否彻底检查了链接图,这是怎么回事? (谷歌给了我一些相关的文章,实际上专注于嵌入式,只是不同的平台embeddedrelated.com/showarticle/900.php)......减少二进制大小的技术=减少代码。例如,第一个计算器确实使用它自己的解释器来适应可用的 ROM (2kiB IIRC)。然后你又有 1+Mi ... :-o
  • 如果它是物联网,它可能应该允许安全 [数字签名] 补丁、一些恢复模式/恢复出厂设置、一些加固的 TCP/IP 堆栈(可能是 IPv6 之一)等等......然后1MiB 可能不是那么多,我希望更像 10-100MiB 大小。
  • 你可能不需要vfprintf byt printf 可能是用它实现的。 printf 是对 stdio 流上的 fprintf 的调用,而后者又是对 vfprintf 的调用。剥离符号会减小 目标代码 而不是二进制文件的大小。 strip 删除的符号信息仅由开发主机调试器使用,而不是二进制映像的一部分。您应该指示链接器输出映射文件,而不是对 .elf 文件进行逆向工程。如果你的图像真的是 1Mb,那么 60kb 就是 6%——这真的是你的问题吗?数据常量或初始化器呢?
  • ESP32 有 16Mb 的闪存;也许您过早地担心这个问题?
  • @old_timer : 资源有限!?我的梦想是 16Mb!

标签: c++ g++ embedded firmware


【解决方案1】:

考虑单个函数的大小不是一个合理的方法。单个“微小”函数在其调用图中可能有数百个同样微小的依赖关系,这些依赖关系总体上构成一个巨大的块。例如以下:

int main()
{
    for(;;)
    {
        do_statemachine() ;
    }

    return 0 ;
}

main() 会很小,但最终会导致所有应用程序的其余部分因为do_statemachine() 所做的任何事情而被链接,这可能是任何大小。您需要考虑函数的总大小以及所有它的依赖项。

静态或常量数据初始化器的总大小,也需要考虑存储在ROM中。

您应该使用链接器来生成地图文件并调用图形 - 这可能比在事件后使用 nm 更有用。

关于您问题中的特定符号,您必须问自己您在 stdio 中调用了什么?例如printf 需要流访问(对于stdout)、格式说明符解析和可变参数遍历——这是vfprintf 提供的全部。如果不是这样,您就会有重复的代码,虽然您可能链接的函数更少,但它们都会非常很大并且可能表现出不同的行为。您在链接中具有面向“文件”的功能这一事实并不是专门的问题; stdio 对流而不是文件进行操作——“文件”是概念性的,而不是物理的。如果您没有将您的库挂钩到文件系统(或者如果工具中尚未提供),则不会包含文件系统代码。低级流访问由可能支持也可能不支持文件访问的低级 I/O 函数执行。

另一种可能是库缺乏粒度——如果所有这些函数都定义在同一个对象模块中,链接器将别无选择,只能链接它们,即使它们没有被引用。这或许可以解释为什么链接中有整数、浮点和宽字符版本。

【讨论】:

  • 我还要补充一点,如果 printf 及其朋友支持完全符合标准,它们往往会非常大。有时,开发环境为较小的尺寸提供较低的功能。但是 printf 可以以阴险的方式侵入,例如使用 ASSERT 宏。我经常发现自己在链接器映射中不断寻找我不想要的对标准库函数的依赖关系。欢迎来到我们的世界。
  • 感谢您的建议。我确实有一个.map 文件,但据我所知,它只包含一个映射“函数名”=>“它来自的目标文件”。我在这里想念什么吗?我将如何生成调用图?我按照here 的建议尝试了radare2 工具,但我还没有找到可以打开生成的点文件的程序。它们都因内存不足错误而失败(点文件大小仅为 2.8 MB)
  • 我明白你关于printf() 基于vfprintf() 的观点,这是有道理的。
  • 也许 GNU 链接器不会为您执行此操作(我目前不使用 GCC),并且第三方工具可能在源代码级别工作,而不是深入到对象库中。毫无疑问,您的 C 库的源代码可用(github.com/jcmvbkbc/newlib-xtensa)?请注意,elf 文件大小与二进制大小不同。
  • 您的最后一段传统上是正确的。但是,链接时优化 (LTO) 并不一定如此。
【解决方案2】:

这些符号出现的地方有更多您不需要的与 wchar 相关的符号。 您可以通过构建 ESP32 工具链来摆脱这些符号

-fdata-sections -ffunction-sections 

为 newlib 启用。还要在 libstdc++ 上设置标志 --disable-wchar_t。然后使用-Wl,--gc-sections 删除这些部分。

您也可以尝试使用-flto,它应该为您做同样的事情 - 但我在使用 lto 构建 newlib 和 libstdc++ 时遇到了问题。似乎 libstdc++ 的构建工具与 newlib 档案有问题。 尽管如此,如果可能,lto 是您应该首选的,因为它还可以很好地检测 ODR 违规或其他可能被逐单元编译隐藏的损坏代码。

【讨论】:

    猜你喜欢
    • 2017-09-20
    • 1970-01-01
    • 1970-01-01
    • 2016-06-14
    • 2015-01-01
    • 2022-01-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多