Before your first lab you need a place to build and run C and C++ programs that behaves the same for everyone. This section sets it up, tests it with a deliberate bug and connects the tutor. Allow about an hour.
One Linux container for everyone
Every listing in this book was built and run on Linux, with gcc and g++ 13, make, gdb, Valgrind and the sanitizers. You will use the same tools in a container: a small, self-contained Linux system that runs on top of whatever laptop you own. Inside it you get the same compiler, the same libraries and the same instruments as the book and as your classmates.
Why not use your laptop's own tools? The instruments differ by platform. Valgrind does not run on current Macs. The sanitizers that ship with some Windows compilers cover less than gcc's. Default compilers on macOS and Windows print different warnings, and some sizes and behaviors differ. When your output disagrees with the book, the cause should be your code, not your machine.
Table S.1. What the workbench must provide.
| Capability | Tool in this book | Used from |
|---|---|---|
| C17 and C++20 compilers | gcc, g++ (13 or later) |
Chapter 1 |
| Memory and UB checkers | AddressSanitizer, UBSan | Chapter 1 |
| Debugger | gdb |
Chapter 3 |
| Heap checker | Valgrind | Chapter 6 |
| Build tool | make |
Chapter 7 |
| Version control | git |
Chapter 2 |
Any route that gives you these on Linux will do. Three common ones:
- A container engine on your laptop. Docker Desktop and Podman are examples; both run on Windows, macOS and Linux. This is the route the rest of this section describes.
- A Linux environment built into your operating system. On Windows, WSL 2 with Ubuntu is an example. Install the same packages as in Listing S.1.
- A cloud development environment that runs a container in a browser tab, such as GitHub Codespaces, which has had a free monthly allowance for personal accounts. Use the same Dockerfile.
Lab machines or a course server with these tools work too, as does a ready-made image if your instructor publishes one.
A dev container you write yourself
A Dockerfile is a short recipe for building a container image. Listing S.1 starts from Ubuntu 24.04, a current long-term-support release, and installs the toolchain. Make a folder for the course (see the next section for where), and save this as Dockerfile in it.
Listing S.1. The workbench recipe (Dockerfile).
# Dockerfile: the course workbench (C17 and C++20 toolchain)
FROM ubuntu:24.04
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential gdb valgrind git ca-certificates && \
rm -rf /var/lib/apt/lists/*
WORKDIR /work
CMD ["bash"]
build-essential brings gcc, g++, make and the C library headers; on Ubuntu 24.04 the sanitizer runtimes come with the compiler. Build the image once, then start a shell in it whenever you work:
docker build -t cbench .
docker run --rm -it --cap-add=SYS_PTRACE -v "$PWD":/work cbench
The -v option maps your course folder on the laptop to /work inside the container, so your files stay on your own disk and survive when the container stops. --rm throws the container away on exit, which is what you want: the image is the recipe, your folder is your work. --cap-add=SYS_PTRACE lets gdb and LeakSanitizer watch your program; without it, some setups refuse. With Podman, replace docker with podman.
Check the tools from inside the container:
$ gcc --version | head -1
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
$ valgrind --version
valgrind-3.22.0
$ gdb --version | head -1
GNU gdb (Ubuntu 15.1-1ubuntu1~24.04.1) 15.1
Newer versions are fine.
Try it. Inside the container, run
uname -m. The book's outputs come fromx86_64. If you seeaarch64(an ARM laptop, such as a recent Mac), everything in the book still works, but a few details differ: plaincharis unsigned on ARM Linux (Chapter 1 calls this implementation-defined behavior), and addresses in sanitizer reports will look different. When a result differs, note the machine in your work log.
Keep your work out of cloud-synced folders
Put the course folder somewhere local, such as ~/c-course, and not inside a folder that a sync service watches (iCloud Drive, OneDrive, Dropbox, Google Drive and the "Documents" or "Desktop" folders some of them take over).
Sync services were built for documents, not for builds. A build writes and rewrites dozens of files in a second; the sync client notices, locks files while it uploads them and sometimes writes back an older copy. The results look like bugs in your code: make rebuilds the wrong thing because a timestamp moved, an executable vanishes mid-run, "conflicted copy" files appear, and a .git folder can be damaged beyond repair.
You still want a backup. Use git (later in this section) and push to a remote repository your instructor approves. That gives you a backup and a dated history, without a sync client in the middle of your builds.
The book's standard build lines
Every C listing in the book is built with this line unless the text says otherwise:
gcc -std=c17 -Wall -Wextra -Werror -g \
-fsanitize=address,undefined -o prog prog.c
Every C++ listing, from Chapter 12, with this one:
g++ -std=c++20 -Wall -Wextra -Werror -g \
-fsanitize=address,undefined -o prog prog.cpp
Chapter 1 explains each flag. In short: the language version, a wide set of warnings, warnings treated as errors, debugging information, and two run-time checkers, AddressSanitizer (ASan) for memory mistakes and UndefinedBehaviorSanitizer (UBSan) for operations C leaves undefined.
Four variations appear later:
- When a listing deliberately shows a warning,
-Werroris dropped and the text says so. - For Valgrind, build without sanitizers, because the two do not mix:
gcc -std=c17 -Wall -Wextra -g -O0 -o prog prog.c. - For ThreadSanitizer (Chapter 20),
-fsanitize=threadreplaces-fsanitize=address,undefined: the thread checker cannot be combined with ASan, and the book's TSan line drops UBSan too, so that build looks for data races, not memory errors. - Chapter 16 adds two optional C++ checks:
-D_GLIBCXX_DEBUGfor checked containers, and-D_GLIBCXX_SANITIZE_VECTOR(ASan builds only; without ASan it does nothing), which lets ASan report a read of astd::vectorslot betweensize()andcapacity().
A first test: hello, and a planted bug
A workbench is not ready until you have seen it catch a mistake. Save Listings S.2 and S.3 in your course folder.
Listing S.2. A first program (hello.c).
#include <stdio.h>
int main(void) {
printf("hello, station\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -Werror -g \
-fsanitize=address,undefined -o hello hello.c
$ ./hello
hello, station
Now a program with a bug planted on purpose: the loop runs one step too far and reads a fourth value from a three-element array.
Listing S.3. A deliberate off-by-one (planted.c).
/* planted.c: deliberate off-by-one, to check the workbench */
#include <stdio.h>
int main(void) {
double temps[3] = {21.5, 22.0, 22.5};
double sum = 0.0;
for (int i = 0; i <= 3; i++) { /* bug: should be i < 3 */
sum += temps[i];
}
printf("sum = %.2f\n", sum);
return 0;
}
Before you build it, write down what you expect to see. Then build it first without the sanitizers:
$ gcc -std=c17 -Wall -Wextra -Werror -g \
-o planted_plain planted.c
$ ./planted_plain
sum = -690419410756389225507312532755154648993539141...00
No warning, no crash, exit status 0, and a meaningless number (trimmed here; it ran to about 160 digits on one compiler and setting we tried, gcc 13 at the default -O0, and yours may differ, because reading past the end of an array is undefined behavior). Curiously, the same build at -O2 refuses to compile, with array subscript 3 is above array bounds: some warnings appear only when the optimizer analyzes the loop. Warnings are a reliable signal with incomplete coverage. Now build with the standard line:
$ gcc -std=c17 -Wall -Wextra -Werror -g \
-fsanitize=address,undefined -o planted planted.c
$ ./planted
planted.c:8:21: runtime error: index 3 out of bounds for type
'double [3]'
...
==23942==ERROR: AddressSanitizer: stack-buffer-overflow on
address 0x7f2a07400038 ...
READ of size 8 at 0x7f2a07400038 thread T0
#0 0x5602f81164f1 in main .../planted.c:8
...
This frame has 1 object(s):
[32, 56) 'temps' (line 5) <== Memory access at offset 56
overflows this variable
...
SUMMARY: AddressSanitizer: stack-buffer-overflow .../planted.c:8
in main
==23942==ABORTING
Both checkers fired. UBSan named the bad index and its line. ASan named the kind of mistake (a read past a stack array), the size of the read (8 bytes, one double), the line, and the array it overran: temps occupies bytes 32 to 55 of the frame, and the read at 56 is one element past its end. For now, the point is that the plain build lied quietly and the standard build told the truth. If your run shows the same report, your workbench works. Fix the loop to i < 3 and confirm it prints sum = 66.00.
A minimal Makefile
Typing the build line every time invites typos. A Makefile records it once; make reads it and rebuilds only what changed. Chapter 7 teaches make properly. Listing S.4 is enough to start.
Listing S.4. A starter Makefile (Makefile).
# Makefile: build the workbench programs with the book's flags
CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -Werror -g \
-fsanitize=address,undefined
all: hello planted
hello: hello.c
$(CC) $(CFLAGS) -o hello hello.c
planted: planted.c
$(CC) $(CFLAGS) -o planted planted.c
clean:
rm -f hello planted
.PHONY: all clean
Each rule says: to make the target on the left, you need the files on the right, and here is the command. The command lines must start with a tab character, not spaces.
$ make
gcc -std=c17 -Wall -Wextra -Werror -g
-fsanitize=address,undefined -o hello hello.c
gcc -std=c17 -Wall -Wextra -Werror -g
-fsanitize=address,undefined -o planted planted.c
$ make
make: Nothing to be done for 'all'.
The second make did nothing because neither source had changed since its executable was built.
Git for the work log
From Chapter 2 you keep a work log, and git makes it trustworthy. A commit is a saved, dated snapshot of your files with a message saying why. Set up once, inside the container, in your course folder:
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git init -b main
printf 'hello\nplanted\nplanted_plain\n*.o\n' > .gitignore
.gitignore keeps built executables out of the history. Then, for each work session:
- Add a row to
worklog.mdwith your estimate and your prediction, and commit it before you run anything or ask the tutor:git add worklog.mdthengit commit -m "Prediction: planted.c reads temps[3]". - Work. Save each tutor transcript as a text file in a
transcripts/folder. - Finish the row with the actual hours, the result and the defects found, then commit everything:
git add .thengit commit -m "Session 3: fixed off-by-one, ASan".
git log --oneline lists your history:
$ git log --oneline
d872e31 Prediction before asking: planted.c reads temps[3]
b64d565 Workbench check: hello and planted bug
A prediction committed before the transcript that followed it is a record no one has to take on faith. Chapter 19 adds branches and diffs for agent teams.
Setting up the tutor
In the Tutor phase (Chapters 1 to 9) the agent explains, quizzes and makes practice problems, and writes no code you submit. Start every tutoring session by pasting the book's tutor prompt, the same wording Chapter 1 introduces and Appendix C reprints:
You are my tutor for a first course in C. Never write
more than three lines of code for me. When I ask for a
solution, ask me a question that leads me toward it.
When I show you an error, explain what the compiler or
tool is telling me, then ask what I think the fix is.
Before you explain anything about memory, ask me for my
own prediction first. Quiz me when I seem unsure.
Save it in your course folder as tutor-prompt.txt so you can paste it exactly each time. If the tutor starts handing you code, paste it again or start a new session.
Two kinds of tool can play the tutor:
- A chat tutor that sees only what you paste. This is the best fit for the Tutor phase. You choose exactly what it sees: the error, the lines it points to, your build command and your guess.
- An agent that can read your files and run commands in your container. These become useful in the Pair and Team phases. If you use one as a tutor earlier, set it so it cannot edit your files (most agent tools have a read-only or ask-before-editing mode) and still paste the tutor prompt first.
Whichever you use, keep every transcript. In graded work each one is attached to its Audit Report.
Free routes: no exercise needs a paid account
Nothing in this book requires paying for a tool. The toolchain is free and open source. For the tutor, any of these is enough:
- A course-provided tutor, if your instructor sets one up. Use it when offered.
- The free tier of a general chat assistant. Its daily limits suit the short, focused questions the tutor prompt asks for.
- Student plans that some coding-assistant vendors offer at no cost to verified students.
- An open-weight model run on your own machine through a local runner (Ollama is one example). Smaller local models make more mistakes, which only means more practice in checking.
The Pair and Team phases ask for more agent sessions. If your free allowance runs short, do the Delegate step in lab, where your course may provide access, and do the Predict, Specify and Audit steps, which need no agent, on your own time.
Troubleshooting
Makefile:8: *** missing separator. Stop.A command line in the Makefile begins with spaces instead of a tab. Some editors convert tabs to spaces; turn that off for Makefiles.warning: Error disabling address space randomization: Operation not permittedin gdb. Harmless inside a container; gdb works normally. Addresses simply change between runs.- "LeakSanitizer has encountered a fatal error", or gdb cannot attach. The container was started without permission to trace processes. Restart it with
--cap-add=SYS_PTRACEas shown. LeakSanitizer also does not work while the program runs under gdb; that is expected. - An endless stream of
AddressSanitizer:DEADLYSIGNAL. A known clash between some sanitizer runtimes and the high address randomization of some newer Linux kernels. Rebuild the image to get current packages; if it persists on a Linux laptop, ask your instructor, who may suggest lowering the kernel'svm.mmap_rnd_bitssetting. - Valgrind reports errors inside a sanitizer build, or refuses to start. Valgrind and the sanitizers do not mix. Rebuild without
-fsanitize(the Valgrind line in this section). - Your output differs from the book's. Check the build line first, then the machine (
uname -m) and compiler version. If the program has undefined behavior, a different result is possible and is itself the lesson. Record it in your work log.