GCC -ffunction-sections and -fdata-sections for dead code elimination
How to use -ffunction-sections, -fdata-sections and --gc-sections to strip unused code and shrink embedded binaries.
25 Sept 2025, 18:50 UTC

Problem and takeaway
You are building firmware for an embedded device with limited flash memory. The GCC-generated binary exceeds the available size even though your C code uses only a subset of functions. Stripping the binary manually is tedious and error‑prone.
Useful takeaway: compile with -ffunction-sections -fdata-sections and link with --gc-sections. This tells GCC to place each function and data item in its own ELF section, and tells the linker to discard sections that no code references, often reducing binary size by 20–40% on embedded targets.
How the mechanism works
By default, GCC collates all functions and global variables into a few large ELF sections (typically .text and .data). The linker keeps every section that appears in the final executable, even if no code calls those functions or references those variables. The -ffunction-sections and -fdata-sections flags change this: each function or data item gets its own section, named something like .text.foo or .data.bar.
When you pass --gc-sections to the linker (often via -Wl,--gc-sections), the linker performs garbage collection: it walks the reference graph starting from entry points such as main, marks all reachable sections, and removes everything else. The result is a leaner binary that contains only the code and data actually used.
Concrete configuration example
Run the following command on a host workstation or cross‑compilation server. No special permissions are required beyond write access to the output directory.
gcc -mcpu=cortex-m4 -mthumb -ffunction-sections -fdata-sections -O2 -o firmware.elf src/main.c drivers/uart.c drivers/spi.c
Then link with garbage collection:
gcc -mcpu=cortex-m4 -mthumb -Wl,--gc-sections -o firmware.bin firmware.elf
Placeholders in the commands:
-mcpu=cortex-m4 -mthumb: target architecture placeholder; replace with your CPU model.src/main.c drivers/uart.c drivers/spi.c: source files placeholder; list the translation units that make up your firmware.-O2: optimization level placeholder;-O1or-Oscan be substituted depending on your size‑vs‑speed trade‑off.
Where to run: any terminal with the cross‑compiler for your target (for example, arm-none-eabi-gcc) installed. Required permissions: write access to the directory where firmware.elf and firmware.bin are created. Meaningful placeholders are shown above. Expected checks: see the Verification section. Relevant risks: if your code uses function pointers or inter‑module calls resolved dynamically, the linker may keep sections that appear unused at link time but are called through pointer indirection; always test functionally after applying these flags.
Verification and checking the result
After building, compare the binary size before and after:
size_before=$(stat -c%s firmware_original.bin)
size_after=$(stat -c%s firmware.bin)
echo 'Reduction: $((100 * (size_before - size_after) / size_before))%'
Use the size tool shipped with the toolchain for a breakdown:
arm-none-eabi-size firmware.elf
Typical output shows separate totals for .text, .data, and .bss; the .text size should shrink noticeably.
Inspect the generated assembly to confirm that removed functions no longer appear:
arm-none-eabi-objdump -d firmware.elf | grep -i foo
If foo is a function you intentionally kept, the line will still appear; if it was discarded, it will be absent.
Common mistakes and limits
- Over‑aggressive inlining with
-O3:-O3may inline functions across translation units, blurring the boundaries that-ffunction-sectionsrelies on. The section‑level granularity helps, but inlining can still increase binary size if not carefully controlled. - Missing
--gc-sectionsin the link step: compiling with the flags alone does nothing if the linker does not discard unreferenced sections. Always include-Wl,--gc-sections(or the equivalent--gc-sectionslinker flag) during the final link. - Broken function‑pointer references: if a function address is taken and passed to code in another module, the linker may keep the section because a reference exists, even if no direct call site remains. Explicitly mark such functions with
__attribute__((used))if you want them retained. - Relocation errors with
-fPICor-pie: on some targets, combining-ffunction-sectionswith position‑independent code flags can cause relocation truncation errors. Verify that your linker script and target ABI support the combination.
Practical way to check for these issues: rebuild your project with the flags, run your test suite, and compare the resulting binary size and functional behavior against a baseline build without the flags.
Summary
Using -ffunction-sections, -fdata-sections, and --gc-sections is a low‑effort, high‑impact way to trim embedded firmwares. The flags work at the compiler and linker level, require no source changes, and the size reduction is measurable with the size command. Pair the technique with -Os for even greater savings, and always validate that functional tests pass after the change.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.