Building professional software in Linux requires more than typing source code. System programmers must master the toolchain: the GNU Compiler Collection (GCC), automation with GNU Make, interactive debugging with GDB, and kernel diagnostic tracing with strace.
In this master class, we break down the 4 distinct compilation phases, create both static (.a) and dynamic shared (.so) libraries, write production Makefiles, and learn step-by-step techniques to track down crashes and segmentation faults in GDB.
1. The 4 Phases of GCC Compilation
When you run gcc program.c -o program, the compiler transparently executes four sequential stages:
| Phase | Sub-tool | Command Flag | Input → Output | Action Performed |
|---|---|---|---|---|
| 1. Preprocessing | cpp |
gcc -E file.c -o file.i |
.c → .i |
Expands #include headers, replaces #define macros, strips comments. |
| 2. Compilation | cc1 |
gcc -S file.i -o file.s |
.i → .s |
Parses C syntax, optimizes AST, outputs assembly language instructions. |
| 3. Assembly | as |
gcc -c file.s -o file.o |
.s → .o |
Translates assembly mnemonics into machine code binary object files. |
| 4. Linking | ld |
gcc file.o -o program |
.o → ELF |
Resolves external symbol references, links libraries (libc), produces ELF binary. |
2. Essential GCC Flags Every Linux Developer Must Know
-Wall -Wextra -pedantic: Enables comprehensive compiler warning diagnostics. Catch bugs at compile time before they reach production!-g: Embeds DWARF debugging symbols into the ELF binary so GDB and Valgrind can map memory back to source code line numbers.-O2 / -O3: Activates compiler code optimizations (inlining, loop unrolling, vectorization).-pthread: Enables POSIX thread support and links libpthread correctly.-I/path/to/include: Adds custom search directory for .h header files.-L/path/to/lib -lname: Specifies library directories and links libname.so or libname.a.3. Static Libraries (.a) vs Dynamic Shared Libraries (.so)
libmath.a)Compiled directly into the final executable at link time.
# 1. Compile to object file
$ gcc -c mymath.c -o mymath.o
# 2. Bundle into static archive (.a)
$ ar rcs libmymath.a mymath.o
# 3. Link with main application
$ gcc main.c -L. -lmymath -o app_static
libmath.so)Loaded dynamically by ld.so at runtime.
# 1. Compile Position-Independent Code
$ gcc -fPIC -c mymath.c -o mymath.o
# 2. Create shared object (.so)
$ gcc -shared mymath.o -o libmymath.so
# 3. Link and execute with path
$ gcc main.c -L. -lmymath -o app_shared
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
$ ./app_shared
4. Makefile Automation: Clean, Portable Builds
A Makefile defines dependency graphs between source files and targets, rebuilding only modified components:
# Makefile - Standard GNU Make template for C projects
CC = gcc
CFLAGS = -Wall -Wextra -g -O2
LDFLAGS = -pthread
TARGET = myapp
SRCS = main.c utils.c server.c
OBJS = $(SRCS:.c=.o)
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(OBJS) $(LDFLAGS) -o $@
@echo "Build successful: $(TARGET)"
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
@echo "Cleaned object files and binary."
Automatic variables reference targets and prerequisites cleanly: $@ represents the target name, $< is the first prerequisite, and $^ is all prerequisites.
5. Interactive Debugging with GDB (GNU Debugger)
When a C program crashes with Segmentation fault (core dumped), GDB inspects execution state at the exact moment of failure:
| GDB Command | Short | Action & Description |
|---|---|---|
gdb ./binary |
- | Loads the executable binary into GDB with debugging symbols. |
break main / break file.c:25 |
b |
Sets a breakpoint at function entry or line number. |
run arg1 arg2 |
r |
Starts program execution with command-line arguments. |
next |
n |
Executes next source line, stepping OVER function calls. |
step |
s |
Executes next line, stepping INTO function calls. |
print var |
p |
Evaluates and displays variable value or memory pointer. |
backtrace |
bt |
Prints the full function call stack trace leading to a crash. |
quit |
q |
Exits the GDB debugger session. |
6. Tracing System Calls with strace
strace traces all interactions between user space and the Linux kernel without requiring source code or recompilation:
# Trace file open/close calls
$ strace -e trace=open,close ./myapp
# Count system calls and time spent
$ strace -c ./myapp
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
45.20 0.000120 6 20 read
35.10 0.000093 4 22 write
12.40 0.000033 8 4 mmap
Put your knowledge into action with complete university exam programs in Class 35: 25+ Linux Programming Practical Lab Programs.
Continue to Class 35: Lab Practice Programs → Previous Class: ← I/O Multiplexing (epoll)
7. University Viva & Job Interview Q&A
Answer: -fPIC instructs the compiler to generate machine code using relative address offsets rather than absolute memory addresses. This allows shared libraries (.so) to be loaded at arbitrary virtual memory addresses across different running processes without memory address conflicts.
Answer: A target marked .PHONY: clean tells Make that clean is an action rather than a physical file on disk. If a file named "clean" ever exists in the directory, Make would otherwise assume the target is up-to-date and refuse to execute the rule.
8. Frequently Asked Questions (FAQ)
strace monitors system calls made to the Linux kernel (e.g. open, read, write, fork). ltrace intercepts user-space dynamic library calls (e.g. malloc, printf, strcmp).