Skip to main content

Build Systems & Toolchain

Understanding the build process is essential for systems programming. Let’s master the tools.

The Compilation Pipeline

C 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.1f;
if (f == 0.1f) { }  // 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 startupSmaller executables
No runtime depsShared between processes
Larger binariesEasier updates
Security: no hijackingCan 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 ..

Toolchain File (CMake)

# 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

1

Makefile from Scratch

Create a project with 3 source files and write a Makefile with automatic dependency generation.
2

Static Library

Create a static library with 2-3 utility functions, then link it to a test program.
3

CMake Project

Convert your Makefile project to CMake with Debug/Release configurations and sanitizer support.
4

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