Repository navigation
Description
Background
PR #424 ships fbuild symbols <elf> — fine-grained per-symbol bloat analysis built on nm --print-size -S + linker map parsing. It produces correct per-symbol attribution for code (everything nm lists as T/t/W/w/R/r) AND a per-(archive, object, output_section) byte roll-up that covers the bytes nm can't see (anonymous merged rodata).
The gap: on ESP32-S3 Blink master (94c4274109), 377 KB of .flash.rodata end up attributed only at the object-file level, not the symbol level. Concretely, with the current implementation:
GAP SECT NM ARCHIVE / OBJECT / SECTION
59131 59131 0 (unattributed) / main.cpp.o / .flash.rodata
46508 46508 0 libFastLED.a / platforms+.cpp.o / .flash.rodata
34324 34324 0 libFastLED.a / fl.audio+.cpp.o / .flash.rodata
22358 22378 20 libFastLED.a / fl.channels+.cpp.o / .flash.rodata
19039 19039 0 libFastLED.a / fl.gfx+.cpp.o / .flash.rodata
...
So for a user trying to figure out which specific functions own the FL_WARN strings in fl.audio+.cpp.o, the report stops at the object file. They can see the 34 KB is in audio detection, but not whether it's Vibe::getName, Beat::getName, the BuildupDetector::detect log strings, etc.
Why nm can't help
-fdata-sections puts each function's rodata into its own .rodata.<mangled-name>(.str1.<N>)? input section. The linker concatenates these into .flash.rodata. nm enumerates only named symbols, but the string-pool fragments are anonymous within their parent function — they have no symbol entry of their own, so nm doesn't surface them. The owning function's symbol lives in .flash.text, not .flash.rodata, so nm's address→symbol mapping doesn't bridge them.
What the linker map DOES tell us
The input-section names carry the owner inline. Sampled from FastLED@master's firmware.map:
.rodata._ZNK2fl5audio8detector4Vibe7getNameEv.str1.1
0x42050a3c 0xa .pio/.../libFastLED.a(fl.audio+.cpp.o)
.rodata._ZNK2fl5audio8detector4Beat7getNameEv.str1.1
0x42050a46 0xa .pio/.../libFastLED.a(fl.audio+.cpp.o)
.rodata._ZN2fl23SpiChannelEngineAdapter28initializeControllerIfNeededERNS0_14ControllerInfoEii.str1.1
0x420... 0x133 .pio/.../libFastLED.a(fl.channels+.cpp.o)
The _ZN... substring between .rodata. and .str1.<N> is the mangled name of the function that owns the string pool. Demangle it and we have the per-symbol attribution.
Proposed fix
Extend the input-section parser in fbuild_core::symbol_analysis::parse_linker_map to recognize these forms:
| Input section shape | Owner extraction |
|---|---|
.rodata.<mangled> |
<mangled> is the rodata owner |
.rodata.<mangled>.str1.<N> |
<mangled> owns this string-pool fragment (currently invisible) |
.rodata.<mangled>.cst<N> |
<mangled> owns this constant-pool fragment |
.text.<mangled> |
code — already covered by nm |
.literal.<mangled> |
literal pool for <mangled> |
.data.<mangled> |
initialized data owner |
.bss.<mangled> |
BSS owner |
.gnu.linkonce.r.<mangled> |
COMDAT rodata |
.gnu.linkonce.t.<mangled> |
COMDAT text |
Concrete changes:
- Parser: add a helper
extract_owner_from_section(name) -> Option<String>that strips known prefixes/suffixes (.str1.<N>,.cst<N>, etc.) and returns the mangled owner. - Attribution pipeline: when an
InputSectionRangehas an extractable owner AND no nm symbol covers its address range, emit a syntheticFineGrainedSymbolrow keyed to that owner withsym_typeset to a new'?'marker (or'r'for rodata-owner-implicit) andsource: "map-derived". Theregionfollows the output_section (rodata → Flash). - Demangling: collect the synthetic mangled names into the same c++filt batch as the nm ones so the JSON contains demangled owners end-to-end.
- JSON shape: add
source: \"nm\" | \"map-derived\"field toFineGrainedSymbolso diff tools can flag the provenance, but otherwise the entry shape stays identical. - Tests: cover the input-section shapes listed above, plus a case where nm AND map both contain the same owner (must not double-count).
Expected impact on the ESP32-S3 Blink case
| Today | After fix | |
|---|---|---|
| Symbol-attributed bytes | 316,368 (text+rodata mix) | ~620,000 (covers almost all of firmware.bin) |
| Object-only bucketed bytes | 377,014 | < 20,000 (only truly anonymous blocks) |
This would let users answer "which specific function's FL_WARN strings dominate the rodata growth between FastLED 3.10.3 and master" by name, not by object file.
Out of scope
- Bytes that genuinely don't carry the owner in their input-section name (e.g. compiler-generated rodata in
.rodatawith no.<mangled>suffix). These stay object-bucketed. - Demangling correctness beyond what GNU
c++filtalready produces. - Per-symbol diff tooling — already lives in the FastLED repo (
.claude/symbolaudit/diff.py) and consumes the JSON.
Related
- PR feat(symbols): standalone
fbuild symbols <elf>fine-grained bloat report #424 (first shipping offbuild symbols) - FastLED #2773 (binary-size meta tracker that drove the need for this)
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTriage
Activity
Merge pull request #427 from FastLED/feat/425-rodata-symbol-attribution