In this lesson, you will learn how the linker acts as a master puzzle-solver to connect references in one object file to the actual definitions residing in another.
The Problem: Isolated Translation Units
When you compile main.c and sensors.c separately, the compiler creates individual object files (main.o and sensors.o). Each file is essentially an island. If main.c calls a function read_temperature(), the compiler doesn't know where that code lives in memory. Instead, it leaves a "placeholder" or undefined symbol in main.o, noting that it expects to find a definition for read_temperature later.
The Linking Solution: Two-Pass Symbol Resolution
The linker performs the heavy lifting by scanning all input object files to reconcile these placeholders. This process generally occurs in two stages:
- Symbol Table Collection: The linker iterates through all provided object files and builds a comprehensive table. It maps every global symbol (function names, global variables) to its associated section and offset within that specific object file.
- Reference Resolution: The linker then revisits every object file that contains an "undefined reference." It looks for a matching symbol name in its collection. If it finds one—for example, matching the call in
main.oto the definition insensors.o—it replaces the placeholder with the actual memory address (or a relocation entry) of that symbol.
A Concrete Illustration
Imagine main.o has an instruction that says: "Call the function located at [UNKNOWN_ADDRESS] for symbol 'read_temperature'."
Once the linker processes sensors.o, it determines that read_temperature is actually located at offset 0x400 inside the .text section of sensors.o. The linker updates the machine code in the final binary so that the instruction now points to 0x400.
/* main.o expects a definition */
extern void read_temperature(void);
/* sensors.o provides the definition */
void read_temperature(void) {
/* implementation */
}
If the linker finds no definition (a missing object file) or two definitions for the same symbol (a collision), it will throw a fatal error. Your job as a firmware developer is to ensure all required symbols are provided to the linker so it can successfully "stitch" your final binary together.
Key Takeaway: The linker resolves symbol references by matching undefined placeholders in one object file with the memory-offset definitions found in the global symbol tables of other compiled modules.