Skip to content
VLSI Mentor

C++ Part I · Module 2 · Building and Running C++ Programs

Translation Units, Object Files and the Linker

Chapter 1.7 gave you the shape of a build: many source files become many object files, and a linker resolves the names between them. It deliberately stopped there, because the shape is what the rest of Part I needs and the mechanics would have been noise at that point. This chapter is the mechanics. We take the two-file project from Chapter 2.4 and stop the build after each stage, keeping the file that stage produced, so that nothing here has to be imagined — the preprocessed text, the assembly, the object file and the finished program are all things you can list and open. Along the way two facts land that are hard to believe until you see them: an object file can contain correct, finished machine code and still be impossible to run, and the thing that finally joins your two files together never sees a single line of your source.

Foundation17 min readC++Part IToolchainLinking

Part I · Module 2 · Chapter 2.6 · Translation Units, Object Files and the Linker

1. What Is New Here

Chapter 1.7 already told you the shape of a build, and it is worth saying plainly what this chapter adds so you know what to pay attention to.

From 1.7, and still true: the compiler reads one translation unit at a time and knows nothing about the others; the linker works on names rather than source; a declaration is a promise and the linker is what checks it was kept.

New here: the four stages, each one stopped at and inspected, with the file it produced. The sizes. Why an object file cannot be run. And a table saying exactly what each stage is able to check — which is the thing that makes Chapter 2.7 readable, because an error you cannot place in a stage is an error you cannot reason about.

2. The Translation Unit

Chapter 2.5 ended with the preprocessor's output: your source file with every included file pasted into it. That text has a name.

A translation unit is one source file together with everything its #include directives brought in. It is the unit the compiler actually processes.

This is a real term from the C++ standard, not jargon. It matters because it is the boundary of what the compiler knows. When Chapter 1.7 said the compiler sees one translation unit at a time, this is the thing it sees.

The project we will follow is the one built in Chapter 2.4. Here it is again, so this chapter stands on its own:

Azvya Education Pvt. Ltd.VLSI Mentor
cycles.hpp — the shared declaration
#ifndef CYCLES_HPP
#define CYCLES_HPP

int cycles_for(int transfers, int cycles_per_transfer);

#endif
Azvya Education Pvt. Ltd.VLSI Mentor
cycles.cpp — the definition
#include "cycles.hpp"

int cycles_for(int transfers, int cycles_per_transfer)
{
    const int setup_cycles = 3;
    return setup_cycles + transfers * cycles_per_transfer;
}
Azvya Education Pvt. Ltd.VLSI Mentor
main.cpp — the user
#include "cycles.hpp"
#include <iostream>

int main()
{
    const int transfers = 8;
    const int cycles_per_transfer = 2;

    std::cout << "transfers = " << transfers << '\n';
    std::cout << "cycles    = " << cycles_for(transfers, cycles_per_transfer) << '\n';

    return 0;
}

That project has two translation units, because it has two .cpp files:

  • main.cpp plus cycles.hpp plus all of <iostream> — 64,206 lines after preprocessing.
  • cycles.cpp plus cycles.hpp — 21 lines.

Note that cycles.hpp is part of both. A header is not a translation unit of its own; it has no separate existence at build time. It is text that gets pasted into the units that include it. That is why a header with a mistake in it produces errors in every file that includes it, and why Chapter 2.4's guard failure was reported at the header with notes naming the including file.

3. The Four Stages

Five artifacts produced by four stages. First, cycles.cpp, the source you wrote, 7 lines. Second, after preprocessing with the dash capital E option, cycles.i, 21 lines of expanded text. Third, after compiling with the dash capital S option, cycles.s, 22 lines of assembly for one kind of processor. Fourth, after assembling with the dash c option, cycles.o, an object file of 560 bytes containing machine code with some names still unresolved. Fifth, after linking together with the other object file, the finished program, 38,656 bytes, which the system can load and run.One translation unit, stage bystage01CYCLES.CPPthe source you wrote · 7 lines02CYCLES.I · -Epreprocessed text · 21 lines03CYCLES.S · -Sassembly for one processor · 22 lines04CYCLES.O · -Cmachine code, names unresolved · 560bytes05APP · LINKa file the system can run · 38,656bytes
Figure 1 — one translation unit, from source to a finished program, with the file each stage leaves behind. The first four boxes are files you can keep and open with the options shown. The sizes are from the smaller of this project's two units.

Each arrow is a separate program run by the driver. Chapter 2.3 called c++ a driver for exactly this reason: one command, four tools.

4. Stopping at Each Stage

Four commands, each stopping one step later. All sizes below are from the machine used to write this chapter.

Stage 1 — preprocess

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -E cycles.cpp -o cycles.i
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cycles.cpp    7 lines,   173 bytes
cycles.i     21 lines,   389 bytes

Chapter 2.5 covered this in full. Seven lines became twenty-one, because cycles.hpp is small. The same command on main.cpp produced 64,206 lines, because <iostream> is not.

Stage 2 — compile

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -S cycles.cpp -o cycles.s

The compiler proper runs here. It reads the translation unit, checks it against the rules of C++, and produces instructions for one particular kind of processor — as text.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cycles.s     22 lines,   599 bytes

Twenty-two lines of assembly from a function with two statements. You are not expected to read assembly and this curriculum will never ask you to write it. Look at it once for one reason only: there is no fixed relationship between a line of C++ and a machine instruction. One line can become many, several can collapse into one, and a line the compiler can prove has no observable effect can become none at all.

This is also the stage where your portable C++ stops being portable. cycles.s is for the processor you compiled on. Compile the same source on a different kind of machine and this file is different; the four lines you wrote are not.

Stage 3 — assemble

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -c cycles.cpp -o cycles.o

The assembler turns those instructions into the numeric form the processor actually accepts, and writes an object file.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cycles.o     560 bytes

Smaller than the assembly text it came from, because numbers are more compact than the words that name them.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ cycles.o main.o -o app
$ ./app
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
transfers = 8
cycles    = 19

Notice what is missing from that command: no -std=c++17, no warning options. Those are instructions for the compiler, and the compiler finished two stages ago. This is a different tool with a different job.

5. Why an Object File Cannot Be Run

cycles.o contains finished machine code. It still cannot be run, and the reason is the whole point of the next stage.

Try it and the system refuses outright:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./cycles.o
exec format error: ./cycles.o

It is not a program and does not have the structure of one. But the deeper reason is visible if you ask the object file what it still needs. The tool is nm, which lists the symbols in an object file:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ nm -u main.o | grep cycles
__Z10cycles_forii

-u asks for the symbols this file uses and does not define. main.o needs something called __Z10cycles_forii and does not have it.

Now ask the other file what it provides:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ nm -gU cycles.o
0000000000000000 T __Z10cycles_forii

It has exactly that. The T means the symbol is defined, in the code section.

So:

The linker's job, in one sentence: take the "needs" lists and the "provides" lists from every object file, and make them cancel out.

That odd name is your function. C++ encodes the parameter types into the symbol, and the encoding can be read back:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ nm -gU cycles.o | awk '{print $NF}' | c++filt
cycles_for(int, int)

Chapter 1.7 §5 explained why the types are in there and what it buys you. Module 24.5 covers the mechanism properly.

6. What Happens When a Promise Is Not Kept

Compile main.cpp on its own and try to make a program from it alone:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ main.o -o app
Undefined symbols for architecture arm64:
  "cycles_for(int, int)", referenced from:
      _main in main.o
ld: symbol(s) not found for architecture arm64

main.cpp compiled perfectly. It had the declaration from cycles.hpp, the call matched it, and the compiler was satisfied — correctly, because a declaration is a promise and checking the promise is somebody else's job.

The linker is that somebody. It looked for cycles_for(int, int), found no object file providing it, and stopped.

Look at the shape of that message and compare it with a compiler error:

  • No line number, and no source file. The linker never saw your source. It cannot tell you where in main.cpp the call was written.
  • It names a symbol, not a statement.
  • It says which object file referenced it — that is the level it works at.
  • ld is the linker's own name in its own message.

That difference is not a curiosity. It is the fastest classification tool you have, and Chapter 2.7 builds a method on it.

7. What Each Stage Can and Cannot Check

This table is the practical result of the whole chapter.

StageCan catchCannot catch
PreprocessingA header that does not exist; a malformed directiveAnything about C++ — it does not understand the language
CompilingEverything about this translation unit: syntax, types, whether a call matches a visible declarationAnything in another translation unit, including whether a declared function exists anywhere
AssemblingEssentially nothing of yours — it is a mechanical translation—
LinkingA name promised and never defined; a name defined twiceWhether the code is correct — it checks names, not behaviour
Running—Everything above already passed. A wrong answer here is a bug in your logic, not in the build

Two rows deserve attention.

Compiling cannot see other translation units. This is Chapter 1.7's thesis, now with a mechanism behind it. The compiler was handed one preprocessed text. Nothing else was in the room.

Linking checks names, not behaviour. A program that links successfully can still be completely wrong. The build finishing means every promise was kept, not that the promises were sensible.

8. Mistakes Worth Making Once

Compiling one file and expecting a program. c++ -c main.cpp gives you an object file, not something to run. Beginners often forget the final link step and then wonder why ./main does not exist.

Forgetting a source file at the link step. You add helpers.cpp to the project, forget to compile and link it, and get an undefined-symbol error naming a function you can see with your own eyes. The function exists; it is just not in any object file you handed the linker.

Passing compiler options to the link step and expecting them to matter. -Wall on the link command does nothing. The compiler has finished.

Reading a linker error as a compiler error. Looking for a line number that is not there, and concluding the message is broken. It is not — different tool, different information.

Believing a successful build means correct code. cycles_for could return setup_cycles - transfers * cycles_per_transfer and the build would be just as clean. The toolchain checked that everything referred to exists, not that any of it is right.

9. What You Should Be Able to Say Now

  1. A translation unit is one source file plus everything its #includes brought in. It is the unit the compiler processes.
  2. A header is not a translation unit. It is text pasted into the units that include it, which is why its errors appear in every file that includes it.
  3. Four stages: preprocess, compile, assemble, link. -E, -S, -c stop after the first three.
  4. Measured on one unit: 7 lines → 21 lines → 22 lines of assembly → a 560-byte object file → a 38,656-byte program.
  5. There is no fixed relationship between a line of C++ and a machine instruction.
  6. The assembly stage is where portable C++ becomes instructions for one kind of processor.
  7. An object file holds finished machine code plus a list of names it still needs. That is why it cannot be run, and it is the point rather than a limitation.
  8. The linker makes the "needs" and "provides" lists cancel. main.o needed __Z10cycles_forii; cycles.o provided it; c++filt reads it back as cycles_for(int, int).
  9. A compiler error carries a file, line and column. A linker error carries a symbol and no line, and says ld.
  10. Compiling cannot see other translation units; linking checks names, not behaviour.
  11. A successful build means every promise was kept, not that the code is correct.

10. Exercises

Exercise 1 — name the stage. For each, give the stage that reports it and one line of reasoning: (a) 'cycles.hpp' file not found; (b) cannot initialize a variable of type 'int' with an lvalue of type 'const char *'; (c) Undefined symbols: "cycles_for(int, int)"; (d) the program runs and prints 35 where you expected 38.

Exercise 2 — explain the size gap. In the same project, cycles.i was 21 lines and main.i was 64,206. Explain the cause in one sentence, and say which of the two source files you would expect to compile faster and why.

Exercise 3 — predict the symbol lists. Before running anything, say what you would expect nm -u to report for an object file built from a main.cpp that calls two functions defined elsewhere. Then say what would have to be true for the link to succeed.

Exercise 4 — the file you forgot. You add report.cpp defining print_summary(), call it from main.cpp, and build with c++ main.o -o app. Describe the exact error, say which tool produced it, and give the fix. Then explain why the compiler did not complain.

Exercise 5 — the useless flag. A colleague adds -Wall to the link command to catch more problems. Explain why it changes nothing, with reference to which tool runs when.

Exercise 6 — a clean build of wrong code. Change cycles_for to return setup_cycles - transfers * cycles_per_transfer. Does the build succeed? What does the program print for 8 transfers at 2 cycles? State what this tells you about the limits of what a toolchain checks.

Exercise 7 — why unfinished is useful. Give two concrete advantages of compiling each translation unit into its own object file rather than compiling the whole program at once. Relate at least one to how long a rebuild takes after a one-line change.

11. Summary

  • A translation unit is a source file plus its includes — the unit the compiler processes, and the boundary of what it knows.
  • A header has no separate existence: it is pasted into every unit that includes it.
  • Four stages, each one stoppable: -E preprocess, -S compile, -c assemble, then link.
  • Measured: 7 lines → 21 → 22 lines of assembly → 560 bytes of object file → a 38,656-byte program.
  • No fixed line-to-instruction correspondence, and the assembly stage is where portability ends.
  • An object file is unfinished by design: machine code plus a list of names it still needs. That is what makes separate compilation, and fast rebuilds, possible.
  • The linker makes "needs" and "provides" cancel. main.o needed __Z10cycles_forii; cycles.o had it.
  • Compiler errors carry a file and line; linker errors carry a symbol and say ld.
  • Each stage checks something different, and nothing checks whether your code is right.

You have now seen every stage of a build and one error from each of two of them. What you do not yet have is a method — a way to take an unfamiliar message, place it in a stage, and know which assumption to check first. That matters more than it sounds, because the single most common way to lose an hour in C++ is to read a message carefully, look in entirely the wrong place, and find nothing wrong there. Chapter 2.7 turns the table in §7 into a routine.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.