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| Release | Flag | Why you would pick it |
|---|---|---|
| C17 (C18) | -std=c17 | The safest default; every compiler supports it |
| C23 | -std=c23 | nullptr, constexpr, typeof, bit-precise integers; check your compiler version first |
| GNU dialect | -std=gnu17 | POSIX and GNU extensions enabled; the default on Linux |
| Strict pedantic | -std=c17 -pedantic-errors | Reject 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 appA 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 -MPmakes the compiler emit.dfiles 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..PHONYstops make from being confused by a file that happens to be namedclean.
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) quitDebug 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?
Why does my program print garbage for a variable?
-Wall -Wextra and run under -fsanitize=address,undefined before reaching for a debugger.Related
Syntax and the compiled toolchain Debugging with gdb, sanitizers and valgrind
Last refreshed 2026-09-18.