Class 34 of 37 • Advanced Linux System Programming

Linux GCC Compilation, Makefiles & GDB Debugging - Developer Guide

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)

Static Library (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
Shared Library (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

Next Step in Your Linux Learning Path:

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

Q1: What is the purpose of Position-Independent Code (-fPIC)?

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.

Q2: Why are targets marked as .PHONY in a Makefile?

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)

What is the difference between strace and ltrace?

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).

Share Share on Facebook Share on Twitter Share on LinkedIn Pin on Pinterest Share on Stumbleupon Share on Tumblr Share on Reddit Share on Diggit

You may also like this!