GCC Command Line Options : Are you new to GCC and feeling overwhelmed by flags like -O2
, .o
files, or linker scripts? This beginner-friendly tutorial will guide you step-by-step through the world of GCC — the GNU Compiler Collection. Learn how your code transforms from .c
to executable, understand every compilation stage (Preprocessing, Compilation, Assembly, Linking), and get hands-on with cross-compilation, object files, static vs shared libraries, and optimization techniques.
You’ll also explore how to debug effectively using -g
, what linker scripts do, and how to use essential toolchain components like as
, ld
, objdump
, nm
, and readelf
.
Perfect for embedded programmers, systems developers, or any curious C/C++ learner who wants to master the build process from the inside out!
What You’ll Learnin this tutorial of GCC Command Line Options:
- GCC Command-Line Options Explained
- Compilation Stages (Preprocessing ➝ Linking)
- Cross-Compilation Made Easy
- Understanding
.o
,.a
,.so
Files - Optimization Flags (
-O0
to-O3
,-Os
,-Ofast
) - Debugging with
-g
and GDB - Basics of Linker Scripts
- Practical Use of
as
,ld
,objdump
,nm
,readelf
What is GCC?
GCC is a powerful compiler used to compile programs written in C, C++, and other languages. It’s used on Linux, embedded systems, and more.
Basic Structure of GCC Command:
gcc [options] source_files [object_files] [-o output_file]
✅ Commonly Used GCC Options:
Option | Description |
---|---|
-o output | Specify the name of the output file. |
-c | Compile source to object file (.o) without linking. |
-Wall | Enable most compiler warnings. |
-Werror | Treat all warnings as errors. |
-g | Include debug information. |
-O0 to -O3 | Control optimization level. |
-E | Stop after preprocessing. |
-S | Stop after compilation (output assembly). |
-v | Verbose output showing stages and toolchain paths. |
-I<dir> | Add directory to header file search path. |
-L<dir> | Add directory to library search path. |
-l<lib> | Link with specified library (e.g., -lm for math). |
Example 1: Compile a C file
gcc hello.c -o hello
Example 2: Compile with warnings and debug info
gcc -Wall -g hello.c -o hello
Example 3: Stop after creating object file
gcc -c hello.c
This creates hello.o
which can be linked later.
When to Use What:
Goal | Flags to Use |
---|---|
Debugging | -g -O0 |
Performance | -O2 or -O3 |
Library building | -c , -fPIC , -shared |
Troubleshooting warnings | -Wall -Werror |
Perfect! Let’s move on to:
GCC Compilation Stages (Preprocessing, Compilation, Assembly, Linking)
GCC doesn’t just “compile” your code in one step—it goes through 4 main stages:
Overview of Stages
Stage | Command Flag | Description |
---|---|---|
1. Preprocessing | -E | Handles macros, #include , #define , removes comments |
2. Compilation | -S | Converts preprocessed code to Assembly |
3. Assembly | -c | Converts Assembly to Object code (.o) |
4. Linking | (default) | Combines object code and libraries into final binary |
1. Preprocessing (-E
)
What Happens:
- Expands
#include
headers - Replaces
#define
macros - Removes comments
Example:
gcc -E hello.c -o hello.i
- Output:
hello.i
(pure C code with macros expanded)
2. Compilation (-S
)
What Happens:
- Converts
.i
to assembly (.s
) - Handles syntax checking and optimizations
Example:
gcc -S hello.i -o hello.s
- Output:
hello.s
(human-readable assembly code)
3. Assembly (-c
)
What Happens:
- Converts
.s
to machine code (.o
) - Done by
as
(assembler)
Example:
gcc -c hello.s -o hello.o
- Output:
hello.o
(object file, not runnable yet)
4. Linking (Default GCC behavior)
What Happens:
- Links
.o
files and libraries to create final executable - Done by
ld
(linker)
Example:
gcc hello.o -o hello
- Output:
hello
(final executable)
Full Manual Process:
gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
gcc -c hello.s -o hello.o
gcc hello.o -o hello
Quick Tip:
To see all stages and tools used by GCC:
gcc -v hello.c -o hello
Cross Compilation
What is Cross Compilation?
Cross compilation is when you compile code on one system (host) but the output is meant to run on a different system (target), often with a different CPU architecture (e.g., x86 host → ARM target).
Why Use Cross Compilation?
- Building for embedded systems (e.g., ARM Cortex on Raspberry Pi, ESP32)
- Developing for devices with limited resources
- Compiling code for different operating systems (Linux → QNX, Windows → Linux)
Common Cross Compilation Toolchain
Tool | Purpose |
---|---|
arm-none-eabi-gcc | GCC for bare-metal ARM targets |
arm-linux-gnueabihf-gcc | GCC for Linux ARM hard-float ABI targets |
aarch64-linux-gnu-gcc | GCC for 64-bit ARM Linux targets |
x86_64-w64-mingw32-gcc | GCC for compiling Windows binaries from Linux |
Structure of a Cross-Compiler:
<target>-<tool>
Example:
arm-linux-gnueabihf-gcc
= cross-compiler for ARM Linux hard-float
Cross Compilation Example:
Let’s say you have this simple C file: main.c
#include <stdio.h>
int main() {
printf("Hello from ARM!\n");
return 0;
}
Compile for ARM Linux:
arm-linux-gnueabihf-gcc main.c -o main_arm
Result:
- Output binary
main_arm
runs on ARM target (e.g., Raspberry Pi) - It will not run on your x86 machine
How to Check Binary Architecture:
file main_arm
Example Output:
main_arm: ELF 32-bit LSB executable, ARM, EABI5, dynamically linked...
Installing Cross Compilers:
On Ubuntu/Debian:
sudo apt-get install gcc-arm-linux-gnueabihf
For bare-metal (no OS):
sudo apt-get install gcc-arm-none-eabi
Checklist for Successful Cross Compilation:
- Correct cross-compiler installed
- Use proper
--sysroot
or-I
,-L
paths if targeting a full OS - Link against target-specific libraries
- Test the binary on the target device
GCC Optimization Flags: -O0
, -O1
, -O2
, -O3
, -Os
, -Ofast
GCC offers optimization flags to control how much it tweaks your code to make it faster, smaller, or more efficient.
🔧 These optimizations are applied during the compilation stage (
.c
→.s
).
Why Use Optimization?
- Improve performance (speed)
- Reduce code size
- Enable inlining, loop unrolling, dead code removal, etc.
Optimization Levels
Flag | Description |
---|---|
-O0 | 🔴 No optimization (default in debug mode). Easier to debug. |
-O1 | 🟡 Basic optimization, removes unused code. |
-O2 | 🟢 Aggressive optimization, safe for most applications. |
-O3 | 🔵 Maximum performance. Includes -O2 + inlining & loop unrolling. |
-Os | ⚪ Optimize for size, not speed. |
-Ofast | 🚨 Fastest possible, but unsafe: ignores standards (e.g., IEEE/strict aliasing rules) |
Example:
gcc -O0 main.c -o main_O0
gcc -O2 main.c -o main_O2
gcc -O3 main.c -o main_O3
gcc -Os main.c -o main_Os
Use time ./main_Ox
or size main_Ox
to compare runtime and binary size.
Inspect Optimized Code:
gcc -S -O3 main.c -o main_O3.s
This outputs assembly code so you can see how optimization changes instructions.
Pro Tip:
Use optimization with debugging (covered next):
gcc -g -O2 main.c -o main
This allows debugging even with optimized code, although some variables might be removed or reordered.
Great! Let’s now cover:
Debug Flags & Symbols: -g
, -O0
to -O3
When developing software, debugging is crucial. GCC provides flags to include debug symbols and control optimization levels that affect debugging behavior.
-g
: Add Debug Info
- Embeds debugging symbols (like variable names, function names, source lines) into the binary.
- These symbols are used by GDB or other debuggers.
- Doesn’t affect performance or behavior at runtime.
Example:
gcc -g main.c -o main
Now you can debug with:
gdb ./main
Optimization & Debugging
Flag combo | Use case |
---|---|
-g -O0 (default) | Easiest debugging: variables, breakpoints work as expected |
-g -O2 or -g -O3 | Debug optimized code: might skip or inline functions, remove variables |
-O2 only | No debug info, faster execution |
-g only | Debug symbols, but no optimization |
Test Code:
#include <stdio.h>
int main() {
int x = 42;
printf("x = %d\n", x);
return 0;
}
Compile and debug:
gcc -g -O0 main.c -o debuggable
gdb ./debuggable
Inside GDB:
(gdb) break main
(gdb) run
(gdb) print x
If you compile with -O2
or -O3
, x
may be optimized away:
gcc -g -O2 main.c -o optimized_debug
Inside GDB:
(gdb) print x
May return “optimized out”.
Common Debug Tools:
Tool | Purpose |
---|---|
gdb | Debugger for stepping through code |
valgrind | Memory leak checker |
strace | Trace system calls |
ltrace | Trace library calls |
Combine Flags Smartly
✅ Development:
gcc -g -O0 -Wall -o app app.c
✅ Release (optional debug):
gcc -g -O2 -o app app.c
You can also strip symbols before release:
strip app
GCC Toolchain Components
What is a Toolchain?
A toolchain is a set of programming tools used together to build (compile), link, and analyze software — especially in systems programming like C, C++, and embedded development.
Think of it like a production line in a factory: each tool does a specific job to turn your source code into a working executable program.
Basic Steps in a Toolchain:
- Preprocessing – handles
#include
,#define
, etc. - Compiling – converts C code into assembly.
- Assembling – converts assembly to object code.
- Linking – combines object files and libraries into a final executable.
Each of these steps is done by a specific tool.
Common Components in a GCC Toolchain:
Step | Tool | Purpose |
---|---|---|
Preprocessing | cpp | Processes #include , macros |
Compiling | gcc or cc1 | Turns C/C++ into assembly code |
Assembling | as | Converts .s to .o |
Linking | ld | Combines .o files into binary |
Debugging | gdb | Debugs your program |
Analyzing | objdump , nm , readelf | Analyze and inspect binaries |
Why Use a Toolchain?
- Automates the process of building software.
- Helps you go from source code to executable step by step.
- Gives you fine control over what happens at each stage.
- Essential in cross-compilation (e.g., compiling code on a PC to run on an embedded board like ARM or ESP32).
Real-World Analogy:
Imagine you’re baking a cake:
- Recipe (source code) → You follow steps (preprocess, compile, etc.)
- Ingredients → Preprocessed and compiled pieces
- Oven → The assembler
- Icing and decoration → Linker adds libraries and finishes the executable
At the end, you get a ready-to-eat cake, or in our case, a working program!
In this tutorial, we’ll understand 5 key tools:
as
— the assemblerld
— the linkerobjdump
— the binary disassemblernm
— lists symbolsreadelf
— reads ELF binary structure
1. as
— The Assembler
What it does:
as
converts assembly language (.s) into machine code object files (.o).
Concept:
After the compiler generates assembly code from C/C++ (gcc -S
), the assembler takes this .s
file and converts it into binary instructions that the CPU can understand.
Example:
gcc -S hello.c # Step 1: Get assembly code (hello.s)
as hello.s -o hello.o # Step 2: Assemble to object file
You now have an object file hello.o
.
2. ld
— The Linker
What it does:
ld
combines multiple .o
files and libraries into a single executable.
Concept:
It resolves symbol references (like function calls), adds startup code, and organizes the layout of memory sections (text, data, etc.).
Example:
ld -o hello hello.o -lc -dynamic-linker /lib64/ld-linux-x86-64.so.2 -e main
Note: Usually, gcc
does this linking automatically. The above is a manual link.
3. objdump
— The Disassembler
What it does:
objdump
shows you what’s inside an object or binary file. You can view assembly code, symbol tables, and more.
Concept:
Useful for debugging or reverse engineering. You can see what machine code was generated from your C/C++ code.
Example:
objdump -d hello.o
This disassembles the object file — shows actual assembly instructions.
You can also inspect all:
objdump -x hello.o
4. nm
— Lists Symbols
What it does:
nm
lists symbols (like functions and global variables) from object files or executables.
Concept:
Symbols can be functions, variables, or labels. It shows whether they’re defined, undefined, or global/static.
Example:
nm hello.o
Sample output:
00000000 T main
U printf
T main
: symbolmain
is defined in text (code) section.U printf
: symbolprintf
is undefined (from libc).
5. readelf
— Reads ELF Binaries
What it does:
readelf
displays information from ELF (Executable and Linkable Format) files — used by Linux.
Concept:
ELF files are used for executables, object files, shared libs, etc. You can view headers, sections, symbol tables, etc.
Example:
readelf -h hello.o # ELF header
readelf -S hello.o # Section headers
readelf -s hello.o # Symbol table
Summary Table
Tool | Purpose | Example |
---|---|---|
as | Assembles .s to .o | as file.s -o file.o |
ld | Links .o to executable | ld file.o -o file |
objdump | Shows machine code, symbols | objdump -d file.o |
nm | Lists functions/variables | nm file.o |
readelf | Displays ELF internals | readelf -h file.o |
Try it Yourself: Mini Demo
- Create a file:
// hello.c
#include <stdio.h>
void greet() { printf("Hello!\n"); }
int main() { greet(); return 0; }
- Compile step by step:
gcc -S hello.c # Creates hello.s
as hello.s -o hello.o # Assemble to object file
ld hello.o -o hello -lc -dynamic-linker /lib64/ld-linux-x86-64.so.2 -e main
./hello # Run the program
- Inspect:
nm hello.o
objdump -d hello.o
readelf -h hello
You can also Visit other tutorials of Embedded Prep
- What is eMMC (Embedded MultiMediaCard) memory ?
- Top 30+ I2C Interview Questions
- Bit Manipulation Interview Questions
- Structure and Union in c
- Little Endian vs. Big Endian: A Complete Guide
- Merge sort algorithm
Special thanks to @mr-raj for contributing to this article on EmbeddedPr
Leave a Reply