Introduction
Welcome to the final lesson in our first module on computer organization fundamentals. In our previous lesson, we traced the journey of a C++ source file through preprocessing, compilation, and assembly, culminating in the creation of an object file (.o). We discovered that this file contains machine code but is not yet a complete, runnable program. The key reason is that it contains an incomplete puzzle: a symbol table with "undefined" symbols—references to functions and variables that are defined in other files or libraries.
This lesson completes the puzzle. Our goal is to explain the role of the linker in resolving symbols and combining object files to create a final executable. We will explore how the linker acts as a master assembler, taking all the individual object files from your project, along with necessary system libraries, and weaving them together into a single file that the operating system can load and run. Understanding this final step in the build process is crucial for debugging a whole class of common C++ errors and for appreciating how large software projects are constructed.
The Linker's Core Task: Symbol Resolution
At its heart, the linker is a "detective" whose job is to resolve names. These names are called symbols.
In the context of linking, a symbol is simply a name given to a piece of data or code. Given your C++ background, you're already very familiar with them:
- A function name like
calculate_torqueis a symbol. - A global variable like
int max_iterations = 100;is a symbol.
Each object file contains a symbol table, which lists every symbol the file defines and every symbol it references but doesn't define.
The linker's primary job is symbol resolution: for every symbol reference in every object file, the linker must find exactly one corresponding definition among all the object files and libraries it's given.

To solidify this concept, let's start with a short article that clearly defines what symbols are and the role they play.
The Linker's Detective Work: How Symbol Resolution Turns Code ...
The article 'The Linker's Detective Work' provides an excellent analogy for the linker's job. Please read the first two main sections to get a clear definition of symbols and their origin in the compilation pipeline.
Read the introduction and the sections titled 'What Are Symbols, Really?' and 'The Compilation Pipeline: Where Symbols Come From'. This will reinforce the connection between your code and the symbols the linker works with.
This entire process of combining separate object files is what makes modular programming in C++ possible. Without a linker, you would have to put your entire program into a single gigantic source file.
Common Linker Errors in Practice
As a C++ programmer, you have almost certainly encountered linker errors. They can be frustrating because they don't point to a specific line of code like a syntax error does. Instead, they signal a structural problem with your project. Understanding how the linker works turns these cryptic messages into actionable diagnostic information.
The following video from The Cherno channel provides a fantastic, practical demonstration of the two most common linker errors using C++ in Visual Studio.
This video, 'How the C++ Linker Works,' walks through common scenarios that cause linker errors. It's highly practical and directly relates to a C++ developer's experience.
Please watch the following three segments: 00:00-01:14: A high-level introduction to the linker's purpose. 05:37-06:59: Demonstrates the 'unresolved external symbol' error. This happens when the linker cannot find a definition for a function that was declared and called. 09:41-12:40: Explains the 'multiply defined symbols' error, showing how defining a function in a header file can lead to this problem.
Let's summarize these two key errors:
-
Unresolved External Symbol (or
undefined reference):- What it means: You've used a function or variable that was declared (so the compiler was happy), but the linker couldn't find its definition in any of the provided object files or libraries.
- Common Causes:
- Forgetting to add a
.cppfile to your build, so its object file is never created and passed to the linker. - A typo in a function name or a mismatch in its signature (e.g., different parameter types). In C++, this is related to name mangling, where the compiler encodes the function's signature into its symbol name.
void foo(int)andvoid foo(double)are different symbols. - Forgetting to link against a required library (e.g.,
-lmfor the math library on Linux).
- Forgetting to add a
-
Multiple Definition (or
multiply defined symbols):- What it means: The linker found two or more definitions for the same symbol. It doesn't know which one is the "correct" one to use.
- Common Cause: The most frequent culprit is defining a function or global variable in a header file. When that header is
#included in multiple.cppfiles, each resulting object file gets its own copy of the definition, leading to a conflict during linking.
The standard C++ solution, as the video points out, is the One Definition Rule (ODR): place declarations (function prototypes, extern variables) in header files, but place definitions (function bodies, global variable instantiations) in exactly one source (.cpp) file.
Resolving Ambiguity: Strong and Weak Symbols
What happens when the linker finds multiple definitions, but the situation is a bit ambiguous? For example, what if you have int my_global; in two different files? The C and C++ standards define a formal set of rules for this, based on the concept of strong and weak symbols.
- Strong Symbols: These are function definitions and initialized global variables. For example:
int x = 5;orvoid my_func() { ... }. - Weak Symbols: These are uninitialized global variables. For example:
int x;.
The linker follows these rules:
- Multiple strong symbols with the same name are not allowed. This results in a "multiple definition" error.
- If there is one strong symbol and one or more weak symbols with the same name, the linker chooses the strong symbol. All references will point to the strong definition.
- If there are multiple weak symbols with the same name, the linker picks one arbitrarily. This is legal but can lead to very subtle and dangerous bugs where different parts of your code unknowingly share the same variable.
The following reading explains this concept clearly.
The Linker's Detective Work: How Symbol Resolution Turns Code ...
Let's return to 'The Linker's Detective Work' to understand how strong and weak symbols resolve conflicts.
Read the section titled 'Symbol Types: Strong vs. Weak'. It lays out the definitions and the three resolution rules very clearly.
From Resolved Symbols to a Runnable Executable
After the linker has successfully resolved every symbol, it performs two final, crucial steps to create the executable file.
1. Relocation
At this point, the linker has decided on the size of the final code and data sections by combining the sections from all input object files. It can now lay out the final program in a virtual address space.
Relocation is the process of patching the machine code to use these final memory addresses. The compiler, when it created the object file, didn't know the final addresses of functions and variables. So it left placeholders in the machine code. The linker now goes back through the code and fills in these placeholders with the actual addresses it has just calculated.
For example, a machine code instruction like call 0x0000 (a placeholder for a function call) will be updated to call 0x08048370 (the final address of the target function).
2. Merging and Output
The linker merges all the sections of the same type (e.g., all .text sections are combined into one large .text section) and combines them with information needed by the operating system's loader. This is all packaged into a standard executable format, such as ELF (Executable and Linkable Format) on Linux or PE (Portable Executable) on Windows.
This final output includes not just your code, but also the code from any static libraries you used, and information about the dynamic libraries (like the C++ standard library) that your program will need at runtime.

Conclusion
In this lesson, we've completed our journey from source code to execution. You now know that the linker is the essential tool that transforms a collection of separately compiled object files into a cohesive, runnable program.
Key Takeaways:
- The linker's primary role is symbol resolution: matching references to functions and global variables with their unique definitions across all object files and libraries.
- The two most common linker errors are unresolved symbols (a definition is missing) and multiple definitions (a symbol is defined more than once).
- The linker uses rules based on strong and weak symbols to resolve certain types of definition conflicts.
- After resolving symbols, the linker performs relocation to patch machine code with final memory addresses.
- The final output is a single executable file that combines code and data from all inputs into a format the OS can understand.
Preview of the Next Module:
We have successfully built an executable file. The next logical question is: what happens when we run it? How does the OS load it into memory, and how does the CPU access that code and data efficiently?
This concludes Module 1. In the next module, Memory Hierarchy and Cache Systems, we will begin to answer these questions by exploring the tiered structure of memory, starting with the fastest and most critical components: CPU caches. We will investigate how they work and why they are fundamental to modern processor performance.