Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Build Systems & Toolchain

Understanding the build process is essential for systems programming. In higher-level languages, you run python script.py or go run main.go and the toolchain is invisible. In C, the toolchain is your constant companion: you control which warnings to enforce, which optimizations to apply, how to link libraries, and how to structure multi-file projects. Mastering these tools is not optional overhead — it is a core competency that separates hobbyist C from production C.

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

Think of optimization levels as a trust dial between you and the compiler. At -O0, the compiler translates your code as literally as possible — every variable gets a memory location, every operation happens in order. At -O3, the compiler aggressively restructures your code: reordering instructions, eliminating dead stores, inlining functions, vectorizing loops. This is usually fine, but it can expose latent undefined behavior in your code that -O0 happened to hide.
# No optimization (best for debugging)
# Variables are exactly where you expect them in GDB. Slowest output.
gcc -O0 main.c -o main

# Basic optimizations
# Removes trivially dead code, does simple register allocation. Modest speedup.
gcc -O1 main.c -o main

# Recommended for production -- the "safe aggressive" level
# Inlines small functions, optimizes loops, reorders code for cache. 
# Does NOT enable optimizations that increase code size significantly.
gcc -O2 main.c -o main

# Aggressive (may change behavior of code with undefined behavior!)
# Enables auto-vectorization, function cloning, loop unrolling.
# Can make binaries larger. Test thoroughly when switching from -O2.
gcc -O3 main.c -o main

# Optimize for size (useful for embedded / cache-constrained code)
# Smaller binaries can actually run faster when the hot path fits in L1 instruction cache.
gcc -Os main.c -o main

# Optimize for debugging (O1 + debug info preservation)
# Best balance when you need some optimization but also want sane GDB sessions.
gcc -Og -g main.c -o main

# Link-time optimization (whole program analysis)
# The compiler can inline across translation units, which is normally impossible.
# Particularly effective for large codebases with many small functions.
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
Practical guidance: Default to static linking for standalone tools and embedded systems where you control the entire deployment. Use dynamic linking for shared libraries that multiple programs depend on (like libc, libssl) or when you need to update a library without recompiling every program that uses it. Many production systems (Go binaries, single-binary tools) have moved back toward static linking because the deployment simplicity outweighs the disk space cost. Common pitfall: Forgetting to set LD_LIBRARY_PATH or install shared libraries on the target machine. If you deploy a dynamically linked binary and the .so files are not in the linker’s search path, you get a confusing “No such file or directory” error even though the executable exists. Use ldd ./your_program to check which shared libraries are needed.

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