Setting up a C toolchain: compiler, make and gdb

Install gcc or clang, choose a language revision, turn on the warnings that matter, and drive the build with make and gdb.

Getting a compiler and choosing a standard

C has no package manager of its own. What you install is a toolchain: a compiler driver (gcc or clang), an assembler, a linker and a C standard library implementation (glibc, musl, or MSVC's UCRT). On Linux install build-essential, on macOS the Xcode command line tools, on Windows MSYS2 with mingw-w64 or a Visual Studio developer prompt.

# check what you actually have
cc --version
gcc --version
clang --version
make --version
gdb --version

# Debian and Ubuntu
sudo apt install build-essential gdb valgrind

# macOS: clang arrives with the command line tools
xcode-select --install
ReleaseFlagWhy you would pick it
C17 (C18)-std=c17The safest default; every compiler supports it
C23-std=c23nullptr, constexpr, typeof, bit-precise integers; check your compiler version first
GNU dialect-std=gnu17POSIX and GNU extensions enabled; the default on Linux
Strict pedantic-std=c17 -pedantic-errorsReject anything the standard forbids, useful for portable code

Compile with warnings on from day one. Retrofitting warnings onto a finished project is far more painful than fixing them as they appear.

gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror -g -O0 main.c -o app

A first Makefile

make compares timestamps: it rebuilds a target only when a prerequisite is newer. The three things to learn are variables, pattern rules and phony targets.

CC      := cc
CFLAGS  := -std=c17 -Wall -Wextra -Wpedantic -g -O2
LDLIBS  := -lm

SRCS    := $(wildcard src/*.c)
OBJS    := $(SRCS:.c=.o)
TARGET  := app

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) $^ -o $@ $(LDLIBS)

# pattern rule: how to turn any .c into a .o
%.o: %.c
	$(CC) $(CFLAGS) -MMD -MP -c $< -o $@

-include $(OBJS:.o=.d)

run: $(TARGET)
	./$(TARGET)

clean:
	rm -f $(OBJS) $(OBJS:.o=.d) $(TARGET)

.PHONY: all run clean
  • -MMD -MP makes the compiler emit .d files listing header dependencies, so editing a header triggers the right rebuilds.
  • $@ is the target, $< the first prerequisite, $^ all prerequisites. Recipe lines must start with a real tab, not spaces.
  • .PHONY stops make from being confused by a file that happens to be named clean.
💡
If make says Nothing to be done for a target that clearly changed, check that the recipe lines are tab-indented and that the target lists every input. Silent no-op builds are nearly always a Makefile mistake, not a compiler one.

Running under gdb

gcc -std=c17 -Wall -Wextra -g -O0 main.c -o app
gdb ./app

# inside gdb
(gdb) break main
(gdb) run
(gdb) next          # step over
(gdb) step          # step into
(gdb) print i
(gdb) backtrace
(gdb) continue
(gdb) quit

Debug builds should be -O0 or -Og. With -O2 the optimiser reorders and inlines code, so stepping jumps around and variables may read as <optimized out>.

FAQ

gcc or clang?
Either. Both implement C17 and C23 and produce comparable code. clang tends to have clearer diagnostics; gcc is the default on most Linux distributions and often has better sanitiser integration on older systems. Write standard C and the choice stops mattering.
Why does my program print garbage for a variable?
Almost always an uninitialised variable or a format-string mismatch. Compile with -Wall -Wextra and run under -fsanitize=address,undefined before reaching for a debugger.

Syntax and the compiled toolchain Debugging with gdb, sanitizers and valgrind

Last refreshed 2026-09-18.