Install the toolchain

Four tools carry a C project: the compiler, a build tool that remembers what changed, a project generator for larger trees, and a debugger. This lesson installs them on Alpine Linux and builds one small program with each, so a failure at this stage is caught before any language topic depends on it.

The packages

Alpine keeps the compiler, the C library headers and the binary utilities in one meta-package, build-base. CMake and GDB are separate. On Debian and Ubuntu the same set is build-essential cmake gdb; on Windows, MSYS2 with the mingw-w64 compiler is the closest equivalent, but nothing on this page was run there.

sudo apk add --no-cache build-base cmake gdb

This container already had the packages, so the command was not run here. The checks below are the real ones: four versions and a program built with the compiler. __VERSION__ is a string the compiler fills in, which is why it prints only the version number.

#include <stdio.h> int main(void) { printf("compiled with: %s\n", __VERSION__); printf("__STDC_VERSION__: %ld\n", __STDC_VERSION__); printf("sizeof(void *): %zu\n", sizeof(void *)); return 0; }

gcc --version | head -1
make --version | head -1
cmake --version | head -1
gdb --version | head -1
gcc -Wall -Wextra versions.c -o versions
./versions

gcc (Alpine 15.2.0) 15.2.0
GNU Make 4.4.1
cmake version 4.2.3
GNU gdb (GDB) 16.3
compiled with: 15.2.0
__STDC_VERSION__: 202311
sizeof(void *): 8

The last line says the machine is 64-bit: a pointer takes eight bytes. The compiler default is C23 again, as in lesson 1, so this course never passes -std=c23 explicitly.

A first makefile

make reads a file called Makefile and rebuilds a target only when a prerequisite is newer. The example below is deliberately small; lesson 20 takes the same file apart. Put the program in hello.c and the rules in Makefile, then run make.

#include <stdio.h> int main(void) { printf("Hello from make\n"); return 0; }

vi Makefile

CC = gcc CFLAGS = -Wall -Wextra hello: hello.c $(CC) $(CFLAGS) hello.c -o hello

The indented line under the target must start with a real tab, not spaces. Make prints every command it runs, so the first build shows the compiler line; the second run has nothing to do:

make
./hello
make

gcc -Wall -Wextra hello.c -o hello
Hello from make
make: 'hello' is up to date.

A first CMake project

CMake writes the build files for another tool instead of building directly. The project declares a target, its source file, the language standard and the warning flags; CMake then generates a Makefile in a separate build directory. Keeping build files out of the source directory is the normal layout.

#include <stdio.h> int main(void) { printf("Hello from cmake\n"); return 0; }

vi CMakeLists.txt

cmake_minimum_required(VERSION 3.20) project(hello C) set(CMAKE_C_STANDARD 23) add_executable(hello main.c) target_compile_options(hello PRIVATE -Wall -Wextra)

The first command configures, the second builds, the third runs the result. The build directory is the one given to -B:

cmake -S . -B build
cmake --build build
./build/hello

-- The C compiler identification is GNU 15.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done (1.3s)
-- Generating done (0.0s)
-- Build files have been written to: /tmp/cc/L02/cmake/build
[ 50%] Building C object CMakeFiles/hello.dir/main.c.o
[100%] Linking C executable hello
[100%] Built target hello
Hello from cmake

The build directory appears in the output as the absolute path where the command ran, and the two timings change on every machine, so expect different values. What matters is Built target hello and the program output.

A first gdb session

A debugger turns a crash into a line number. Compile with -g so the binary carries source information, and with -O0 while debugging so the code keeps its shape. The program below dereferences a null pointer on purpose.

#include <stdio.h> int main(void) { int *p = NULL; printf("%d\n", *p); return 0; }

-batch runs the listed commands and exits, which is handy in a terminal without an interactive session. set disable-randomization off is needed in a container that does not allow the debugger to turn address space randomisation off.

gcc -g -O0 -Wall -Wextra crash.c -o crash
gdb -batch -ex 'set disable-randomization off' -ex run -ex bt ./crash

Program received signal SIGSEGV, Segmentation fault.
0x00005e9412617199 in main () at crash.c:6
6 printf("%d\n", *p);
#0 0x00005e9412617199 in main () at crash.c:6

The address changes on every run; the file name, the line number and the statement are stable. Lesson 61 covers breakpoints, stepping and inspecting variables.

Warnings are not noise

A warning is a place where the compiler had to guess. This function takes a parameter it never reads: the call is legal, the program runs, and the sign that something is unfinished appears only in the build. Treat the first warning as a bug report, because the next one is often a real mistake hiding behind it.

#include <stdio.h> int add(int a, int b, int scale) { return a + b; } int main(void) { printf("%d\n", add(2, 3, 10)); return 0; }

gcc -Wall -Wextra unused.c -o unused
./unused

unused.c: In function 'add':
unused.c:3:27: warning: unused parameter 'scale' [-Wunused-parameter]
3 | int add(int a, int b, int scale)
| ~~~~^~~~~
5

Either use the parameter or remove it. Silencing the warning with a cast to void is a last resort that hides the contract from the next reader.

Exercises

  1. Add a second source file to the makefile example and make the program call a function from it. Check that make rebuilds only when the changed file is newer.
  2. Change CMAKE_C_STANDARD to 17, reconfigure and confirm that the compiler receives -std=gnu17 in the generated build files.
  3. Compile the crashing program without -g and run the same gdb command. Compare how much the backtrace tells you.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Which components does the Alpine build-base package bring in?

    Answer

    Compiler, headers and binary utilities

    build-base is a meta-package with the compiler, the libc headers and tools such as the assembler and linker; CMake and GDB are separate packages.

  2. Why is -g added when a program will be debugged?

    Answer

    It keeps source file and line information in the binary

    -g stores debug information, which lets GDB name the source file and line instead of showing bare addresses.

  3. What is the role of CMake in a C project?

    Answer

    It generates build files for another tool

    CMake writes the files that Make or another generator then uses; it does not compile the sources on its own.

  4. When does make rebuild a target?

    Answer

    When a prerequisite is newer than the target

    make compares timestamps: a prerequisite newer than the target triggers the recipe, and an up-to-date target is left alone.

  5. What does sizeof(void *) print on the machine used in this course?

    Answer

    8

    The machine is 64-bit, so a pointer takes eight bytes; that is why sizeof(void *) prints 8.

  6. Which command configures a CMake project in a separate build directory?

    Answer

    cmake -S . -B build

    -S names the source directory and -B names the build directory; configuration happens before any compilation.

  7. Which command builds the project already configured in build?

    Answer

    cmake --build build

    cmake --build build invokes the generated build tool in that directory; the configure step is not repeated.

  8. A makefile recipe line is indented with spaces instead of a tab. What does make report?

    Answer

    missing separator

    Every recipe line must start with a real tab; spaces make make report missing separator and stop.

  9. A crashing binary is built without -g. What does its GDB backtrace lack?

    Answer

    Source file names and line numbers

    Without debug information GDB shows addresses and function names, but no source lines for the frames.

  10. Which warning flag reports a function parameter that is never used?

    Answer

    -Wunused-parameter

    -Wunused-parameter, enabled by -Wextra, points at a parameter the function never reads.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0, GNU Make 4.4.1, cmake 4.2.3 and gdb 16.3 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev