最小化二进制文件大小

[English]

ESP-IDF 构建系统会编译项目和 ESP-IDF 中所有源文件,但只有程序实际引用的函数和变量才会链接到最终的二进制文件中。在某些情况下,需要减小固件二进制文件的总大小,例如,为使固件适配 flash 分区大小。

要减小固件二进制文件总大小,首先要找到导致其大小增加的原因。

测量静态数据大小

为了优化固件二进制文件大小和内存使用,需要测量项目中静态分配的 RAM (data, bss),代码 (text) 和只读数据 (rodata)。

使用 idf.py 的子命令 sizesize-componentssize-files 可以输出项目使用内存概况:

备注

添加 -DOUTPUT_FORMAT=csv-DOUTPUT_FORMAT=json,即可用 CSV 或 JSON 格式输出文件。

数据大小概况 idf.py size

$ idf.py size
[...]
Total sizes:
Used static DRAM:   10608 bytes ( 170128 remain, 5.9% used)
      .data size:    8464 bytes
      .bss  size:    2144 bytes
Used static IRAM:   48834 bytes (  82238 remain, 37.3% used)
      .text size:   47807 bytes
   .vectors size:    1027 bytes
Used Flash size :  117391 bytes
           .text:   80103 bytes
         .rodata:   37032 bytes
Total image size:  174689 bytes (.bin may be padded larger)

该输出结果细分了固件二进制文件中所有静态内存区域的大小:

$ idf.py size
[...]
Total sizes:
Used static DRAM:   10608 bytes ( 170128 remain, 5.9% used)
      .data size:    8464 bytes
      .bss  size:    2144 bytes
Used static IRAM:   48834 bytes (  82238 remain, 37.3% used)
      .text size:   47807 bytes
   .vectors size:    1027 bytes
Used Flash size :  117391 bytes
           .text:   80103 bytes
         .rodata:   37032 bytes
Total image size:  174689 bytes (.bin may be padded larger)
  • Used static DRAM:编译时分配的 DRAM 大小。 remain 表示在运行时可用作堆内存的 DRAM 余量。请注意,由于元数据开销、实现限制和启动时的堆分配,实际的 DRAM 堆会更小。

    • .data size:编译时为 .data (即所有初始化值为非零值的静态变量)分配的 DRAM 大小。 .data 还在二进制映像中占用空间来存储非零初始化值。

    • .bss size:编译时为 .bss (即所有初始化值为零的静态变量)分配的 DRAM 大小。.bss 不会在 flash 中占用额外空间。

  • Used static IRAM:编译时分配的 IRAM 大小。 remain 表示在运行时可用作堆内存的 IRAM 余量。请注意,由于元数据开销、实现限制和启动时的堆分配,实际的 IRAM 堆会更小。

    • .text size:用于 .text 的 IRAM 大小(即所有从 IRAM 执行的代码)。由于代码最初存储在 .text 中,在启动时才会复制到 IRAM,因此 .text 在二进制映像中也会占用空间。

  • Used Flash size:使用的 flash 大小(不包括 DRAM 和 IRAM 的使用量)。

    • .text:用于 .text (即通过 flash 缓存执行的所有代码,请参阅 IROM)的 flash 大小。

    • .rodata:用于 .rodata (即通过 flash 缓存加载的只读数据,参阅 DROM)的 flash 大小。

  • Total image size 是二进制文件的预估总大小。

组件使用概况 idf.py size-components

idf.py size 的输出结果不够详细,无法找出导致二进制文件过大的主要原因。要进行更详细的分析,请使用 idf.py size-components

$ idf.py size-components
[...]
    Total sizes:
 DRAM .data size:   14956 bytes
 DRAM .bss  size:   15808 bytes
Used static DRAM:   30764 bytes ( 149972 available, 17.0% used)
Used static IRAM:   83918 bytes (  47154 available, 64.0% used)
      Flash code:  559943 bytes
    Flash rodata:  176736 bytes
Total image size:~ 835553 bytes (.bin may be padded larger)
Per-archive contributions to ELF file:
            Archive File DRAM .data & .bss & other   IRAM   D/IRAM Flash code & rodata   Total
           libnet80211.a       1267   6044       0   5490        0     107445    18484  138730
               liblwip.a         21   3838       0      0        0      97465    16116  117440
            libmbedtls.a         60    524       0      0        0      27655    69907   98146
         libmbedcrypto.a         64     81       0     30        0      76645    11661   88481
                 libpp.a       2427   1292       0  20851        0      37208     4708   66486
                  libc.a          4      0       0      0        0      57056     6455   63515
                libphy.a       1439    715       0   7798        0      33074        0   43026
     libwpa_supplicant.a         12    848       0      0        0      35505     1446   37811
           libfreertos.a       3104    740       0  15711        0        367     4228   24150
          libnvs_flash.a          0     24       0      0        0      14347     2924   17295
          libspi_flash.a       1562    294       0   8851        0       1840     1913   14460
         libesp_system.a        245    206       0   3078        0       5990     3817   13336
            libesp-tls.a          0      4       0      0        0       5637     3524    9165
[... removed some lines here ...]
            libesp_rom.a          0      0       0    112        0          0        0     112
                libcxx.a          0      0       0      0        0         47        0      47
                   (exe)          0      0       0      3        0          3       12      18
             libesp_pm.a          0      0       0      0        0          8        0       8
            libesp_eth.a          0      0       0      0        0          0        0       0
               libmesh.a          0      0       0      0        0          0        0       0

idf.py size-components 输出的前几行与 idf.py size 相同,此外还会输出 Per-archive contributions to ELF file 表格,显示每个静态库对最终二进制文件大小的贡献程度。

通常,每个组件都会构建一个静态库归档文件,尽管部分是由特定组件包含的二进制库,例如, esp_wifi 组件包含了 libnet80211.a。此外,这里还列出了一些工具链库,例如 libc.alibgcc.a,用于提供 C/C++ 标准库和工具链内置功能。

对于只有一个 main 组件的简单项目,可在 libmain.a 目录下找到所有项目代码。若项目包含其特有组件(参阅 构建系统),则每个组件将单独在一行中显示。

该表格按静态库归档文件对最终二进制文件大小的贡献程度降序排序。

各列含义如下:

  • DRAM .data & .bss & other - .data.bss 分别与上方显示的总数相同。两者都是静态变量,且都会减少运行时的可用 RAM,但 .bss 不会增加二进制文件大小。 other 列指任何会增加 RAM 大小的自定义数据段,该值通常为 0。

  • IRAM - 该列与上方显示的总数相同,表示链接到从 IRAM 执行的代码,这些代码占用二进制文件空间,并且会减少执行 HEAP_CAP_32BIT 时可动态分配的 IRAM。

  • D/IRAM - 显示了 IRAM 占用的空间。D/IRAM 占用的空间会减少运行时可用作堆内存的 DRAM 空间。

  • Flash code & rodata - 这些值与上方显示总数相同,指通过 flash 缓存访问的 IROM 和 DROM 空间,对二进制文件大小的贡献。

源文件使用概况 idf.py size-files

要了解更多详情,请运行 idf.py size-files,获取每个目标文件对最终二进制文件大小的贡献概况。每个目标文件对应一个单独的源文件。

$ idf.py size-files
[...]
Total sizes:
 DRAM .data size:   14956 bytes
 DRAM .bss  size:   15808 bytes
Used static DRAM:   30764 bytes ( 149972 available, 17.0% used)
Used static IRAM:   83918 bytes (  47154 available, 64.0% used)
      Flash code:  559943 bytes
    Flash rodata:  176736 bytes
Total image size:~ 835553 bytes (.bin may be padded larger)
Per-file contributions to ELF file:
             Object File DRAM .data & .bss & other   IRAM   D/IRAM Flash code & rodata   Total
     x509_crt_bundle.S.o          0      0       0      0        0          0    64212   64212
                wl_cnx.o          2   3183       0    221        0      13119     3286   19811
           phy_chip_v7.o        721    614       0   1642        0      16820        0   19797
       ieee80211_ioctl.o        740     96       0    437        0      15325     2627   19225
                    pp.o       1142     45       0   8871        0       5030      537   15625
      ieee80211_output.o          2     20       0   2118        0      11617      914   14671
         ieee80211_sta.o          1     41       0   1498        0      10858     2218   14616
        lib_a-vfprintf.o          0      0       0      0        0      13829      752   14581
       lib_a-svfprintf.o          0      0       0      0        0      13251      752   14003
             ssl_tls.c.o         60      0       0      0        0      12769      463   13292
             sockets.c.o          0    648       0      0        0      11096     1030   12774
                 nd6.c.o          8    932       0      0        0      11515      314   12769
       phy_chip_v7_cal.o        477     53       0   3499        0       8561        0   12590
                    pm.o         32    364       0   2673        0       7788      782   11639
        ieee80211_scan.o         18    288       0      0        0       8889     1921   11116
      lib_a-svfiprintf.o          0      0       0      0        0       9654     1206   10860
       lib_a-vfiprintf.o          0      0       0      0        0      10069      734   10803
          ieee80211_ht.o          0      4       0   1186        0       8628      898   10716
       phy_chip_v7_ana.o        241     48       0   2657        0       7677        0   10623
              bignum.c.o          0      4       0      0        0       9652      752   10408
              tcp_in.c.o          0     52       0      0        0       8750     1282   10084
                   trc.o        664     88       0   1726        0       6245     1108    9831
               tasks.c.o          8    704       0   7594        0          0     1475    9781
          ecp_curves.c.o         28      0       0      0        0       7384     2325    9737
                 ecp.c.o          0     64       0      0        0       8864      286    9214
      ieee80211_hostap.o          1     41       0      0        0       8578      585    9205
                  wdev.o        121    125       0   4499        0       3684      580    9009
             tcp_out.c.o          0      0       0      0        0       5686     2161    7847
                 tcp.c.o          2     26       0      0        0       6161     1617    7806
       ieee80211_input.o          0      0       0      0        0       6797      973    7770
                 wpa.c.o          0    656       0      0        0       6828       55    7539
[... additional lines removed ...]

文件总大小概况下方会显示 Per-file contributions to ELF file 表格。

该表格的列与上文运行 idy.py size-components 显示的列相同,但该表格的粒度更细,展示了每个目标文件对二进制文件大小的贡献。

例如,文件 x509_crt_bundle.S.o 对总固件大小贡献了 64,212 字节,全都存储在 flash 中的 .rodata 区域。由此可以推知,该应用程序正在使用 ESP x509 证书包 功能。如果不使用该功能,固件大小至少可以减少 64,212 字节。

某些目标文件从二进制库中链接至此,因此无法找到对应源文件。要确定一个源文件属于哪个组件,通常可以在 ESP-IDF 源代码树中搜索,或者在 链接器映射文件 中查找完整路径。

比较两个二进制文件

如果某些改动影响了二进制文件大小,可以使用 ESP-IDF 工具来详细分析文件大小的确切差异。

该操作不是通过运行 idf.py 进行的,而是需要直接运行 Python 工具 esp_idf_size

执行该操作,首先需要在构建目录中找到链接器映射文件 PROJECTNAME.mapesp_idf_size 工具会基于链接器映射文件的输出结果分析文件大小差异。

要与另一个二进制文件进行比较,还需要保存该文件对应的 .map 文件,该文件位于构建目录中。

例如,要比较两个构建文件,其中一个使用默认的 CONFIG_COMPILER_OPTIMIZATION Debug (-Og) 配置,而另一个使用 Optimize for size (-Os) 配置:

$ python -m esp_idf_size --diff build_Og/https_request.map build_Os/https_request.map
<CURRENT> MAP file: build_Os/https_request.map
<REFERENCE> MAP file: build_Og/https_request.map
Difference is counted as <CURRENT> - <REFERENCE>, i.e. a positive number means that <CURRENT> is larger.
Total sizes of <CURRENT>:                                                 <REFERENCE>     Difference
 DRAM .data size:   14516 bytes                                                 14956           -440
 DRAM .bss  size:   15792 bytes                                                 15808            -16
Used static DRAM:   30308 bytes ( 150428 available, 16.8% used)                 30764           -456 (   +456 available,      +0 total)
Used static IRAM:   78498 bytes (  52574 available, 59.9% used)                 83918          -5420 (  +5420 available,      +0 total)
      Flash code:  509183 bytes                                                559943         -50760
    Flash rodata:  170592 bytes                                                176736          -6144
Total image size:~ 772789 bytes (.bin may be padded larger)                    835553         -62764

Difference 列可以看出,改变该设置导致整个二进制文件减小了 60 KB 以上,并且可用的 RAM 增加了 5 KB 以上。

还可以使用 diff 模式来输出表格,显示组件级(静态库)的差异:

备注

运行 esp_idf_size 时可以使用 --format 选项输出 JSON 或 CSV 格式的结果。

python -m esp_idf_size --archives --diff build_Og/https_request.map build_Oshttps_request.map

同样适用于比较单个源文件级的差异:

python -m esp_idf_size --files --diff build_Og/https_request.map build_Oshttps_request.map

了解将输出写入文件等其他选项,可以输入 --help 查看完整列表。

链接器失败时显示文件大小

如果被分配的静态内存大小超越上限,链接器会失败并显示错误信息,例如 DRAM segment data does not fitregion `iram0_0_seg' overflowed by 44 bytes 等。

在这些情况下, idf.py size 也无法成功执行。然而,通过手动运行 esp_idf_size,可以查看 部分静态内存使用情况 。内存使用情况将不包含无法链接的变量,因此仍然会显示有部分可用空间。

映射文件参数为构建目录下的 <projectname>.map 文件。

python -m esp_idf_size build/project_name.map

还可以查看类似于 size-componentssize-files 输出的内容:

python -m esp_idf_size --archives build/project_name.map
python -m esp_idf_size --files build/project_name.map

链接器映射文件

备注

这是一种非常有用的进阶分析方法。可以先跳转到 减小总体文件大小,以后再详阅这一部分。

分析工具 idf.py size 通过解析 GNU binutils 的“链接器映射文件”来输出结果,该文件囊括了链接器在创建(即链接到)最终固件二进制文件时的所有操作。

链接器映射文件本身是纯文本文件,因此可以进行读取并准确了解链接器的操作,但这些文件非常复杂冗长,通常有 100,000 行甚至更多。

映射文件分为多个部分,每个部分各有标题,包括:

  • Archive member included to satisfy reference by file (symbol)

    • 该列表显示了链接器链接各个目标文件时所搜寻的特定符号(函数或变量)。

    • 要了解二进制文件包含特定目标文件的原因,可以查看该列表以及文件末尾的 Cross Reference Table

    备注

    请注意,并非每个显示在该列表中的目标文件最后都会出现在最终二进制文件中,有些目标文件可能会列在 Discarded input sections 中。

  • Allocating common symbols

    • 该列表显示了部分全局变量及其大小。常见符号在 ELF 二进制文件中具有特定含义,但 ESP-IDF 并未广泛使用这些符号。

  • Discarded input sections

    • 在链接器读取目标文件时,会将一些输入段作为文件的一部分读取并准备链接到最终的二进制文件中,但由于没有其他部分引用这些输入段,这些段最终会被丢弃。

    • 对于 ESP-IDF 项目来说,这个列表可能会非常长,因为我们将每个函数和静态变量都编译到一个独立的段中,以最小化最终二进制文件的大小。具体而言,ESP-IDF 将使用编译器选项 -ffunction-sections -fdata-sections 和链接器选项 --gc-sections

    • 在这个列表中出现的条目 不会 对最终的二进制文件大小产生影响。

  • Memory ConfigurationLinker script and memory map

    • 这两部分相互关联。输出结果的一部分来自由 构建系统 提供的链接器命令行和链接脚本,部分链接脚本由 ESP-IDF 项目通过 链接器脚本生成机制 功能生成。

    • 在 map 文件的 Linker script and memory map 输出中,会显示链接到最终二进制文件中的每个符号(函数或静态变量)及其地址(以 16 位十六进制数字表示)和长度(也以十六进制表示),还有链接的库和目标文件(可以用于确定组件和源文件)。

    • 在所有占用最终 .bin 文件的输出段之后, memory map 还会显示一些 ELF 文件中用于调试的段,如 .debug_* 等。这些段不会对最终的二进制文件大小产生影响,且这些符号的地址数值很小,从 0x0000000000000000 开始递增。

  • Cross Reference Table

    • 该表格显示了引用了各个符号(函数或静态变量)的目标文件。了解二进制文件包含某个特定符号的原因,可参考该表格以确定引用特定符号的目标文件。

    备注

    Cross Reference Table 不仅包含最终二进制文件中的符号,还包含已丢弃的段内符号。因此,某个符号出现在该表中并不意味着最终二进制文件包含这一符号,需要单独检查。

备注

链接器映射文件由 GNU binutils 的链接器 ld 而非由 ESP-IDF 生成。本快速概览专从 ESP-IDF 构建系统的角度编写而成,建议自行搜索更多关于链接器映射文件格式的信息。

减小总体文件大小

可以通过以下配置选项减小几乎所有 ESP-IDF 项目最终二进制文件的大小:

备注

除了上述众多配置项之外,还有一些配置选项在更改为非默认设置时会增加二进制文件的大小,这些选项未在此列出。配置项的帮助文本中通常会阐明显著增加二进制文件大小的设置。

针对性优化

以下二进制文件大小优化适用于特定的组件或函数:

Wi-Fi

ADC

Bluetooth NimBLE

如果使用 NimBLE-based Host APIs,要减小二进制文件的大小,可以执行以下操作:

lwIP IPv6

  • CONFIG_LWIP_IPV6 设置为 false 可以减小 lwIP TCP/IP 堆栈的大小,但将仅支持 IPv4。

    备注

    如果禁用 IPv6,ASIO 端口 等组件将无法使用。

lwIP IPv4

  • 如果不需要 IPv4 连接功能,将 CONFIG_LWIP_IPV4 设置为 false 可以减小 lwIP 的大小,使其成为仅支持 IPv6 的 TCP/IP 堆栈。

    备注

    在禁用 IPv4 支持之前,请注意,仅支持 IPv6 的网络环境尚未普及,必须在本地网络中提供支持,例如,由互联网服务供应商提供支持,或使用受限制的本地网络设置。

Newlib Nano 格式化

ESP-IDF 的 I/O 函数( printf()scanf() 等)默认使用 Newlib 的 “完整” 格式化功能。

启用配置选项 CONFIG_NEWLIB_NANO_FORMAT 将使 Newlib 切换到 Nano 格式化模式。这种模式的代码量更小,并且大部分内容被编译到了 ESP32 的 ROM 中,因此不需要将其添加至二进制文件中。

具体的二进制文件大小差异取决于固件使用的功能,但通常为 25 KB 到 50 KB。

启用 Nano 格式化会减少调用 printf() 或其他字符串格式化函数的堆栈使用量,参阅 栈内存大小优化

“Nano” 格式化不支持 64 位整数或 C99 格式化功能。请在 Newlib README 文件 中搜索 --enable-newlib-nano-formatted-io 来获取完整的限制列表。

MbedTLS 功能

Component Config > mbedTLS 下有多个默认启用的 mbedTLS 功能,如果不需要,可以禁用相应功能以减小代码大小。

这些功能包括:

每个选项的帮助文本中都有更多信息可供参考。

重要

强烈建议不要禁用所有 mbedTLS 选项。 仅在理解功能用途,并确定在应用程序中不需要此功能时,方可禁用相应选项。请特别注意以下两点:

  • 确保设备连接的任何 TLS 服务器仍然可用。如果服务器由第三方或云服务控制,建议确保固件至少支持两种 TLS 密码套件,以防未来某次更新禁用了其中一种。

  • 确保连接设备的任何 TLS 客户端仍然可以使用支持/推荐的密码套件进行连接。请注意,未来版本的客户端操作系统可能会移除对某些功能的支持,因此建议启用多个支持的密码套件或算法以实现冗余。

如果依赖于第三方客户端或服务器,请密切关注其有关支持的 TLS 功能的公告和变更。否则,当所支持功能变更时,ESP32 设备可能无法访问。

备注

ESP-IDF 并未测试所有 mbedTLS 编译配置组合。如果发现某个组合无法编译或无法按预期执行,请在 GitHub 上报告详细信息。

虚拟文件系统 (VFS)

在 ESP-IDF 中,虚拟文件系统组件 功能允许使用标准的 I/O 函数(如 openreadwrite 等)和 C 库函数(如 fopenfreadfwrite 等)来访问多个文件系统驱动程序和类似文件的外设驱动程序。当应用程序中不需要文件系统或类似文件的外设驱动功能时,可以部分或完全禁用该功能。VFS 组件提供以下配置选项:

  • CONFIG_VFS_SUPPORT_TERMIOS — 如果应用程序不使用 termios 函数族,可以禁用此选项。目前,这些函数仅在 UART VFS 驱动程序中实现,大多数应用程序可以禁用此选项。禁用后可以减小约 1.8 KB 代码大小。

  • CONFIG_VFS_SUPPORT_SELECT — 如果应用程序不使用 select 函数处理文件描述符,可以禁用此选项。目前,只有 UART 和 eventfd VFS 驱动程序支持 select 函数。请注意,当禁用该选项时,仍然可以使用 select 处理套接字文件描述符。禁用此选项将减小约 2.7 KB 代码大小。

  • CONFIG_VFS_SUPPORT_DIR — 如果应用程序不使用与目录相关的函数,例如 readdir (参阅此选项的描述以获取完整列表),可以禁用此选项。如果应用程序只需打开、读取和写入特定文件,而不需要枚举或创建目录,可以禁用此选项,从而减少超过 0.5 KB 代码大小,具体减小多少取决于使用的文件系统驱动程序。

  • CONFIG_VFS_SUPPORT_IO — 如果应用程序不使用文件系统或类似文件的外设驱动程序,可以禁用此选项,这将禁用所有 VFS 功能,包括上述三个选项。当禁用此选项时,无法使用 控制台终端。请注意,当禁用此选项时,应用程序仍然可以使用标准 I/O 函数处理套接字文件描述符。相较于默认配置,禁用此选项可以减小约 9.4 KB 代码大小。

引导加载程序大小

本文档仅涉及 ESP-IDF 应用程序的二进制文件大小,而不涉及 ESP-IDF 二级引导程序

关于 ESP-IDF 引导加载程序二进制文件大小的讨论,参阅 引导加载程序大小

IRAM 二进制文件大小

如果二进制文件的 IRAM 部分过大,可以通过减少 IRAM 使用来解决这个问题,参阅 IRAM 优化