Skip to content
BoKSA

GCC Toolchain in Depth

GCC Toolchain in Depth

Introduction

You write main.c in English-like C. The microcontroller only flips transistors according to machine code — patterns of bits the CPU decodes as "add", "load", "jump". Somewhere between your editor and the chip, a toolchain must translate, combine, and place that code in flash and RAM.

Compilers and toolchains gave the big picture. Here you see each GCC step, what object files contain, why linking fails, and what names like arm-none-eabi-gcc mean. When you stare at undefined reference to 'sin' or a linker script error, this is the chapter that tells you which stage failed and why.


The translation pipeline — a factory line

Imagine a factory with four stations. Raw material enters; a finished executable leaves.

flowchart LR C[Source *.c] CPP[Preprocessor] CC[Compiler] AS[Assembler] LD[Linker] ELF[Executable] C --> CPP --> CC --> AS --> LD --> ELF LIB[Libraries] --> LD
Station Tool Plain-language job
1 Preprocessor (cpp) Paste headers, expand #define, handle #ifdef
2 Compiler (cc) Turn C into assembly — human-readable CPU instructions
3 Assembler (as) Turn assembly into object code — binary with gaps
4 Linker (ld) Stitch objects + libraries, assign final addresses

When you type gcc main.c -o main, GCC runs all four. Flags let you stop early and inspect intermediate output — essential for learning.

In plain terms

Preprocessor = highlight and expand templates in a document.
Compiler = translate the story from Dutch to machine language.
Assembler = typeset each page as numeric codes.
Linker = bind all chapters into one book with a table of contents and page numbers.


Stage 1: Preprocessor

Before the compiler sees your file, lines starting with # are processed.

1
2
3
4
5
#include <stdio.h>      /* paste entire header here */
#define LED_PIN 5       /* replace LED_PIN with 5 everywhere */
#ifdef DEBUG
    printf("debug\n");  /* only if DEBUG was defined */
#endif

Embedded boards use this heavily:

1
2
3
4
5
#ifdef BOARD_V2
    #define LED_PIN  GPIO_PIN_5
#else
    #define LED_PIN  GPIO_PIN_13
#endif

One source tree, multiple hardware targets — define BOARD_V2 at compile time (-DBOARD_V2).

See C types and macros for macro pitfalls.


Stage 2: Compiler — C to assembly

1
gcc -S main.c -o main.s

Open main.s — you see labels, mnemonics, and registers. A simple loop in C becomes compare-and-branch instructions. Optimization level matters:

Flag Effect
-O0 Easiest to debug — code maps closely to source
-Os Optimize for size — common on MCUs
-O2 Faster, may reorder code — measure timing on release builds

Reading assembly connects to Assembly language.


Stage 3: Assembler — object files

1
gcc -c main.c -o main.o

An object file (.o) is not runnable yet. It contains:

  • Machine code for functions in this file
  • Data (global variables with initial values)
  • BSS info (how much zeroed space to reserve)
  • Symbol table — names like main, counter, HAL_Init
  • Relocation table — "patch this jump when you know final address"

Symbols: defined vs undefined

Symbol Meaning
Defined "I provide this function/variable — here is my local address"
Undefined "I use printf but I do not define it — linker, find it elsewhere"

main.o might define main and reference undefined printf. libc.a later provides printf.


Stage 4: Linker — the puzzle solver

1
gcc main.o utils.o -o firmware.elf

The linker:

  1. Lays out all code and data in memory (flash at 0x08000000, RAM at 0x20000000 on typical STM32 — from linker script)
  2. Resolves every undefined symbol to exactly one definition
  3. Patches relocation entries — branch targets, load addresses
  4. Discards debug tables or merges them into .elf

If two files define the same global name, or nothing defines a used name → error.

"Undefined reference" — what it really means

1
undefined reference to `HAL_GPIO_WritePin'

Translation: "Some .o called HAL_GPIO_WritePin, but no object or library in the link defined it." Fix: add the right .c file to the project, or link the HAL library.

This is not a syntax error — compile succeeded. Link failed.

Relocation in one sentence

Object files say "call function g" with a placeholder address 0. The linker knows g ended up at 0x08001234 and writes that number into the call instruction.


Stopping GCC at each stage

Command Stops after Inspect
gcc -E file.c Preprocessing Expanded source (huge with big headers)
gcc -S file.c Assembly file.s
gcc -c file.c Object file file.o — use nm, objdump
gcc file.c Full program Runnable binary or .elf

gcc -v prints every subprocess invoked — useful when the IDE hides commands.


Libraries — static and dynamic

A library is a collection of .o files archived together.

Type File What happens
Static .a Needed code is copied into your .elf
Dynamic .so Executable holds names; OS loads .so at runtime

Bare-metal firmware almost always statically links libc_nano, libm, and vendor HAL — the chip has no OS to load .so files. Embedded Linux (Pi, gateway) often uses dynamic linking for libc.so to save disk space.

Static trade-off: bigger flash image, but self-contained — flash the one .bin and go.

Library order: sometimes gcc app.o -lm must come after objects that use sin() — linker scans left to right once.


Toolchain names decoded

arm-none-eabi-gcc breaks down as:

Part Value Meaning
arch arm ARM instruction set
vendor none No specific vendor
os eabi Embedded ABI (no Linux)
tool gcc Compiler driver

arm-linux-gnueabihf-gcc targets Linux on ARM with hardware float — wrong choice for bare-metal STM32.

Using the wrong toolchain can produce code that does not match your vector table or calling convention.


Build, host, and target

Term Your lab setup example
Build Machine that compiled GCC itself
Host Your laptop running arm-none-eabi-gcc
Target STM32 on the bench

Cross-compilation means host ≠ target. You cannot run the .elf by double-clicking on Windows — you flash it.


Useful inspection commands

1
2
3
4
gcc -v main.c -o main          # verbose — see subprocesses
arm-none-eabi-nm firmware.o    # list symbols
arm-none-eabi-objdump -d firmware.elf   # disassemble
arm-none-eabi-size firmware.elf         # flash/RAM usage by section

Your IDE "Build" button runs the same tools with a long flag list and a linker script STM32F411RETx_FLASH.ld.


Relevant topics


Starting points

  1. Run gcc -S on a file with a for loop — count branch instructions.
  2. Deliberately omit a source file from the link — read the undefined reference line carefully.
  3. nm an object file — list defined (T, D) vs undefined (U) symbols.
  4. Open your project's linker script — find FLASH and RAM length.

Focus points

  • Compile errorlink error — different stage, different fix.
  • Linker script is part of the build — wrong RAM size fails link or boot.
  • -c builds objects; you still need a link step for an executable.
  • Match toolchain to target (none-eabi vs linux-gnueabi).

Key points

  • GCC runs preprocess → compile → assemble → link.
  • Object files carry code plus symbol and relocation metadata.
  • The linker assigns addresses and resolves cross-file references.
  • Static linking bundles libraries into firmware; MCUs rarely use dynamic .so.