Skip to content

fbuild symbols: extract owning symbol from .rodata.<mangled>.str1.N input sections (377 KB of rodata currently buckets at object level) #425

Description

@zackees

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:

  1. 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.
  2. Attribution pipeline: when an InputSectionRange has an extractable owner AND no nm symbol covers its address range, emit a synthetic FineGrainedSymbol row keyed to that owner with sym_type set to a new '?' marker (or 'r' for rodata-owner-implicit) and source: "map-derived". The region follows the output_section (rodata → Flash).
  3. 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.
  4. JSON shape: add source: \"nm\" | \"map-derived\" field to FineGrainedSymbol so diff tools can flag the provenance, but otherwise the entry shape stays identical.
  5. 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 .rodata with no .<mangled> suffix). These stay object-bucketed.
  • Demangling correctness beyond what GNU c++filt already produces.
  • Per-symbol diff tooling — already lives in the FastLED repo (.claude/symbolaudit/diff.py) and consumes the JSON.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions