Build Systems & Toolchain
Understanding the build process is essential for systems programming. Let’s master the tools.
The Compilation Pipeline
GCC Deep Dive
Essential Warning Flags
# Minimum for any serious project
gcc -Wall -Wextra -Werror -std=c11 main.c -o main
# Full paranoid mode (recommended)
gcc -Wall -Wextra -Wpedantic -Werror \
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=2 \
-Wnull-dereference -Wstack-protector \
-Wstrict-overflow=3 -Warray-bounds=2 \
-Wimplicit-fallthrough=3 \
-Wconversion -Wsign-conversion \
-Wdouble-promotion -Wfloat-equal \
-Wshadow -Wcast-qual -Wcast-align \
-Wwrite-strings -Wduplicated-cond \
-Wlogical-op -Wredundant-decls \
-std=c11 -pedantic \
main.c -o main
What Each Warning Catches
// -Wconversion: Catches implicit narrowing conversions
int a = 100000 ;
short b = a; // Warning! Potential truncation
// -Wshadow: Catches variable shadowing
int x = 5 ;
{
int x = 10 ; // Warning! Shadows outer x
}
// -Wfloat-equal: Catches float equality comparisons
float f = 0.1 f ;
if (f == 0.1 f ) { } // Warning! Dangerous comparison
// -Wcast-qual: Catches dropping const
const int * p = & a;
int * q = p; // Warning! Drops const
// -Wformat=2: Catches format string vulnerabilities
printf (user_input); // Warning! Format string attack
Optimization Levels
# No optimization (best for debugging)
gcc -O0 main.c -o main
# Basic optimizations
gcc -O1 main.c -o main
# Recommended for production
gcc -O2 main.c -o main
# Aggressive (may break some code!)
gcc -O3 main.c -o main
# Optimize for size
gcc -Os main.c -o main
# Optimize for debugging (O1 + debug info)
gcc -Og -g main.c -o main
# Link-time optimization (whole program)
gcc -O2 -flto main.c -o main
Sanitizers (Find Bugs at Runtime)
# Address Sanitizer - buffer overflows, use-after-free
gcc -fsanitize=address -g main.c -o main
# Undefined Behavior Sanitizer
gcc -fsanitize=undefined -g main.c -o main
# Thread Sanitizer - data races
gcc -fsanitize=thread -g main.c -o main
# Memory Sanitizer (Clang only) - uninitialized reads
clang -fsanitize=memory -g main.c -o main
# Stack protection
gcc -fstack-protector-strong main.c -o main
# All the sanitizers (except Thread, which conflicts)
gcc -fsanitize=address,undefined -g main.c -o main
Sanitizers have runtime overhead . Use them during development and testing, not in production builds.
Understanding Object Files
# Compile to object file
gcc -c main.c -o main.o
gcc -c util.c -o util.o
# Link object files
gcc main.o util.o -o program
# Inspect object file
nm main.o # List symbols
objdump -d main.o # Disassemble
objdump -h main.o # Section headers
readelf -a main.o # ELF details (Linux)
Symbol Types
$ nm main.o
0000000000000000 T main # T = Text (code), defined here
U printf # U = Undefined, needs linking
0000000000000000 D global_var # D = Data, initialized
0000000000000004 B uninit_var # B = BSS, uninitialized
0000000000000000 t helper # t = local text (static function)
Static vs Dynamic Libraries
Creating a Static Library
# Compile source files
gcc -c mylib.c -o mylib.o
gcc -c utils.c -o utils.o
# Create static library (.a)
ar rcs libmylib.a mylib.o utils.o
# Link against static library
gcc main.c -L. -lmylib -o program
# Or specify path directly
gcc main.c libmylib.a -o program
Creating a Dynamic Library
# Compile with position-independent code
gcc -c -fPIC mylib.c -o mylib.o
gcc -c -fPIC utils.c -o utils.o
# Create shared library (.so)
gcc -shared -o libmylib.so mylib.o utils.o
# Link against dynamic library
gcc main.c -L. -lmylib -o program
# Run (must find library at runtime)
export LD_LIBRARY_PATH = .: $LD_LIBRARY_PATH
./program
# Or use rpath
gcc main.c -L. -lmylib -Wl,-rpath,. -o program
When to Use Each
Static (.a) Dynamic (.so) Faster startup Smaller executables No runtime deps Shared between processes Larger binaries Easier updates Security: no hijacking Can be preloaded
Make
Basic Makefile
# Variables
CC = gcc
CFLAGS = -Wall -Wextra -Werror -std=c11 -g
LDFLAGS = -lm
# Source files
SRCS = main.c util.c parser.c
OBJS = $( SRCS:.c=.o )
TARGET = myprogram
# Default target
all : $( TARGET )
# Link
$( TARGET ) : $( OBJS )
$( CC ) $( OBJS ) $( LDFLAGS ) -o $@
# Compile
% .o : % .c
$( CC ) $( CFLAGS ) -c $< -o $@
# Dependencies (auto-generated is better)
main.o : main.c util.h parser.h
util.o : util.c util.h
parser.o : parser.c parser.h
# Clean
clean :
rm -f $( OBJS ) $( TARGET )
# Phony targets
.PHONY : all clean
Advanced Makefile with Auto-Dependencies
CC = gcc
CFLAGS = -Wall -Wextra -std=c11 -g -MMD -MP
LDFLAGS =
SRC_DIR = src
BUILD_DIR = build
SRCS = $( wildcard $( SRC_DIR )/ * .c)
OBJS = $( SRCS: $( SRC_DIR ) /%.c= $( BUILD_DIR ) /%.o )
DEPS = $( OBJS:.o=.d )
TARGET = $( BUILD_DIR ) /program
all : $( TARGET )
$( TARGET ) : $( OBJS )
@ mkdir -p $( dir $@ )
$( CC ) $( OBJS ) $( LDFLAGS ) -o $@
$( BUILD_DIR ) /%.o: $( SRC_DIR ) /%.c
@ mkdir -p $( dir $@ )
$( CC ) $( CFLAGS ) -c $< -o $@
-include $( DEPS )
clean :
rm -rf $( BUILD_DIR )
.PHONY : all clean
CMake
Basic CMakeLists.txt
cmake_minimum_required ( VERSION 3.16)
project (MyProject C)
# C standard
set (CMAKE_C_STANDARD 11)
set (CMAKE_C_STANDARD_REQUIRED ON )
set (CMAKE_C_EXTENSIONS OFF )
# Compiler flags
add_compile_options (-Wall -Wextra -Werror)
# Debug/Release configurations
set (CMAKE_C_FLAGS_DEBUG "-g -O0 -fsanitize=address,undefined" )
set (CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG" )
# Executable
add_executable (myprogram
src/main.c
src/util.c
src/parser.c
)
# Include directories
target_include_directories (myprogram PRIVATE include)
# Link libraries
target_link_libraries (myprogram m pthread)
CMake with Libraries
cmake_minimum_required ( VERSION 3.16)
project (MyProject C)
set (CMAKE_C_STANDARD 11)
set (CMAKE_C_STANDARD_REQUIRED ON )
# Static library
add_library (mylib STATIC
src/lib/util.c
src/lib/parser.c
)
target_include_directories (mylib PUBLIC include)
# Shared library
add_library (mylib_shared SHARED
src/lib/util.c
src/lib/parser.c
)
target_include_directories (mylib_shared PUBLIC include)
# Executable using library
add_executable (myprogram src/main.c)
target_link_libraries (myprogram mylib)
# Testing (optional)
enable_testing ()
add_executable (test_util tests/test_util.c)
target_link_libraries (test_util mylib)
add_test ( NAME test_util COMMAND test_util)
Building with CMake
# Create build directory
mkdir build && cd build
# Configure (Debug)
cmake -DCMAKE_BUILD_TYPE=Debug ..
# Configure (Release)
cmake -DCMAKE_BUILD_TYPE=Release ..
# Build
cmake --build .
# Or use make directly
make -j$( nproc )
# Run tests
ctest
# Install
cmake --install . --prefix /usr/local
pkg-config
Finding and using system libraries:
# Find library flags
pkg-config --cflags --libs openssl
# Output: -I/usr/include/openssl -lssl -lcrypto
# Use in compilation
gcc main.c $( pkg-config --cflags --libs openssl ) -o main
In CMake:
find_package (PkgConfig REQUIRED)
pkg_check_modules(OPENSSL REQUIRED openssl)
target_include_directories (myprogram PRIVATE ${OPENSSL_INCLUDE_DIRS} )
target_link_libraries (myprogram ${OPENSSL_LIBRARIES} )
Cross-Compilation
ARM Cross-Compilation
# Install ARM toolchain
sudo apt install gcc-arm-linux-gnueabihf
# Cross-compile
arm-linux-gnueabihf-gcc -o program main.c
# With CMake
cmake -DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc \
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_SYSTEM_PROCESSOR=arm ..
# arm-toolchain.cmake
set ( CMAKE_SYSTEM_NAME Linux)
set ( CMAKE_SYSTEM_PROCESSOR arm)
set (CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set (CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
set (CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)
set (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set (CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set (CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
cmake -DCMAKE_TOOLCHAIN_FILE=arm-toolchain.cmake ..
Project Structure Best Practices
myproject/
├── CMakeLists.txt
├── README.md
├── LICENSE
├── .gitignore
├── include/ # Public headers
│ └── myproject/
│ ├── public.h
│ └── types.h
├── src/ # Implementation
│ ├── main.c
│ ├── module1.c
│ ├── module1.h # Private header
│ ├── module2.c
│ └── module2.h
├── lib/ # External libraries (vendored)
│ └── cJSON/
├── tests/
│ ├── CMakeLists.txt
│ ├── test_module1.c
│ └── test_module2.c
├── docs/
├── build/ # Out-of-source build (gitignored)
└── scripts/
└── build.sh
Exercises
Makefile from Scratch
Create a project with 3 source files and write a Makefile with automatic dependency generation.
Static Library
Create a static library with 2-3 utility functions, then link it to a test program.
CMake Project
Convert your Makefile project to CMake with Debug/Release configurations and sanitizer support.
Sanitizer Safari
Write intentionally buggy code (buffer overflow, use-after-free, data race) and verify sanitizers catch them.
Next Up
Debugging Fundamentals Master GDB and memory debugging tools