Catto / Topic Map · Computer Science II Level 2
Computer Science II · 120 h · Topic 3 of 8

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.

Preprocessor Compiler Linker make git

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:

StageInputOutputTypical error
Preprocessor.c.iCannot find a header
Compiler.i.s (assembly)Syntax or type error
Assembler.s.o (object)Rarely fails
Linker.o and librariesExecutableUndefined reference
How to tell a compile error from a link error

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.

Lab · what each stage does

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 typeFileHow it is used
Static.a on Linux, .lib on WindowsCopied into the executable at link time
Dynamic.so on Linux, .dll on WindowsLoaded 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.

Flags worth always using

-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:

CommandWhat it does
git initCreates the repository
git statusWhat changed since the last commit
git add fileMarks what will go into the next commit
git commit -m "…"Saves a version with its explanation
git logThe history
git diffThe exact differences, line by line
git checkout -b branchOpens 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: break to set a breakpoint, run, next and step to advance, print to see a variable and backtrace to find out how execution got there. With a program that crashed, backtrace on 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=address instruments the program so that it aborts at the exact moment of the invalid access, with the line pointed out.
The error that cannot be reproduced

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

Exercise 1 · Seeing each stage

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.

Exercise 2 · A Makefile of your own

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.

Exercise 3 · Causing and reading errors

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 -lm when 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 -g and 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.
Development of the topic “Software build tools” of Computer Science II (Level 2), based on the curriculum of the UTN Electronic Engineering program, 2023 curriculum — Ordinance No. 1849 of the UTN Higher Council. Back to the Topic Map · catto.ar