HomeAbout UsContact Us

Reducing Embedded Firmware Size with Linker Garbage Collection

By Jithin Tom
Published in Embedded C/C++
July 26, 2026
3 min read
Reducing Embedded Firmware Size with Linker Garbage Collection

Table Of Contents

01
How Section Garbage Collection Works
02
Required Compiler and Linker Flags
03
Verifying the Effect
04
Common Pitfalls
05
Linker Script Integration
06
Measuring Results
07
Build System Integration
08
When NOT to Use --gc-sections
09
Summary
10
Related Reading
11
References
12
Frequently Asked Questions

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.

How Section Garbage Collection Works

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() | | | | |
+------------------+ +------------------+ +------------------+

Required Compiler and Linker Flags

# 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.

Verifying the Effect

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

Common Pitfalls

1. Interrupt Vector Tables

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))

2. Constructor/Destructor Sections

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*))

Linker Script Integration

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.

Measuring Results

Typical savings on a Cortex-M3 project with newlib-nano:

Configuration.text (KB).data (B).bss (B)Total Flash
Default42.31,2488,19243.5 KB
-ffunction-sections -fdata-sections -Wl,--gc-sections31.78726,54032.6 KB
Savings25%30%20%25%

Savings vary with library usage. Projects avoiding printf, malloc, and floating-point math see less benefit.

Build System Integration

Makefile

CFLAGS += -ffunction-sections -fdata-sections
LDFLAGS += -Wl,--gc-sections
# Debug: print discarded sections
LDFLAGS_DEBUG += -Wl,--print-gc-sections

Meson

c_args = ['-ffunction-sections', '-fdata-sections']
c_link_args = ['-Wl,--gc-sections']

Zephyr RTOS

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.conf
CONFIG_SIZE_OPTIMIZATIONS=y

When NOT to Use —gc-sections

  • Bootloaders with fixed memory layouts where specific unreferenced symbols or padding are intentionally placed to reserve space
  • Shared libraries or dynamically loadable modules where functions are resolved by name at runtime, as the linker cannot statically determine which symbols will be required

Summary

TechniqueFlagEffect
Per-function sections-ffunction-sectionsEnables function-level reachability
Per-variable sections-fdata-sectionsEnables variable-level reachability
Linker GC-Wl,--gc-sectionsDiscards unreachable sections
Debug GC output-Wl,--print-gc-sectionsLists 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.

References

  1. LD Manual: --gc-sectionshttps://sourceware.org/binutils/docs/ld/Options.html#index-gc-sections
  2. GCC Manual: Function Sections — https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html#index-ffunction-sections
  3. ARM Embedded Application Binary Interface (EABI) — https://github.com/ARM-software/abi-aa
  4. newlib-nano README: Size Optimization — https://github.com/32bitmicro/newlib-nano-1.0/blob/master/newlib/README.nano
  5. Zephyr RTOS Documentation: Build System (CMake) — https://docs.zephyrproject.org/latest/build/cmake/index.html
  6. Joseph Yiu, “The Definitive Guide to ARM Cortex-M3 and Cortex-M4 Processors”, Ch. 12 — ISBN 978-0124080829

Frequently Asked Questions

What is the difference between -ffunction-sections and -fdata-sections?

-ffunction-sections places each function in its own .text.* section. -fdata-sections places each global/static variable in its own .data.*, .bss.*, or .rodata.* section. Both are required for the linker to identify and discard unused code and data independently.

Does -Wl,--gc-sections remove unused functions from libraries like libc?

Only if the library was built with -ffunction-sections -fdata-sections. Standard newlib/newlib-nano typically is, but custom static libraries must be compiled with these flags for --gc-sections to strip their unused symbols.

Can --gc-sections break code that uses hardware interrupt vectors or custom sections?

Yes. Sections accessed only by hardware (like interrupt vector tables) or iterated over via linker-defined symbols (like C++ constructors or custom link-time arrays) are not explicitly referenced in the code via relocations. They must be protected with KEEP() in the linker script.

How much flash savings can --gc-sections typically achieve?

Typically 10-30% reduction for applications using large libraries (e.g., printf, math). Savings depend on how much of the linked library code is actually referenced. Minimal bare-metal apps may see little benefit.

Does --gc-sections affect RAM usage or only flash?

Primarily flash (code + rodata). With -fdata-sections, unreferenced global/static variables in .data/.bss are also discarded, reducing both .data (flash copy) and .bss (zero-init RAM) footprint.

Tags

embedded-clinkergc-sectionsfirmware-sizeoptimizationgccarm-cortex-m

Share


Previous Article
Embedded Firmware Testing Strategies for Safety-Critical Systems
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Fixed-Point Arithmetic in Embedded C: A Practical Guide
Fixed-Point Arithmetic in Embedded C: A Practical Guide
July 22, 2026
1 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media