Master GCC Command Line Options: A Beginner-Friendly Guide (2025)
, ,

Master GCC Command Line Options: A Beginner-Friendly Guide (2025)

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:

OptionDescription
-o outputSpecify the name of the output file.
-cCompile source to object file (.o) without linking.
-WallEnable most compiler warnings.
-WerrorTreat all warnings as errors.
-gInclude debug information.
-O0 to -O3Control optimization level.
-EStop after preprocessing.
-SStop after compilation (output assembly).
-vVerbose 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:

GoalFlags 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

StageCommand FlagDescription
1. Preprocessing-EHandles macros, #include, #define, removes comments
2. Compilation-SConverts preprocessed code to Assembly
3. Assembly-cConverts 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

ToolPurpose
arm-none-eabi-gccGCC for bare-metal ARM targets
arm-linux-gnueabihf-gccGCC for Linux ARM hard-float ABI targets
aarch64-linux-gnu-gccGCC for 64-bit ARM Linux targets
x86_64-w64-mingw32-gccGCC 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

FlagDescription
-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 comboUse case
-g -O0 (default)Easiest debugging: variables, breakpoints work as expected
-g -O2 or -g -O3Debug optimized code: might skip or inline functions, remove variables
-O2 onlyNo debug info, faster execution
-g onlyDebug 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:

ToolPurpose
gdbDebugger for stepping through code
valgrindMemory leak checker
straceTrace system calls
ltraceTrace 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:

  1. Preprocessing – handles #include, #define, etc.
  2. Compiling – converts C code into assembly.
  3. Assembling – converts assembly to object code.
  4. 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:

StepToolPurpose
PreprocessingcppProcesses #include, macros
Compilinggcc or cc1Turns C/C++ into assembly code
AssemblingasConverts .s to .o
LinkingldCombines .o files into binary
DebugginggdbDebugs your program
Analyzingobjdump, nm, readelfAnalyze 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 assembler
  • ld — the linker
  • objdump — the binary disassembler
  • nm — lists symbols
  • readelf — 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: symbol main is defined in text (code) section.
  • U printf: symbol printf 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

ToolPurposeExample
asAssembles .s to .oas file.s -o file.o
ldLinks .o to executableld file.o -o file
objdumpShows machine code, symbolsobjdump -d file.o
nmLists functions/variablesnm file.o
readelfDisplays ELF internalsreadelf -h file.o

Try it Yourself: Mini Demo

  1. Create a file:
// hello.c
#include <stdio.h>
void greet() { printf("Hello!\n"); }
int main() { greet(); return 0; }
  1. 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
  1. Inspect:
nm hello.o
objdump -d hello.o
readelf -h hello

You can also Visit other tutorials of Embedded Prep 

Special thanks to @mr-raj for contributing to this article on EmbeddedPr

Leave a Reply

Your email address will not be published. Required fields are marked *