~/learn

Compiling C/C++

How source code becomes an executable — the preprocess, compile, assemble, and link pipeline, libraries, memory layout, and make.

This week is about the toolchain: how a .c or .cpp file becomes a running program. We trace each stage of the build by hand with gcc/g++, look at what the linker produces, and then let make automate the whole thing.

Anatomy of a program

A C/C++ source file mixes three kinds of things: preprocessor directives (#include, #define), declarations (what exists — e.g. a function prototype in a header), and definitions (the actual implementation). Declarations usually live in headers (.h) and definitions in source files (.c / .cpp); the build stitches them together.

The four stages

Compilation is a pipeline. Each stage has its own tool and its own output file:

hello.c ──▶ hello.i ──▶ hello.s ──▶ hello.o ──▶ hello
   preprocess   compile     assemble    link
     (cpp)       (cc1)        (as)       (ld)

You can stop after any stage with the right gcc flag:

gcc -E hello.c -o hello.i   # 1. preprocess: expand #include / #define / macros
gcc -S hello.i -o hello.s   # 2. compile:    C  → assembly
gcc -c hello.s -o hello.o   # 3. assemble:   assembly → object code (ELF .o)
gcc    hello.o -o hello     # 4. link:       object files + libraries → executable

In practice you run gcc hello.c -o hello and the driver performs all four steps for you.

Inspecting the artifacts

file hello.o tells you what a file is, nm hello.o lists its symbols, and objdump -d hello.o disassembles it back to assembly — handy for seeing what the compiler produced.

Linking and libraries

The linker resolves the symbols your code refers to (like printf) against libraries. There are two ways to link them:

  • Static (.a archives) — the code is copied into your executable at link time. Bigger binary, no runtime dependency. Build one with ar, link with -static.
  • Dynamic / shared (.so) — the executable only records which library it needs; the code is loaded at run time and shared between programs. Compile the objects with -fPIC.

ldd hello shows which shared libraries an executable will load.

Memory layout

When the program runs, the loader maps it into an address space with distinct segments:

high ┌────────────────────┐
     │ stack   (grows ↓)  │  local variables, call frames
     │        ...         │
     │ heap    (grows ↑)  │  malloc / new
     │ bss                │  uninitialised globals
     │ data               │  initialised globals
 low │ text               │  the machine code (read-only)
     └────────────────────┘

Automating with make

Typing the commands by hand gets old fast. A Makefile describes targets, their dependencies, and the recipe to build them — and make rebuilds only what changed:

CC = gcc
CFLAGS = -Wall -O2

all: hello

hello: hello.o
	$(CC) hello.o -o hello

hello.o: hello.c
	$(CC) $(CFLAGS) -c hello.c

clean:
	rm -f *.o hello

make builds the all target; make clean removes the build output.

Go deeper

The Compilation and build process guide walks through every stage on a real program, and the Using Makefile sample shows a multi-file build end to end.

Practice / lab

  • Run the four gcc commands above on a Hello World and inspect each artifact (.i, .s, .o) with less and file.
  • List symbols with nm hello.o and disassemble with objdump -d hello.o.
  • Write a Makefile with all and clean targets; watch make skip unchanged files.
  • Compare gcc hello.c -o hello with gcc -static hello.c -o hello and check both with ldd.

Homework

References & resources

Required

Optional / enrichment

Self-check

  1. What does the preprocessor do, and what is its output?
  2. Which stage produces the .o file, and what does it contain?
  3. How does static linking differ from dynamic linking?
  4. Which tool lists the symbols inside an object file?
  5. Why are Makefiles useful once a project has more than one source file?

On this page