Interactive Lecture Plan
This document is a session-by-session teaching guide for the instructor. It contains live demo scripts, timing notes, and internal references. Students should use the main lecture document instead.
This document is the session-by-session plan for the 6 hours of interactive lectures. Each session is 1.5h. All demonstrations are done live in a terminal, students follow along.
The 6 hours of practicals (TPs) are described separately.
Context: large lecture hall (60+ students), most without laptops. Active learning is driven by Wooclap quizzes on smartphones. Each Wooclap block takes 2–3 min: launch the question, wait for answers, reveal results, debrief. Aim for 2–3 questions per session.
Setup for the Instructor
Required Tools
| Tool | Purpose |
|---|---|
gcc / clang | C compilation |
make | Build automation |
ar, nm, ldd/otool | Library inspection |
git | Version control |
clang-format | Code formatting |
clang-tidy | Static analysis |
gdb / lldb | Debugger |
valgrind or -fsanitize=address | Memory checking |
Wooclap Preparation
Create a Wooclap event before the course and pre-load all questions below so you can launch them instantly without typing during the session. Suggested event name: “Compilation Toolchain — [date]”.
| # | Session | Question type | Topic |
|---|---|---|---|
| Q1 | 1.2 | Poll | Preprocessor role |
| Q2 | 1.3 | MCQ | Symbol types in nm output |
| Q3 | 1.4 | MCQ | Linker vs preprocessor error |
| Q4 | 2.3 | Matching | Automatic variables |
| Q5 | 2.4 | Multiple select | What gets rebuilt? |
| Q6 | 3.2 | Poll | Static library after deletion |
| Q7 | 3.3 | Poll | Static vs dynamic binary size |
| Q8 | 4.3 | MCQ | Correct conflict resolution |
| Q9 | 4.5 | MCQ | NASA rule violated by uninitialized variable |
| Q10 | 4.7 | Number | Number of issues clang-tidy finds in ugly.c |
Terminal Setup
- Use a terminal with a large font (24pt+) — e.g.
Alacritty,kitty, or any terminal withCtrl++zoom - Split panes with
tmuxto show code + terminal side by side:
tmux new-session -s lecture
# Ctrl-b % → vertical split (editor left, shell right)
# Ctrl-b " → horizontal split if needed
- Use a minimal Vim/Neovim or
nanofor editing (students should not be distracted by IDE features) - Prepare a
~/lecture/workspace with starter files for each session (see below)
File Preparation
Create the following directory structure before the course:
~/lecture/
├── session1/ # Compilation basics
│ ├── hello.c
│ ├── util.h
│ ├── util.c
│ ├── main.c
│ ├── a.h / b.h # multiple inclusion demo
│ └── c.h / d.h # cyclic inclusion demo
├── session2/ # Makefile
│ ├── src/
│ │ ├── main.c
│ │ ├── inout.c
│ │ └── math_utils.c
│ ├── include/
│ │ ├── inout.h
│ │ └── math_utils.h
│ └── Makefile.v0 # empty, will be built live
├── session3/ # Libraries + Git basics
│ ├── lib/
│ │ ├── mylib.h
│ │ └── mylib.c
│ ├── app/
│ │ └── main.c
│ └── Makefile
└── session4/ # Git advanced + Code quality
├── ugly.c # poorly formatted code
├── buggy.c # memory errors for sanitizer demo
├── .clang-format
└── .clang-tidy
Session 1 — Compilation Basics (1.5h)
Learning Objectives
- Understand the full compilation pipeline (preprocessing → AST → IR → assembly → binary → linking)
- Be able to run each step individually with
gcc/clang - Diagnose common errors (missing includes, undefined symbols)
1.1 — Motivation (10 min)
Format: Discussion with students
Show the “Program vs. Software” table from the existing slides. Ask the class:
“Who has worked on a project with more than 3 files?” “How did you compile it? Did you type
gcccommands by hand?”
Introduce the 4 questions: simultaneous work, versioning, debugging, automation.
1.2 — Preprocessing (15 min)
Format: Live terminal demo
Wooclap Q1 — Poll (2 min)
Launch before showing any terminal output.
What does the C preprocessor do with a
#include "util.h"directive?
- A. It checks that the file exists and records the dependency
- B. It physically copies the content of
util.hinto the source file - C. It compiles
util.hseparately and links the result - D. It does nothing —
#includeis handled by the linker
Expected: B. Debrief: show gcc -E main.c | less and point out how the header content appears verbatim. The size jump from wc -l reinforces this concretely.
# Start with a simple example
cat util.h
cat main.c
# Show what the preprocessor does
gcc -E main.c | less
# Point out: #include is literally copy-paste, #define is text replacement
1.3 — From Source to Binary (30 min)
Format: Step-by-step live demo
Walk through each stage on the same main.c + util.c example:
# 1. Preprocessing
gcc -E main.c -o main.i
wc -l main.c main.i # show how much the preprocessor adds
# 2. AST (optional — show briefly, don't dwell)
clang -Xclang -ast-dump -fsyntax-only main.c 2>/dev/null | head -30
# 3. Intermediate representation
clang -S -emit-llvm main.c -o main.ll
cat main.ll # highlight: ideal machine, no register limits
# 4. Assembly
gcc -S main.c -o main.s
cat main.s # highlight: real registers (rax, rdi, rsi)
# 5. Object file
gcc -c main.c -o main.o
hexdump -C main.o | head -20 # it's just bytes now
nm main.o # show symbols: T = defined, U = undefined
# 6. Linking
gcc -c util.c -o util.o
nm util.o # compare: _compare is T here, U in main.o
gcc main.o util.o -o program
nm program # all symbols resolved
./program ; echo $? # run it!
Wooclap Q2 — MCQ (2 min)
Launch after showing nm main.o, before running nm util.o.
In the output of
nm main.o, the symbolcompareappears with the letterU. What does this mean?
- A. The symbol is Undefined —
main.oneeds it but doesn’t provide it - B. The symbol is Used — it is called at least once in the file
- C. The symbol is Unlinked — it was stripped during optimisation
- D.
Uis an error code — the symbol is corrupt
Expected: A. Debrief: contrast with nm util.o where compare appears as T (Text section = defined). Linking resolves the U by finding the matching T.
Key teaching moment: After showing nm, ask:
“What does
U _comparemean inmain.o? Why does linking fix it?“
1.4 — Common Errors (15 min)
Format: Live error reproduction
Error 1 — Missing include / undeclared identifier:
# Remove #include "util.h" from main.c, try to compile
gcc -c main.c
# → "implicit declaration" or "undeclared identifier"
# Fix: add the include back
Error 2 — Undefined symbol (linker error):
# Try to build main.o alone into an executable
gcc main.o
# → "Undefined symbols: _compare"
# Fix: link with util.o
Wooclap Q3 — MCQ (3 min)
Show this error on the projector, launch before explaining it.
/usr/bin/ld: main.o: undefined reference to symbol 'compare'At which compilation stage does this error occur?
- A. Preprocessing — the
#includedirective was not resolved - B. Compilation — the compiler could not parse the source
- C. Linking — the symbol was declared but never defined in any object file
- D. Runtime — the symbol could not be found in the dynamic library
Expected: C. Debrief: contrast with a preprocessor error (implicit declaration of function) — a preprocessor/compiler error mentions a file and line number, while a linker error mentions an object file and a symbol name. This distinction is the key diagnostic skill for the course.
1.5 — Include Guards (15 min)
Format: Live demo of the problem + solution
# Show multiple inclusion error
cat a.h # defines g_variable
cat b.h # includes a.h
cat main.c # includes both a.h and b.h
gcc main.c # → redefinition error
# Show the preprocessed output to explain WHY
gcc -E main.c # g_variable defined twice!
# Fix with header guards
cat a_fixed.h # with #ifndef/#define/#endif
gcc main_fixed.c # compiles!
Show both styles: #ifndef guards and #pragma once.
1.6 — Quick Recap (5 min)
Draw the pipeline on the board (or whiteboard app):
.c → [preprocessor] → .i → [compiler] → .s → [assembler] → .o → [linker] → executable
gcc -E gcc -S as/gcc -c gcc
Ask: “If you see undefined reference to X, which step failed?”
Session 2 — Makefile (1.5h)
Learning Objectives
- Understand Make’s dependency graph model
- Write Makefiles from scratch, progressing from basic to generic
- Use automatic variables, inference rules, and dependency generation
2.1 — Why Make? (10 min)
Format: Live demo of the pain
cd session2/
# Compile manually
gcc -W -Wall -Wextra -c src/main.c -Iinclude
gcc -W -Wall -Wextra -c src/inout.c -Iinclude
gcc -W -Wall -Wextra -c src/math_utils.c -Iinclude
gcc main.o inout.o math_utils.o -o project -lm
# Now change one file
vim src/inout.c # small change
# Do we rebuild everything? Just inout.o? How do we know?
Reference the Quake 3 example: 328 .c files = 329+ commands minimum.
2.2 — First Makefile (20 min)
Format: Build a Makefile from scratch, live
vim Makefile
Build it step by step, explaining each concept:
Step 1 — Hardcoded rules:
project: main.o inout.o math_utils.o
gcc main.o inout.o math_utils.o -o project -lm
main.o: src/main.c include/inout.h include/math_utils.h
gcc -W -Wall -Wextra -Iinclude -c src/main.c
inout.o: src/inout.c include/inout.h
gcc -W -Wall -Wextra -Iinclude -c src/inout.c
math_utils.o: src/math_utils.c include/math_utils.h
gcc -W -Wall -Wextra -Iinclude -c src/math_utils.c
clean:
rm -f *.o project
Run it, show incremental builds:
make
# touch one file, rebuild
touch src/inout.c
make # only inout.o and project are rebuilt!
make clean
Key teaching moment: explain the dependency graph. Draw it on the board.
2.3 — Variables and Automatic Variables (15 min)
Format: Refactor the Makefile live
Step 2 — Add variables:
TARGET = project
CC = gcc
CFLAGS = -W -Wall -Wextra -Iinclude
LDFLAGS = -lm
Step 3 — Use automatic variables:
main.o: src/main.c include/inout.h include/math_utils.h
$(CC) $(CFLAGS) -c $<
Wooclap Q4 — Matching (3 min)
Launch after writing the rule with automatic variables on screen.
Match each automatic variable to its meaning in a Makefile rule.
| Variable | Meaning |
|---|---|
$@ | ? |
$< | ? |
$^ | ? |
Choices: The target name / The first prerequisite / All prerequisites (space-separated)
Expected: $@ → target, $< → first prerequisite, $^ → all prerequisites. Debrief: rewrite one of the rules live with and without automatic variables to show the equivalence. Common mistake: students confuse $< and $^ — the mnemonic “less-than = first/smallest” can help.
Show the variable reference table: $@, $<, $^, $*.
2.4 — Inference Rules (15 min)
Format: Continue refactoring live
Step 4 — Pattern rules:
%.o: src/%.c
$(CC) $(CFLAGS) -c $<
Show: this replaces all three .o rules!
Problem: we lost header dependencies. Before demonstrating, launch:
Wooclap Q5 — Multiple select (2 min)
Show the pattern-rule Makefile on the projector. Launch before running touch.
We run
touch include/inout.hthenmake. Which files will be rebuilt?
main.oinout.omath_utils.oproject(the final executable)- Nothing — Make does not track headers
Expected: Nothing — with pattern rules and no -MMD, Make has no way to know that inout.o depends on inout.h. Debrief: this is the exact bug -MMD and -include $(DEPS) fixes. The contrast with the correct behaviour (section 2.5) will make the fix memorable.
# Change something in inout.h
touch include/inout.h
make # nothing rebuilds! Bad.
2.5 — Automatic Dependencies (15 min)
Format: Live demo
# Show what -MMD does
gcc -MMD -Iinclude -c src/main.c
cat main.d
# Output: main.o: src/main.c include/inout.h include/math_utils.h
Step 5 — Add -MMD and -include:
CFLAGS = -W -Wall -Wextra -Iinclude -MMD
DEPS = $(OBJ:.o=.d)
-include $(DEPS)
Now touch include/inout.h && make correctly rebuilds dependent files.
2.6 — Generic Makefile (10 min)
Format: Final version, built live
TARGET = project
CC = gcc
CFLAGS = -W -Wall -Wextra -Iinclude -MMD
LDFLAGS = -lm
SRC = $(wildcard src/*.c)
OBJ = $(SRC:src/%.c=%.o)
DEPS = $(OBJ:.o=.d)
default: $(TARGET)
%.o: src/%.c
$(CC) $(CFLAGS) -c $<
$(TARGET): $(OBJ)
$(CC) $(LDFLAGS) $^ -o $@
-include $(DEPS)
.PHONY: clean
clean:
rm -f *.o *.d $(TARGET)
Add a new .c file live and show it’s automatically picked up.
2.7 — Debug/Release Builds (5 min)
Quick demo of conditional variables:
DEBUG ?= yes
ifeq ($(DEBUG),yes)
CFLAGS += -g -DDEBUG
else
CFLAGS += -O3
endif
make # debug build
make DEBUG=no # release build
Session 3 — Libraries (1.5h)
Learning Objectives
- Understand static vs. dynamic libraries
- Build and use both types
- Inspect library contents and dependencies
3.1 — What Are Libraries? (10 min)
Format: Discussion + terminal demo
# Every C program uses libraries — even "Hello World"
cat > /tmp/hello.c << 'EOF'
#include <stdio.h>
int main() { printf("Hello\n"); return 0; }
EOF
gcc /tmp/hello.c -o /tmp/hello
# Show linked libraries
ldd /tmp/hello # Linux
# otool -L /tmp/hello # macOS
“Where does
printfcome from? You didn’t write it. You didn’t compile it.”
Key concept: a library is precompiled code + headers.
3.2 — Static Libraries (25 min)
Format: Build one from scratch
cd session3/
# Look at our library source
cat lib/mylib.h
cat lib/mylib.c
# Step 1: Compile to object file
gcc -W -Wall -c lib/mylib.c -o lib/mylib.o
# Step 2: Create static library
ar rcs lib/libmylib.a lib/mylib.o
# Inspect it
ar -t lib/libmylib.a # list contents
nm lib/libmylib.a # show symbols
# Step 3: Use it
gcc -W -Wall app/main.c -Ilib -Llib -lmylib -o app/program
./app/program
Key teaching moments:
ar= archiver (bundles.ofiles)-L= where to look,-l= what to link (striplibprefix and.asuffix)- The library is embedded in the executable
Wooclap Q6 — Poll (2 min)
Launch before running rm.
We delete
libmylib.aafter linkingapp/programstatically. What happens when we run./app/program?
- A. It crashes immediately — the library is missing
- B. It runs fine — the library code was copied into the executable at link time
- C. It runs fine — the OS finds the library automatically in
/usr/lib - D. It refuses to start — the dynamic linker reports a missing dependency
Expected: B. Debrief: the whole point of a static library — independence from the environment at runtime. The executable is self-contained.
# Prove it: delete the library, program still works
rm lib/libmylib.a
./app/program # still runs!
3.3 — Dynamic Libraries (25 min)
Format: Build one from scratch
# Step 1: Compile with Position Independent Code
gcc -W -Wall -fPIC -c lib/mylib.c -o lib/mylib.o
# Step 2: Create shared library
gcc -shared -o lib/libmylib.so lib/mylib.o # Linux
# gcc -shared -o lib/libmylib.dylib lib/mylib.o # macOS
# Step 3: Link against it
gcc -W -Wall app/main.c -Ilib -Llib -lmylib -o app/program
# Try to run...
./app/program
# Error! Library not found at runtime
# Fix: tell the loader where to find it
LD_LIBRARY_PATH=./lib ./app/program # Linux
# DYLD_LIBRARY_PATH=./lib ./app/program # macOS
Key teaching moment: difference between compile-time linking and runtime loading.
# Show the dependency
ldd app/program # libmylib.so => not found
# Now delete the library
rm lib/libmylib.so
./app/program # fails! Unlike static.
3.4 — Static vs. Dynamic Comparison (10 min)
Format: Side-by-side comparison in terminal
# Build both versions
gcc app/main.c -Ilib -Llib -lmylib_static -o app/static_prog # static
gcc app/main.c -Ilib -Llib -lmylib -o app/dynamic_prog # dynamic
# Compare sizes
ls -la app/static_prog app/dynamic_prog
# Compare dependencies
ldd app/static_prog
ldd app/dynamic_prog
Wooclap Q7 — Poll (2 min)
Launch before running ls -la.
We build two versions of the same program: one linked statically against
libmylib.a, one dynamically againstlibmylib.so. Which executable file is larger on disk?
- A. The statically linked binary is larger
- B. The dynamically linked binary is larger
- C. They are the same size
Expected: A. Debrief: show the actual ls -l output. The static binary embeds the compiled library code; the dynamic binary only stores a reference (the soname). Relate to the RAM sharing benefit of dynamic libraries: 100 processes linking the same .so share one copy in memory.
Recap the tradeoffs (embedded in existing slides):
- Static: no dependencies, larger binary, needs rebuild if lib changes
- Dynamic: smaller binary, shared in RAM, but runtime dependency
3.5 — Library Versioning (10 min)
Format: Discussion with terminal examples
# Real-world example
ls -la /usr/lib/x86_64-linux-gnu/libz*
# libz.a
# libz.so -> libz.so.1.2.11
# libz.so.1 -> libz.so.1.2.11
# libz.so.1.2.11
# Symlink chain: libz.so → libz.so.1 → libz.so.1.2.11
# At compile time: linker follows libz.so
# At runtime: loader follows libz.so.1 (soname)
Explain M.m.p versioning: major (breaking), minor (compatible), patch (bugfix).
3.6 — Integrating Libraries in Makefile (10 min)
Format: Update the session 2 Makefile
# Add library build targets
lib/libmylib.a: lib/mylib.o
ar rcs $@ $^
lib/mylib.o: lib/mylib.c lib/mylib.h
$(CC) $(CFLAGS) -c $< -o $@
$(TARGET): $(OBJ) lib/libmylib.a
$(CC) $(LDFLAGS) $(OBJ) -Llib -lmylib -o $@
Session 4 — Git Advanced + Code Quality (1.5h)
Learning Objectives
- Work with branches, resolve merge conflicts
- Understand the pull/merge request workflow
- Use
clang-format,clang-tidyand sanitizers to improve code quality
4.1 — Git Quick Recap (5 min)
Format: Live demo
# Assume students know: init, add, commit, push, pull
git init demo && cd demo
echo "int main() { return 0; }" > main.c
git add main.c && git commit -m "Initial commit"
git log --oneline
4.2 — Branches (20 min)
Format: Live terminal demo
# Create and switch to a feature branch
git checkout -b feature/add-utils
# Make changes
cat > utils.c << 'EOF'
int add(int a, int b) { return a + b; }
EOF
git add utils.c && git commit -m "Add utils module"
# Show the branch graph
git log --oneline --graph --all
# Switch back to main
git checkout master
ls # utils.c is gone!
# Make a change on master too
echo "// updated" >> main.c
git add main.c && git commit -m "Update main"
# Show divergence
git log --oneline --graph --all
Key teaching moment: branches are just pointers to commits. Draw the DAG on the board.
4.3 — Merging and Conflict Resolution (25 min)
Format: Deliberately create a conflict, resolve it live
# Setup: both branches modify the same line in main.c
git checkout master
echo 'int main() { return 0; }' > main.c
git add main.c && git commit -m "Reset main"
git checkout -b feature/return-one
sed -i 's/return 0/return 1/' main.c
git add main.c && git commit -m "Return 1"
git checkout master
sed -i 's/return 0/return 42/' main.c
git add main.c && git commit -m "Return 42"
# Now merge — conflict!
git merge feature/return-one
# CONFLICT!
Wooclap Q8 — MCQ (3 min)
Show the conflicted file on the projector. Launch before editing.
<<<<<<< HEAD
int main() { return 42; }
=======
int main() { return 1; }
>>>>>>> feature/return-oneYou want to keep the value from the feature branch. What should the resolved file contain?
- A.
int main() { return 42; }← keep HEAD - B.
int main() { return 1; }← keep the feature branch - C.
int main() { return 42; return 1; }← keep both lines - D. The conflict markers must stay — Git resolves them automatically on commit
Expected: B. Debrief: emphasise that all conflict markers (<<<<, ====, >>>>) must be removed and the file must compile. Option D is a very common misconception. Option C is a fun wrong answer to discuss.
Walk through resolution step by step:
# 1. See the conflict markers
cat main.c
# <<<<<<< HEAD
# int main() { return 42; }
# =======
# int main() { return 1; }
# >>>>>>> feature/return-one
# 2. Edit to resolve (pick one, combine, or write something new)
vim main.c
# 3. Mark as resolved
git add main.c
git commit # default merge commit message
# 4. Verify
git log --oneline --graph --all
Interactive exercise (5 min): Give students a conflict scenario on paper. Ask them to write the resolved file.
4.4 — Pull / Merge Requests (15 min)
Format: GitLab UI demo (screen share or screenshots)
Show the workflow:
-
Create a branch locally, push it:
git checkout -b feature/new-feature # make changes, commit git push -u origin feature/new-feature -
Open a Merge Request in GitLab:
- Title, description, assignee, reviewer
- Show the diff view
- Show inline comments / code review
-
CI pipeline runs on the MR
-
Reviewer approves → merge → branch deleted
Key teaching moment: MRs are about code review and quality gates, not just merging.
Show a real example from a project if possible.
4.5 — Why Code Quality Matters: NASA’s “Power of 10” (5 min)
Format: Brief discussion before diving into tools
“Before we look at the tools, let’s talk about why code quality matters in your field.”
Introduce NASA JPL’s “Power of 10” rules (Gerard Holzmann, 2006) — 10 rules for writing reliable C in safety-critical embedded systems (Mars rovers, flight software).
Show 4 rules on the board (the most accessible for students):
| # | Rule | Why it matters |
|---|---|---|
| 1 | No goto, setjmp, or longjmp | Keeps control flow predictable and analyzable |
| 2 | All loops must have a fixed upper bound | Prevents infinite hangs — critical on hardware with no OS to kill your process |
| 6 | Declare variables at the smallest possible scope | Reduces bugs from stale or reused state |
| 7 | Check the return value of all non-void functions | A failed malloc or fopen that goes unchecked can crash a satellite |
“These aren’t academic guidelines — they’re enforced on code that runs 225 million km from the nearest debugger.”
Wooclap Q9 — MCQ (2 min)
Show this code snippet on the projector.
int x;
printf("Uninitialized: %d\n", x);Which NASA “Power of 10” rule does this code violate?
- A. Rule 1 — no
gotoorsetjmp(control flow) - B. Rule 2 — all loops must have a fixed upper bound
- C. Rule 6 — declare variables at the smallest possible scope and initialise them
- D. Rule 7 — check the return value of all non-void functions
Expected: C. Debrief: using an uninitialised variable is precisely the kind of latent bug that causes non-deterministic failures — exactly what Rule 6 (scope + initialisation discipline) prevents. clang-tidy reports this as cert-dcl03-c / uninitialized read.
Bridge to the tools: explain that clang-tidy and sanitizers can enforce many of these rules automatically. That’s what we’ll set up next.
4.6 — Code Formatting with clang-format (10 min)
Format: Live demo
cd session4/
# Show ugly code
cat ugly.c
# Format it
clang-format ugly.c # prints to stdout
clang-format -i ugly.c # in-place
# Show the config file
cat .clang-format
# Create one interactively
clang-format -style=llvm -dump-config > .clang-format
Show how to integrate with editors (brief mention) and how to add to Makefile:
format:
clang-format -i src/*.c include/*.h
4.7 — Static Analysis with clang-tidy (10 min)
Format: Live demo
# Run on a file with issues
clang-tidy --quiet -checks='*' ugly.c --
# Show specific check categories:
# - bugprone-*: likely bugs
# - cert-*: CERT secure coding
# - readability-*: code clarity
# - modernize-*: modern C practices
# Use a config file
cat .clang-tidy
# Fix automatically
clang-tidy --fix ugly.c --
Wooclap Q10 — Number (2 min)
Show ugly.c on the projector (full file). Launch before running clang-tidy.
How many warnings do you think
clang-tidywill report onugly.c?
(Students enter a number — the closest guess wins.)
Expected: varies (can be 20–100+ depending on checks enabled). Debrief: reveal the actual count, then contrast with gcc -Wall (typically 4–6 warnings). The point is not the exact number but the order-of-magnitude difference — clang-tidy catches whole categories of issues that the compiler ignores by design.
Show the comparison from the slides: gcc = 0 warnings, gcc -Wall = 6, clang-tidy = 88.
4.8 — Memory Checking (5 min)
Format: Quick live demo (covered more in TP)
# Compile with sanitizer
clang -W -Wall -g -fsanitize=address buggy.c -o buggy
./buggy
# → AddressSanitizer: heap-use-after-free
# Shows exact line number!
Course Summary
Time Distribution
| Session | Topic | Duration | Style |
|---|---|---|---|
| 1 | Compilation pipeline | 1.5h | 80% live terminal, 20% discussion |
| 2 | Makefile from scratch | 1.5h | 90% live coding, 10% explanation |
| 3 | Static & dynamic libraries | 1.5h | 70% live terminal, 30% discussion |
| 4 | Git advanced + code quality | 1.5h | 60% live terminal, 25% UI demo, 15% discussion |
Mapping to Practicals (6h)
| TP | Topic | Builds on Session |
|---|---|---|
| TP1 (2h) | Makefile: write from scratch for a multi-file project, use variables, inference rules, automatic dependencies | Sessions 1–2 |
| TP2 (2h) | Libraries: build static + dynamic libs, multi-file archives, objdump inspection, library versioning with soname | Session 3 |
| TP3 (2h) | Advanced Git: branching, conflict resolution, stash, rebase, MR workflow; Code quality: clang-format, clang-tidy, AddressSanitizer, Valgrind | Session 4 |
Key Pedagogical Principles
- Terminal first: Every concept is demonstrated live, not on slides
- Error-driven: Show errors first, then explain why they happen, then fix them
- Incremental complexity: Start simple, refactor toward the “right” solution
- Students predict: Before running a command, ask “what do you think will happen?”
- Real-world motivation: Reference Quake 3, system libraries, actual CI pipelines
Appendix — Demo Files
All files referenced in the sessions above. Copy them into the ~/lecture/ directory structure before the course.
Session 1 — Compilation Basics
session1/hello.c
#include <stdio.h>
int main(void)
{
printf("Hello, toolchain!\n");
return 0;
}
session1/util.h
#ifndef UTIL_H
#define UTIL_H
int compare(int a, int b);
#endif
session1/util.c
#include "util.h"
int compare(int a, int b)
{
if (a == b) return 0;
if (a < b) return 1;
return -1;
}
session1/main.c
#include "util.h"
#define N 20
#define M 35
int main(void)
{
int ret_value = compare(N, M);
return ret_value;
}
session1/a.h — Multiple inclusion demo (broken)
/* a.h — NO header guard on purpose */
const int g_variable = 50;
session1/b.h — Multiple inclusion demo (broken)
/* b.h — includes a.h */
#include "a.h"
int func(void)
{
return g_variable;
}
session1/main_broken.c — Triggers redefinition error
#include "a.h"
#include "b.h"
int main(void)
{
int var = g_variable;
return var + func();
}
session1/a_fixed.h — With header guard
#ifndef A_H
#define A_H
const int g_variable = 50;
#endif
session1/b_fixed.h — With header guard
#ifndef B_H
#define B_H
#include "a_fixed.h"
int func(void)
{
return g_variable;
}
#endif
session1/main_fixed.c — Compiles correctly
#include "a_fixed.h"
#include "b_fixed.h"
int main(void)
{
int var = g_variable;
return var + func();
}
session1/c.h — Cyclic inclusion demo (broken)
/* c.h — includes d.h, creating a cycle */
#include "d.h"
int g_var = 10;
int temp_c(void)
{
return temp_d() + g_var;
}
session1/d.h — Cyclic inclusion demo (broken)
/* d.h — includes c.h, creating a cycle */
#include "c.h"
int temp_d(void)
{
return temp_c() + g_var;
}
Session 2 — Makefile
session2/include/inout.h
#ifndef INOUT_H
#define INOUT_H
#define MAX_LINE 256
/* Read an array of integers from a text file (one per line).
Returns the number of values read, or -1 on error. */
int read_array(const char *filename, int *array, int max_size);
/* Write an array of integers to a text file (one per line).
Returns 0 on success, -1 on error. */
int write_array(const char *filename, const int *array, int size);
/* Print an array to stdout. */
void print_array(const int *array, int size);
#endif
session2/include/math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
/* Sort an integer array in-place (selection sort). */
void sort_array(int *array, int size);
/* Return the minimum value in the array. */
int array_min(const int *array, int size);
/* Return the maximum value in the array. */
int array_max(const int *array, int size);
/* Return the sum of all elements. */
long array_sum(const int *array, int size);
#endif
session2/src/inout.c
#include <stdio.h>
#include <stdlib.h>
#include "inout.h"
int read_array(const char *filename, int *array, int max_size)
{
FILE *fp = fopen(filename, "r");
if (fp == NULL)
return -1;
int count = 0;
char line[MAX_LINE];
while (count < max_size && fgets(line, sizeof(line), fp) != NULL)
{
array[count] = atoi(line);
count++;
}
fclose(fp);
return count;
}
int write_array(const char *filename, const int *array, int size)
{
FILE *fp = fopen(filename, "w");
if (fp == NULL)
return -1;
for (int i = 0; i < size; i++)
fprintf(fp, "%d\n", array[i]);
fclose(fp);
return 0;
}
void print_array(const int *array, int size)
{
printf("[");
for (int i = 0; i < size; i++)
{
if (i > 0) printf(", ");
printf("%d", array[i]);
}
printf("]\n");
}
session2/src/math_utils.c
#include "math_utils.h"
#include <limits.h>
void sort_array(int *array, int size)
{
for (int i = 0; i < size - 1; i++)
{
int min_idx = i;
for (int j = i + 1; j < size; j++)
{
if (array[j] < array[min_idx])
min_idx = j;
}
if (min_idx != i)
{
int tmp = array[i];
array[i] = array[min_idx];
array[min_idx] = tmp;
}
}
}
int array_min(const int *array, int size)
{
int min = INT_MAX;
for (int i = 0; i < size; i++)
{
if (array[i] < min)
min = array[i];
}
return min;
}
int array_max(const int *array, int size)
{
int max = INT_MIN;
for (int i = 0; i < size; i++)
{
if (array[i] > max)
max = array[i];
}
return max;
}
long array_sum(const int *array, int size)
{
long sum = 0;
for (int i = 0; i < size; i++)
sum += array[i];
return sum;
}
session2/src/main.c
#include <stdio.h>
#include "inout.h"
#include "math_utils.h"
#define MAX_VALUES 100
int main(void)
{
int values[MAX_VALUES];
int n = read_array("data.txt", values, MAX_VALUES);
if (n < 0)
{
fprintf(stderr, "Error: could not read data.txt\n");
return 1;
}
printf("Read %d values: ", n);
print_array(values, n);
printf("Min = %d\n", array_min(values, n));
printf("Max = %d\n", array_max(values, n));
printf("Sum = %ld\n", array_sum(values, n));
sort_array(values, n);
printf("Sorted: ");
print_array(values, n);
write_array("sorted.txt", values, n);
printf("Written to sorted.txt\n");
return 0;
}
session2/data.txt
42
17
8
99
23
56
3
71
session2/Makefile.v0 — Empty starting point
# Makefile for session 2 — build this from scratch during the lecture
#
# Targets to implement:
# default — build the project executable
# clean — remove build artifacts
#
# Source files:
# src/main.c (uses inout.h, math_utils.h)
# src/inout.c (uses inout.h)
# src/math_utils.c (uses math_utils.h)
Session 3 — Libraries
session3/lib/mylib.h
#ifndef MYLIB_H
#define MYLIB_H
/* A small vector math library for demonstration purposes. */
typedef struct
{
double x;
double y;
} Vec2;
/* Create a vector. */
Vec2 vec2_create(double x, double y);
/* Add two vectors. */
Vec2 vec2_add(Vec2 a, Vec2 b);
/* Subtract b from a. */
Vec2 vec2_sub(Vec2 a, Vec2 b);
/* Dot product of two vectors. */
double vec2_dot(Vec2 a, Vec2 b);
/* Length (magnitude) of a vector. */
double vec2_length(Vec2 v);
/* Normalize a vector (return unit vector). */
Vec2 vec2_normalize(Vec2 v);
/* Print a vector to stdout. */
void vec2_print(Vec2 v);
#endif
session3/lib/mylib.c
#include <stdio.h>
#include <math.h>
#include "mylib.h"
Vec2 vec2_create(double x, double y)
{
Vec2 v = {x, y};
return v;
}
Vec2 vec2_add(Vec2 a, Vec2 b)
{
return vec2_create(a.x + b.x, a.y + b.y);
}
Vec2 vec2_sub(Vec2 a, Vec2 b)
{
return vec2_create(a.x - b.x, a.y - b.y);
}
double vec2_dot(Vec2 a, Vec2 b)
{
return a.x * b.x + a.y * b.y;
}
double vec2_length(Vec2 v)
{
return sqrt(v.x * v.x + v.y * v.y);
}
Vec2 vec2_normalize(Vec2 v)
{
double len = vec2_length(v);
if (len == 0.0)
return vec2_create(0.0, 0.0);
return vec2_create(v.x / len, v.y / len);
}
void vec2_print(Vec2 v)
{
printf("(%.2f, %.2f)\n", v.x, v.y);
}
session3/app/main.c
#include <stdio.h>
#include "mylib.h"
int main(void)
{
Vec2 a = vec2_create(3.0, 4.0);
Vec2 b = vec2_create(1.0, 2.0);
printf("a = ");
vec2_print(a);
printf("b = ");
vec2_print(b);
Vec2 sum = vec2_add(a, b);
printf("a + b = ");
vec2_print(sum);
printf("a . b = %.2f\n", vec2_dot(a, b));
printf("|a| = %.2f\n", vec2_length(a));
Vec2 n = vec2_normalize(a);
printf("norm(a) = ");
vec2_print(n);
printf("|norm(a)| = %.2f\n", vec2_length(n));
return 0;
}
session3/Makefile
# Makefile for session 3 — library demo
# Students will extend this during the lecture
CC = gcc
CFLAGS = -W -Wall -Wextra -Ilib
LDFLAGS = -lm
TARGET = app/program
default: $(TARGET)
$(TARGET): app/main.c
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
.PHONY: clean
clean:
rm -f $(TARGET) lib/*.o lib/*.a lib/*.so lib/*.dylib app/*.o
Session 4 — Git Advanced + Code Quality
session4/ugly.c
A deliberately poorly formatted file with multiple issues for clang-format and clang-tidy to catch.
#include <stdio.h>
#include<stdlib.h>
#include <string.h>
#define TAILLE 100
// compute the average
double moyenne(int* tab,int n){
double s=0;int i;
for(i=0;i<n;i++){
s=s+tab[i];
}
return s/n;
}
//find max
int maximum(int* t , int n)
{
int max=t[0];
for(int i=1;i<n;i++){
if(t[i]>max)max=t[i];
}
return max;
}
// identical to maximum but for minimum — violates DRY
int minimum(int* t,int n)
{
int min=t[0];
for(int i=1;i<n;i++){
if(t[i]<min)min=t[i];
}
return min;
}
char* concat(char* a,char* b){
char* result=malloc(strlen(a)+strlen(b)+1);
strcpy(result,a);
strcat(result,b);
return result;
}
void print_tab(int tab[],int n)
{
int i;
printf("[");
for(i=0;i<n;i++){
if(i>0)printf(", ");
printf("%d",tab[i]);}
printf("]\n");
}
int main()
{
int tab[TAILLE];
int i;
for(i=0;i<10;i++)tab[i]=rand()%100;
printf("Array: ");
print_tab(tab,10);
printf("Average: %.2f\n",moyenne(tab,10));
printf("Max: %d\n",maximum(tab,10));
printf("Min: %d\n",minimum(tab,10));
char* msg=concat("Hello ","World!");
printf("%s\n",msg);
// memory leak: msg is never freed
int x;
printf("Uninitialized: %d\n",x);
return 0;
}
Issues for students to find:
- Inconsistent spacing, braces, indentation everywhere
#include<stdlib.h>missing spacemain()should bemain(void)in Cconcat()has a memory leak (never freed)xis used uninitializedmin/maxfunctions are near-duplicates (DRY violation)- Missing
conston read-only pointer parameters
session4/buggy.c
Demonstrates memory errors caught by AddressSanitizer and Valgrind.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Bug 1: heap-use-after-free */
void use_after_free(void)
{
char *buffer = malloc(16);
strcpy(buffer, "hello");
free(buffer);
printf("After free: %s\n", buffer); /* BUG: use after free */
}
/* Bug 2: heap-buffer-overflow */
void buffer_overflow(void)
{
int *arr = malloc(5 * sizeof(int));
for (int i = 0; i <= 5; i++) /* BUG: off-by-one, writes arr[5] */
arr[i] = i * 10;
printf("arr[4] = %d\n", arr[4]);
free(arr);
}
/* Bug 3: memory leak */
void leak(void)
{
char *data = malloc(1024);
strcpy(data, "this is leaked");
printf("Leaked data: %s\n", data);
/* BUG: no free(data) */
}
/* Bug 4: double free */
void double_free(void)
{
int *p = malloc(sizeof(int));
*p = 42;
free(p);
/* free(p); — uncomment to demo double free (crashes immediately) */
}
/* Bug 5: stack-buffer-overflow */
void stack_overflow(void)
{
int arr[10];
for (int i = 0; i <= 10; i++) /* BUG: writes arr[10] */
arr[i] = i;
printf("arr[9] = %d\n", arr[9]);
}
int main(void)
{
printf("=== Bug 1: use-after-free ===\n");
use_after_free();
/* Uncomment one at a time — ASan stops at the first error */
/*
printf("=== Bug 2: heap-buffer-overflow ===\n");
buffer_overflow();
printf("=== Bug 3: memory leak ===\n");
leak();
printf("=== Bug 4: double free ===\n");
double_free();
printf("=== Bug 5: stack-buffer-overflow ===\n");
stack_overflow();
*/
return 0;
}
Usage during lecture:
# Each bug can be tested individually by uncommenting in main()
clang -W -Wall -g -fsanitize=address buggy.c -o buggy
./buggy
# ASan reports the exact line + stack trace
# For leak detection specifically:
clang -W -Wall -g -fsanitize=address -fsanitize=leak buggy.c -o buggy
./buggy
session4/.clang-format
---
Language: C
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 100
BreakBeforeBraces: Allman
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: true
PointerAlignment: Right
SpaceBeforeParens: ControlStatements
...
session4/.clang-tidy
---
Checks: >
-*,
bugprone-*,
cert-*,
clang-analyzer-*,
misc-*,
performance-*,
readability-braces-around-statements,
readability-identifier-naming,
readability-implicit-bool-conversion,
readability-misleading-indentation
WarningsAsErrors: ''
HeaderFilterRegex: '.*'
CheckOptions:
- key: readability-identifier-naming.FunctionCase
value: lower_case
- key: readability-identifier-naming.VariableCase
value: lower_case
- key: readability-identifier-naming.MacroDefinitionCase
value: UPPER_CASE
...
Setup Script
You can use this script to create the full directory structure on your machine:
#!/usr/bin/env bash
set -euo pipefail
BASE=~/lecture
echo "Creating lecture demo files in $BASE..."
mkdir -p "$BASE"/{session1,session2/{src,include},session3/{lib,app},session4}
echo "Done. Now copy each file from the appendix into the corresponding location."
echo "Structure:"
find "$BASE" -type d | sort