Summary
Add -fno-asynchronous-unwind-tables -fno-unwind-tables to the build_flags of every ESP32 (and Cortex-M / RISC-V) platform .ini file in platforms/. This strips dead exception-unwinding metadata from the firmware. On a stock FastLED Blink for ESP32-S3 the binary drops from 657 KB → ~432 KB (−225 KB, −34%) with zero source changes and no diagnostic regression on Xtensa.
Background
A full per-archive byte audit of the current FastLED master built against the platformio-starter ESP32-S3 config (see FastLED/FastLED#2473) showed that 36% of the firmware (~231 KB) is orphan .eh_frame — DWARF exception-unwinding metadata for code that the ESP32 panic handler never reads:
libFastLED.a 168,772 bytes
libstdc++.a 56,148 bytes
libnvs_flash.a 4,068 bytes
libFrameworkArduino 3,240 bytes
... (others)
TOTAL 236,932 bytes (231 KB)
.eh_frame is only consumed by:
- C++ exceptions — disabled by default on ESP-IDF (and FastLED uses
-fno-exceptions)
- Async-signal sampling profilers — not part of the Arduino-ESP32 toolchain
- DWARF-based panic backtraces — ESP-IDF's panic handler uses register-window walking on Xtensa and frame-pointer walking on RISC-V, not
.eh_frame. Stack traces still work without it.
So on a default Arduino-ESP32 build, the 231 KB is pure waste.
Why this belongs in platformio-starter (not in FastLED)
FastLED tried to fix this with a pragma-based approach (PR FastLED/FastLED#2423, FL_NO_UNWIND_BEGIN/_END). That fix is a confirmed no-op on GCC 14.2.0 / xtensa-esp-elf because GCC's _Pragma("GCC optimize") doesn't reliably propagate codegen flags like -fno-asynchronous-unwind-tables. The only mechanism that actually works is flags on the cc1 command line, which is platformio.ini's job, not a library header's.
The proper long-term fix lives in fbuild (tracked as FastLED/fbuild#243 — auto-detect when stripping is safe). Until that lands, the starter project is the right place to surface these flags so new FastLED users on ESP32 don't get a 657 KB binary by default.
Proposed change
Add to every ESP32-variant .ini in platforms/ (platformio.esp32dev.ini, platformio.esp32s2.ini, platformio.esp32s3.ini, platformio.seeed_xiao_esp32s3.ini, platformio.esp32c2.ini, platformio.esp32c3.ini, platformio.esp32c5.ini, platformio.esp32c6.ini, platformio.esp32h2.ini):
build_flags =
; --------------------------------------------------------------------------
; Strip exception-unwinding metadata (.eh_frame) from the firmware.
; FastLED + ESP-IDF both build with -fno-exceptions, so these tables are
; never consumed at runtime. The ESP32 panic handler uses register-window
; walking on Xtensa and frame-pointer walking on RISC-V, not .eh_frame,
; so stack traces still work.
; Removing this saves ~225 KB on a typical FastLED ESP32-S3 binary.
; To restore .eh_frame for DWARF-based debugging, comment these two flags.
; --------------------------------------------------------------------------
-fno-asynchronous-unwind-tables
-fno-unwind-tables
The same flags also help (smaller magnitude) on STM32 (platformio.bluepill.ini, platformio.blackpill.ini), nRF52 (platformio.nrf52840.ini), Teensy, and RP2040 builds — anywhere GCC defaults -fasynchronous-unwind-tables to ON.
Why this is safe by default
| Platform |
Backtrace mechanism |
.eh_frame needed? |
| Xtensa (ESP32 / S2 / S3) |
Register-window walking |
No — panic backtrace doesn't read .eh_frame |
| RISC-V (C3 / C6 / H2 / C5 / P4) |
Frame-pointer walking (default -fno-omit-frame-pointer) |
No — panic backtrace doesn't read .eh_frame |
| ARM Cortex-M (STM32, nRF52, etc.) |
Frame-pointer walking |
No — panic backtrace doesn't read .eh_frame |
| AVR (Uno, Mega) |
n/a (no panic handler) |
No |
The only case where stripping would degrade diagnostics is if a user explicitly:
- Enables
CONFIG_ESP_SYSTEM_USE_EH_FRAME=y in sdkconfig (RISC-V, opt-in only)
- Uses
-fexceptions somewhere in their project
- Builds with
build_type = debug and wants gdb to step using DWARF unwind
Each of those cases is something the user is explicitly opting into; commenting out the two flags in the starter's .ini is the documented escape hatch.
Measured impact
Stock examples/Blink/Blink.ino on ESP32-S3 against current FastLED master (commit 8616873b73):
| Build |
firmware.bin |
| No flags (today's starter) |
657,072 B |
| With both flags added |
~432,000 B (−225 KB, −34%) |
| FastLED 3.10.3 baseline (for reference) |
~344,000 B |
Verified with xtensa-esp32s3-elf-readelf --debug-dump=frames firmware.elf | grep -c "FDE cie" — count drops from ~6,500 FDEs to under 200 (the residual is from libgcc and a few libraries that always emit eh_frame even with these flags).
Acceptance criteria
Reproduction
git clone https://github.com/fastled/platformio-starter
cd platformio-starter
# Baseline
pio run -e esp32s3 -d platforms/platformio.esp32s3.ini
ls -la .pio/build/esp32s3/firmware.bin # ~657 KB
# Add the two flags to platforms/platformio.esp32s3.ini build_flags
pio run -e esp32s3 -d platforms/platformio.esp32s3.ini --target clean
pio run -e esp32s3 -d platforms/platformio.esp32s3.ini
ls -la .pio/build/esp32s3/firmware.bin # ~432 KB
Related
Summary
Add
-fno-asynchronous-unwind-tables -fno-unwind-tablesto thebuild_flagsof every ESP32 (and Cortex-M / RISC-V) platform.inifile inplatforms/. This strips dead exception-unwinding metadata from the firmware. On a stock FastLED Blink for ESP32-S3 the binary drops from 657 KB → ~432 KB (−225 KB, −34%) with zero source changes and no diagnostic regression on Xtensa.Background
A full per-archive byte audit of the current FastLED master built against the platformio-starter ESP32-S3 config (see FastLED/FastLED#2473) showed that 36% of the firmware (~231 KB) is orphan
.eh_frame— DWARF exception-unwinding metadata for code that the ESP32 panic handler never reads:.eh_frameis only consumed by:-fno-exceptions).eh_frame. Stack traces still work without it.So on a default Arduino-ESP32 build, the 231 KB is pure waste.
Why this belongs in platformio-starter (not in FastLED)
FastLED tried to fix this with a pragma-based approach (PR FastLED/FastLED#2423,
FL_NO_UNWIND_BEGIN/_END). That fix is a confirmed no-op on GCC 14.2.0 / xtensa-esp-elf because GCC's_Pragma("GCC optimize")doesn't reliably propagate codegen flags like-fno-asynchronous-unwind-tables. The only mechanism that actually works is flags on thecc1command line, which isplatformio.ini's job, not a library header's.The proper long-term fix lives in fbuild (tracked as FastLED/fbuild#243 — auto-detect when stripping is safe). Until that lands, the starter project is the right place to surface these flags so new FastLED users on ESP32 don't get a 657 KB binary by default.
Proposed change
Add to every ESP32-variant
.iniinplatforms/(platformio.esp32dev.ini,platformio.esp32s2.ini,platformio.esp32s3.ini,platformio.seeed_xiao_esp32s3.ini,platformio.esp32c2.ini,platformio.esp32c3.ini,platformio.esp32c5.ini,platformio.esp32c6.ini,platformio.esp32h2.ini):The same flags also help (smaller magnitude) on STM32 (
platformio.bluepill.ini,platformio.blackpill.ini), nRF52 (platformio.nrf52840.ini), Teensy, and RP2040 builds — anywhere GCC defaults-fasynchronous-unwind-tablesto ON.Why this is safe by default
.eh_frameneeded?.eh_frame-fno-omit-frame-pointer).eh_frame.eh_frameThe only case where stripping would degrade diagnostics is if a user explicitly:
CONFIG_ESP_SYSTEM_USE_EH_FRAME=yin sdkconfig (RISC-V, opt-in only)-fexceptionssomewhere in their projectbuild_type = debugand wants gdb to step using DWARF unwindEach of those cases is something the user is explicitly opting into; commenting out the two flags in the starter's
.iniis the documented escape hatch.Measured impact
Stock
examples/Blink/Blink.inoon ESP32-S3 against current FastLED master (commit8616873b73):firmware.binVerified with
xtensa-esp32s3-elf-readelf --debug-dump=frames firmware.elf | grep -c "FDE cie"— count drops from ~6,500 FDEs to under 200 (the residual is from libgcc and a few libraries that always emit eh_frame even with these flags).Acceptance criteria
.iniinplatforms/.inis (smaller win, but free).inis (platformio.uno.ini,platformio.attiny85.ini,platformio.nano_every.ini) — AVR-GCC default is-fno-asynchronous-unwind-tablesalready, flags would be redundantfirmware.binis materially smaller (sanity test)Reproduction
Related
FL_NO_UNWINDmacros