Software build tools
Between the text file and the running program there are four stages, and each one throws its own errors. Knowing which one failed saves half of the debugging time.
01The four stages
Typing gcc main.c -o program looks like a single step, but it is four programs
chained together. Knowing which one failed is half of debugging:
| Stage | Input | Output | Typical error |
|---|---|---|---|
| Preprocessor | .c | .i | Cannot find a header |
| Compiler | .i | .s (assembly) | Syntax or type error |
| Assembler | .s | .o (object) | Rarely fails |
| Linker | .o and libraries | Executable | Undefined reference |
A compile error mentions a file and line: the compiler is reading your code. A
link error says undefined reference to 'something' and points to no line: the code compiled
fine, but the body of a function is missing. It is almost always a library that was not added —the classic
-lm for the math functions— or a file that was left out of the build.
Walk through the four stages and see what the file turns into, along with the command that runs each one.
02Separate compilation and libraries
Each .c is compiled separately into its own .o, and the linker joins them. The
advantage is concrete: if you touch a single file, only that one is recompiled.
gcc -c main.c → main.o
gcc -c sensor.c → sensor.o
gcc main.o sensor.o -o program -lm
| Library type | File | How it is used |
|---|---|---|
| Static | .a on Linux, .lib on Windows | Copied into the executable at link time |
| Dynamic | .so on Linux, .dll on Windows | Loaded at run time; shared between programs |
A static library gives a self-contained, larger executable; a dynamic one, a small executable that depends on the library being installed. For an embedded device the static one is almost always the better choice: fewer surprises in the field.
03make
Once there are more than three files, recompiling by hand is impractical. make reads a file of rules and rebuilds only what is out of date, by comparing modification times:
CC = gcc
CFLAGS = -Wall -Wextra -O2
program: main.o sensor.o
$(CC) main.o sensor.o -o program -lm
main.o: main.c sensor.h
$(CC) $(CFLAGS) -c main.c
clean:
rm -f *.o program
Each rule has a target, its dependencies and the commands to build it,
which are indented with a real tab character. If sensor.h changes, make knows that
main.o is stale and rebuilds it; if nothing changed, it does nothing.
-Wall -Wextra turn on warnings, and you have to read them: most run-time errors
were first an ignored warning. -g adds information for
the debugger, -O2 optimizes, and -std=c11 fixes the language version so that
the program behaves the same on another machine.
04Version control
A version control system keeps the history of the project: what changed, when and why. With git, the basic cycle is short:
| Command | What it does |
|---|---|
git init | Creates the repository |
git status | What changed since the last commit |
git add file | Marks what will go into the next commit |
git commit -m "…" | Saves a version with its explanation |
git log | The history |
git diff | The exact differences, line by line |
git checkout -b branch | Opens a parallel line of work |
Three rules of thumb that matter more than the list of commands: small commits that do one thing only,
messages that explain the why and not the what —the diff already says what changed—, and a
.gitignore that leaves out whatever is generated: the .o files, the executable and temporary
files are not versioned.
05Debugging
- printf. Rudimentary but effective, especially in embedded systems where there is no debugger.
- gdb. Compiling with
-g:breakto set a breakpoint,run,nextandstepto advance,printto see a variable andbacktraceto find out how execution got there. With a program that crashed,backtraceon the core dump often solves the case in a minute. - valgrind. Detects out-of-bounds accesses, use of uninitialized memory and memory leaks. A program that seems to run fine can be full of errors that valgrind reveals.
- Sanitizers.
-fsanitize=addressinstruments the program so that it aborts at the exact moment of the invalid access, with the line pointed out.
A program that fails “sometimes” almost always has misused memory: an overflowed array or a dangling pointer. The symptom appears far from the cause, which is why analysis tools are worth more than hours of reading the code.
06In the lab
With a ten-line program, run gcc -E, -S and -c and
look at the intermediate files. Count how many lines the .i has after including
stdio.h.
Set up a three-file project with its Makefile, with targets all,
clean and debug. Check that when you touch a single .c only
that one is recompiled.
Deliberately produce one error from each stage: a nonexistent #include, a missing
semicolon and a function that is declared but not defined. Write down exactly what each message says and
learn to recognize them.
07Common mistakes
- Confusing a compile error with a link error.
- Forgetting
-lmwhen using math functions. - Indenting the commands of a Makefile with spaces instead of a tab.
- Ignoring the compiler’s warnings.
- Versioning generated files —objects, executables, build folders— and cluttering the history.
- Debugging a binary compiled without
-gand not even seeing variable names.
08Self-assessment
What exactly does the preprocessor do?
It manipulates text: it pastes in the included files, replaces macros and resolves conditional compilation. It does not understand C.
undefined reference to 'sqrt' appears. Which stage failed?
The linking. You need to add -lm: the prototype was in
math.h, but the body of the function is in the math library.
What are the #ifndef guards in a header for?
To keep the contents from being included twice in the same translation unit, which would cause duplicate definitions.
How does make know what to recompile?
By comparing modification times: if a dependency is newer than the target, it rebuilds that target.
What is the difference between a static and a dynamic library?
A static library is copied into the executable at link time; a dynamic one is loaded at run time and shared between programs.
What should go in a commit message?
The why of the change. The diff already shows the what.
09Further reading
- Brian W. Kernighan and Rob Pike. The Practice of Programming. Addison-Wesley, 1999. Debugging, testing and tools, with the judgment of two authors who worked on Unix.
- Scott Chacon and Ben Straub. Pro Git. 2nd ed., Apress, 2014. Available online and in Spanish. The git book, from the basics to the team workflow.
- John Graham-Cumming. The GNU Make Book. No Starch Press, 2015. For when the Makefile grows and starts to have logic of its own.
- Randal E. Bryant and David R. O'Hallaron. Computer Systems: A Programmer's Perspective. 3rd ed., Pearson, 2015. Chapter 7 explains linking in a level of detail found nowhere else.