Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
ARM GCC Build Process: Compile, Assemble & Link
Understand every stage of the ARM bare-metal build pipeline — from C source to flashable binary — with real command-line examples.
1. Build Process Overview
When you hit “Build” in an IDE, a lot happens behind the scenes. Understanding each stage helps you debug build errors, optimize binary size, and write correct Makefiles. The entire process is driven by a single command — arm-none-eabi-gcc — which internally calls the preprocessor, compiler, assembler, and linker.
You can run all stages with one command, or run each individually to inspect intermediate output. Running stages individually is extremely useful for debugging.
2. Stage 1: Preprocessing (.c → .i)
The preprocessor handles all directives that begin with #. It expands macros, inserts included headers, and evaluates conditional compilation blocks. The output is a pure C file with no # directives remaining.
What the Preprocessor Does
#include— inserts the full content of the header file inline.#define— performs text substitution of macros.#ifdef/#ifndef— includes/excludes code blocks based on conditions.- Strips all comments.
C — Input: led.c
#include "stm32f4xx.h"
#define LED_PIN 12U
void led_init(void)
{
/* Enable GPIOD clock */
RCC->AHB1ENR |= (1U << 3U);
/* Set pin 12 as output */
GPIOD->MODER |= (1U << (LED_PIN * 2U));
}
Shell — Run Preprocessor Only
# -E flag: stop after preprocessing, output .i file
arm-none-eabi-gcc -E led.c -o led.i
# Inspect the output (can be very large due to header expansion)
# grep for led_init to find the relevant section
grep -A 10 "led_init" led.i
Tip: The preprocessed .i file is plain text C. If you are getting mysterious macro expansion errors, inspect the .i file to see exactly what the preprocessor produced.
3. Stage 2: Code Generation (.i → .s)
The compiler’s core takes the preprocessed C code and translates it into ARM assembly language. This is the most intellectually complex stage — the parser builds an abstract syntax tree (AST), an optimizer transforms the tree, and the code generator emits ARM Thumb-2 assembly mnemonics.
What Happens Inside the Compiler
Reads tokens, builds syntax tree, checks grammar
Dead code elimination, constant folding, inlining
Emits ARM Thumb-2 assembly mnemonics
Shell — Compile to Assembly
# -S flag: stop after compilation, produce .s assembly file
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-O0 \ # no optimization for readable output
-S led.c \
-o led.s
# View the generated assembly
cat led.s
Sample Generated Assembly
ARM Assembly Output (led.s fragment)
; led_init function compiled to Thumb-2 instructions
led_init:
PUSH {R4, LR} ; save link register
LDR R3, =0x40023830 ; address of RCC->AHB1ENR
LDR R2, [R3] ; read current value
ORR R2, R2, #0x8 ; set bit 3 (GPIOD clock)
STR R2, [R3] ; write back
LDR R3, =0x40020C00 ; address of GPIOD->MODER
LDR R2, [R3]
ORR R2, R2, #0x1000000 ; set pin 12 as output
STR R2, [R3]
POP {R4, PC} ; restore and return
4. Stage 3: Assembly (.s → .o)
The assembler converts human-readable ARM assembly mnemonics into binary machine code (opcodes). The output is a relocatable object file in ELF (Executable and Linkable Format). “Relocatable” means the code has no fixed memory addresses yet — those are assigned by the linker.
What is an ELF Object File?
ELF is a standard binary format. A relocatable .o file contains:
- .text — machine code instructions
- .data — initialized global/static variables
- .bss — uninitialized global/static variables (size only, no data)
- .rodata — read-only data (string literals, const arrays)
- Symbol table — names and (relative) addresses of functions and variables
- Relocation entries — placeholders for addresses to be filled in by the linker
Shell — Assemble to Object File
# -c flag: compile and assemble but do not link
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-c led.c \
-o led.o
# Inspect sections in the object file
arm-none-eabi-objdump -h led.o
# Expected output (section headers):
# Idx Name Size VMA LMA Offset Align
# 0 .text 00000018 00000000 00000000 00000034 2**1
# 1 .data 00000000 00000000 00000000 0000004c 2**0
# 2 .bss 00000000 00000000 00000000 0000004c 2**0
# Disassemble — shows machine opcodes with mnemonics
arm-none-eabi-objdump -d led.o
# Show symbol table
arm-none-eabi-nm led.o
Notice: All addresses in the .o file are relative (starting from 0). They are not the final MCU addresses. The linker assigns absolute addresses using the linker script.
5. Stage 4: Linking (.o → .elf)
The linker combines multiple .o files, resolves cross-file symbol references (e.g., main.c calls led_init() defined in led.c), and assigns absolute addresses to all sections using the linker script.
.text, .data, .bss, .rodata
.text, .data, .bss, .rodata
.text (vector table)
linker
+ linker.ld
→
Shell — Link Object Files
# Link all .o files using a linker script
arm-none-eabi-gcc \
-mcpu=cortex-m4 \
-mthumb \
-T linker.ld \ # provide linker script
-nostdlib \ # no standard library (bare metal)
-o firmware.elf \ # output ELF file
main.o led.o startup.o
# Check section sizes and addresses
arm-none-eabi-objdump -h firmware.elf
# Convert ELF to raw binary for flashing
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
# Or Intel hex format
arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
6. Important GCC Flags for ARM Embedded
| Flag | Purpose | Example |
|---|---|---|
-mcpu=cortex-m4 | Specify target CPU; enables architecture-specific optimizations | -mcpu=cortex-m4 |
-mthumb | Generate Thumb-2 instructions (required for Cortex-M) | -mthumb |
-mfloat-abi=hard | Use hardware FPU registers for floating point (M4 with FPU) | -mfloat-abi=hard -mfpu=fpv4-sp-d16 |
-O0 | No optimization — best for debugging | -O0 -g3 |
-O2 | Standard optimization — good balance for production | -O2 |
-Os | Optimize for code size — important for flash-constrained MCUs | -Os |
-g3 | Include maximum debug info in the ELF (does not affect .bin) | -g3 |
-Wall -Wextra | Enable comprehensive compiler warnings | -Wall -Wextra -Werror |
-nostdlib | Do not link with standard C library (bare metal) | -nostdlib |
-T linker.ld | Specify custom linker script | -T stm32f4.ld |
-Wl,-Map=out.map | Generate linker map file showing symbol addresses | -Wl,-Map=firmware.map |
-c | Compile and assemble only, do not link | -c main.c -o main.o |
-S | Stop after compilation, keep assembly output | -S main.c -o main.s |
-E | Stop after preprocessing | -E main.c -o main.i |
7. Programming Exercises
Exercise 1 — Inspect Each Build Stage
Write a simple C function and walk through all four stages manually. At each stage, inspect the output file.
C — Source File (blink.c)
#define GPIOD_ODR (*((volatile uint32_t*)0x40020C14))
#define LED_BIT (1U << 12U)
static uint32_t blink_count = 0;
void toggle_led(void)
{
GPIOD_ODR ^= LED_BIT;
blink_count++;
}
Shell — Step-by-Step Build
# Step 1: Preprocess
arm-none-eabi-gcc -E blink.c -o blink.i
wc -l blink.i # count lines after expansion
# Step 2: Compile to assembly
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O0 -S blink.c -o blink.s
cat blink.s
# Step 3: Assemble to object file
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -c blink.c -o blink.o
arm-none-eabi-objdump -h blink.o # inspect sections
arm-none-eabi-nm blink.o # list symbols (blink_count, toggle_led)
# Note: blink_count should appear as a .data symbol
# toggle_led should appear as a .text symbol
Exercise 2 — Compare Optimization Levels
Compile the same function with -O0, -O1, -O2, and -Os. Compare the generated assembly and object file sizes. Record your findings.
Shell
for OPT in O0 O1 O2 Os; do
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -$OPT -c blink.c -o blink_$OPT.o
echo "=== -$OPT ==="
arm-none-eabi-size blink_$OPT.o
done
Exercise 3 — Find Undefined Symbols
Create a file main.c that calls toggle_led() but does NOT include blink.c during linking. Observe the linker error. Then add blink.o to the link command and verify it succeeds.
8. Frequently Asked Questions
What is the difference between -O0, -O2, and -Os?
-O0 disables all optimization — slowest code, largest binary, best for debugging because variables and statements map 1:1 with source lines. -O2 enables most optimizations (inlining, loop unrolling, dead code elimination) — much faster but harder to debug. -Os optimizes specifically for code size by avoiding size-increasing optimizations like loop unrolling. For flash-constrained MCUs, -Os is often the best choice.
Why is the .o file address 0x00000000 but the .elf has real addresses?
Object files are “relocatable” — they contain code with placeholder addresses starting at 0. The linker reads your linker script (which specifies FLASH starts at 0x08000000 and SRAM at 0x20000000) and applies those base addresses to each section. The result is the fully addressed .elf file.
Do I need -nostdlib for all bare-metal projects?
Not always. Without -nostdlib, GCC links in the standard C runtime (crt0) and libc. On bare metal this fails because the startup code assumes an OS environment. Use -nostdlib when you are writing your own startup code (startup.s), OR use --specs=nosys.specs to provide stub implementations of OS calls. If you want printf and malloc, use newlib-nano with --specs=nano.specs.
What does the linker map file (.map) tell me?
The map file lists every function and variable with its exact address, section, and size in the final binary. It also shows which object file each symbol came from, and the total memory usage per section. This is the primary tool for diagnosing “binary too large” problems — you can see exactly which functions are consuming the most flash.
Quick Knowledge Check
1. Which GCC flag stops after preprocessing and keeps the .i file?
- a) -S
- b) -c
- c) -E
- d) -o
Check Answer
2. What type of addresses does a relocatable .o file contain?
- a) Absolute flash addresses (e.g., 0x08000000)
- b) Virtual addresses starting from 0 (relative)
- c) Physical RAM addresses
- d) JTAG debug addresses
Check Answer
3. Which optimization flag is best for minimizing flash memory usage?
- a) -O0
- b) -O3
- c) -Os
- d) -Og
Check Answer
EmbeddedPathashala — Embedded Systems Programming on ARM Cortex-M3/M4
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments