
Firmware size constraints are a daily reality on Cortex-M0/M0+/M3 devices with 32—64 KB flash. The toolchain’s most effective size-reduction lever is linker garbage collection via -ffunction-sections -fdata-sections -Wl,--gc-sections. This article explains how it works, the required compiler and linker flags, common pitfalls with interrupt vectors and custom sections, and how to verify the savings.
By default, GCC places all functions in a single .text section and all global data in .data/.bss/.rodata. The linker sees one monolithic section per object file — it cannot selectively discard unused functions without discarding the entire object file.
-ffunction-sections and -fdata-sections change this: each function gets .text.function_name, each variable gets .data.var_name, .bss.var_name, or .rodata.var_name. The linker builds a reference graph starting from root sections (such as the entry point and any sections marked with KEEP()), marking all reachable sections. Unreferenced sections are dropped with --gc-sections.
+------------------+ +------------------+ +------------------+| Source Code | | Compiler | | Object File || (main.c) |---->| -ffunction- |---->| .text.used || void used() {} | | sections | | .text.unused || void unused() {}| | -fdata-sections | | |+------------------+ +------------------+ +------------------+|v+------------------+ +------------------+ +------------------+| Final Binary | | Linker Output | | Linker || (smaller size) |<----| (discards |<----| --gc-sections || contains only | | .text.unused) | | (reachability) || used() | | | | |+------------------+ +------------------+ +------------------+
# Compiler flags (per translation unit)CFLAGS += -ffunction-sections -fdata-sections# Linker flag (passed via driver)LDFLAGS += -Wl,--gc-sections
In CMake:
target_compile_options(firmware PRIVATE -ffunction-sections -fdata-sections)target_link_options(firmware PRIVATE -Wl,--gc-sections)
Critical: These flags must be used consistently. If any translation unit or static library is compiled without -ffunction-sections, its symbols remain in a monolithic section and cannot be individually garbage-collected.
Use --print-gc-sections to list discarded sections directly on stderr:
arm-none-eabi-gcc -ffunction-sections -fdata-sections -Wl,--gc-sections \-Wl,--print-gc-sections -Wl,-Map=output.map -o firmware.elf ...
The linker will print each discarded section to stderr:
removing unused section '.text.unused_function' in file 'main.o'
You can also compare total sizes before and after using arm-none-eabi-size:
arm-none-eabi-size firmware.elf
The interrupt vector table is read directly by the hardware core, not called by software. Because there are no explicit relocations from the application code to the vector table, the linker will see the entire table as unreferenced and discard it, along with all the interrupt handlers it points to.
Fix: Explicitly keep the vector table section in the linker script. Since the table contains relocations to the interrupt handlers, keeping the table automatically keeps all referenced handlers:
KEEP(*(.isr_vector))
C++ global constructors (.init_array) and destructors (.fini_array) are typically iterated over during startup using linker-defined symbols (like __init_array_start and __init_array_end). Since there are no direct relocations from the startup code to the individual function pointer sections, the linker will discard them. Note that prioritized constructors generate .init_array.* sections.
Fix: Explicitly keep these sections (and their prioritized variants) in the linker script:
KEEP(*(.init_array*))KEEP(*(.fini_array*))
A common embedded C pattern is placing structures into a custom section to form an array in memory (e.g., for CLI commands or test cases), which is then accessed via linker symbols. Similar to constructors, these custom sections lack direct relocations from reachable code and will be discarded by --gc-sections.
Fix: Use KEEP() for any custom sections accessed only via boundary symbols:
KEEP(*(.cli_commands*))
Minimal --gc-sections aware linker script:
ENTRY(Reset_Handler)SECTIONS{. = ORIGIN(FLASH);.isr_vector :{KEEP(*(.isr_vector))} > FLASH.text :{*(.text*)*(.rodata*)KEEP(*(.init_array*))KEEP(*(.fini_array*))} > FLASH_sidata = LOADADDR(.data);.data :{. = ALIGN(4);_sdata = .;*(.data*). = ALIGN(4);_edata = .;} > RAM AT> FLASH.bss :{_sbss = .;*(.bss*)*(COMMON). = ALIGN(4);_ebss = .;} > RAM/DISCARD/ :{*(.note.gnu.property)*(.note.gnu.build-id)*(.comment)}}
The /DISCARD/ section unconditionally removes the specified sections from the output, regardless of whether --gc-sections is used.
Typical savings on a Cortex-M3 project with newlib-nano:
| Configuration | .text (KB) | .data (B) | .bss (B) | Total Flash |
|---|---|---|---|---|
| Default | 42.3 | 1,248 | 8,192 | 43.5 KB |
-ffunction-sections -fdata-sections -Wl,--gc-sections | 31.7 | 872 | 6,540 | 32.6 KB |
| Savings | 25% | 30% | 20% | 25% |
Savings vary with library usage. Projects avoiding printf, malloc, and floating-point math see less benefit.
CFLAGS += -ffunction-sections -fdata-sectionsLDFLAGS += -Wl,--gc-sections# Debug: print discarded sectionsLDFLAGS_DEBUG += -Wl,--print-gc-sections
c_args = ['-ffunction-sections', '-fdata-sections']c_link_args = ['-Wl,--gc-sections']
Zephyr unconditionally enables -ffunction-sections, -fdata-sections, and -Wl,--gc-sections in its build system. No user configuration is required. To additionally optimize for code size with -Os:
# prj.confCONFIG_SIZE_OPTIMIZATIONS=y
| Technique | Flag | Effect |
|---|---|---|
| Per-function sections | -ffunction-sections | Enables function-level reachability |
| Per-variable sections | -fdata-sections | Enables variable-level reachability |
| Linker GC | -Wl,--gc-sections | Discards unreachable sections |
| Debug GC output | -Wl,--print-gc-sections | Lists discarded sections in map |
Linker garbage collection is the single highest-impact size optimization for embedded firmware. Apply it consistently across all translation units and libraries, guard reachability roots with KEEP() or __attribute__((used)), and verify with map files. On constrained Cortex-M0/M3 devices, it routinely recovers 10-30% flash — often the difference between fitting and not fitting.
--gc-sections — https://sourceware.org/binutils/docs/ld/Options.html#index-gc-sectionsQuick Links
Legal Stuff





