The Workshop
In December 1997, id Software released the Doom[1] source code to the public.



Inside the tarball, in a directory called linuxdoom-1.10,
there is a file called Makefile. A few dozen lines of declarations
and rules. It assumes a Unix box with gcc and make. That file
is what built the version of Doom that ran on Linux from 1994
onward — id programmer Dave Taylor's[2] port, compiled from the same
C source the DOS version used.
The 1999 Quake[3] source release ships with Makefiles. The id
Tech 3[4] release does. Every C-era game whose code you can read
in public was built by typing make at a prompt and waiting.
There is no version of "shipping a C program" that does not involve
a build system. There is no story of fixing a bug in a real C program
that does not involve a debugger. There is no modern memory-safety
story that does not involve at least one of valgrind,
AddressSanitizer, or both.
You wrote your first C programs in the previous chapter and ran
them with gcc hello.c -o hello. That worked because each program
was one file and never crashed. The next chapter is where the real
curriculum starts: multi-file projects, libtci, shells, threading.
Before that, you need the workshop. This chapter is the workshop.
The implementation pages walk you through writing a tiny sort
utility, watching it misbehave, fixing it with the right tool for
each kind of misbehaviour, and then refactoring it into a multi-file
project with a Makefile. By the end you have the toolchain id used
to ship Doom, configured the same way they configured it. Start at
Setup.
The First Time I Ran Valgrind
The first time I ran a program under valgrind I had been writing
C for a few weeks. The program worked — until it didn't. It would
crash on input that should have been fine. I did what I had been
doing: added printf calls between every line until I narrowed
down where it crashed. That took an hour. Then someone showed me
valgrind. I rebuilt the program, ran it under valgrind, and the
output told me — in three lines — that I had read past the end of a
buffer, the exact line, and the size of the read. Two seconds.
The program I built to learn all of them was a sort utility:
reads lines from a file, sorts them alphabetically, prints them.
Small enough to write in an afternoon. Large enough to hold three
bugs at once.
I shipped the first one before I had tested anything:
in = fopen(argv[1], "r");
while (fgets(buf, sizeof(buf), in)) {No null check on fopen. Run it on a missing file and it segfaults
inside libc. gdb showed me in was NULL in under a minute. I
had wasted a morning on the same mistake two weeks earlier.
The second — free(lines) without first freeing each string it held
— valgrind caught as seventeen bytes leaked across three blocks.
The third was one character:
copy = malloc(strlen(buf));
strcpy(copy, buf);strlen does not count the null terminator. strcpy writes it
anyway — one byte past the end of every allocation. AddressSanitizer
aborted the program at the exact line and printed the allocation
size.
Once all three were gone I had a working single-file program. The
natural next step was splitting it: lines.c for the dynamic-array
API, sort.c for the program that uses it. That split is the last
thing this chapter builds, and the first time the Makefile matters.
The Project
Build a program called sort that:
- Takes a filename on the command line.
- Reads the lines of that file into memory.
- Sorts them alphabetically.
- Prints them to standard output.
- Returns 0 on success, 1 on usage or I/O error.
When you are done, the project must compile cleanly with
gcc -Wall -Wextra -g -std=c99, run clean under valgrind --leak-check=full,
and run clean when rebuilt with -fsanitize=address -fsanitize=undefined. The final source tree is:
sort.c # main(): argv handling, file open, glue
lines.c # the lines_t API
lines.h # the lines_t API declarations
Makefile # gcc flags, pattern rules, all/clean/fclean/reThe Tester
The tester lives in thecodingidiot-com/f05-building-c. It builds
your project with make, runs it on a sample input and checks the
output, runs it with a missing file and with no arguments and checks
the exit code, re-runs the binary under valgrind, and rebuilds
with -fsanitize=address -fsanitize=undefined and runs that clean.
All six checks must pass.
git clone https://github.com/thecodingidiot-com/f05-building-c.git
cp f05-building-c/test.sh ~/f05-practice/
cd ~/f05-practice
bash test.shThe companion repo also ships a broken/sort.c — the single-file
pre-debug snapshot with the three bugs already planted. If you want
to skip the writing pages and go straight to the debugging exercise,
copy that file into your practice directory and start from there.