Embedded Systems
Understanding External Variables in C: From Memory Layout to Link-Time Symbol Resolution
Introduction
One of the most misunderstood concepts in C programming is the external variable, commonly referred to using the extern keyword. While most programmers know that extern allows a variable to be shared across multiple source files, very few understand what actually happens behind the scenes – from compilation to linking – and how the operating system or embedded runtime eventually places the variable into memory. This topic becomes even more important in embedded systems, where large software projects may contain hundreds of source files. Understanding external variables not only helps in writing modular software but also provides valuable insight into how compilers,
assemblers, and linkers work together to create an executable program.
In this article, we’ll explore:
- What external variables are
- Difference between declaration and definition
- Where external variables are stored in memory
- Scope, lifetime, and linkage
- What happens during compilation
- Why the compiler does not report errors
- How the linker resolves external variables
- Best practices for using external variables in large projects
What is an External Variable?
An external variable is simply a global variable that is defined in one source file but can be accessed/reference from another source file.
Consider the following example.
Lets say a File: system.c
int systemState = 0;
This is the definition of the variable.
Now another source file can access it by referring it as mentioned in the below line by using extern keyword File: application.c
extern int systemState;
void Application_Task(void)
{
systemState = 1;
}
Here, the keyword extern tells the compiler:
“This variable exists somewhere else outside the file, but within the current project. I only want to use it here.” Notice that the variable is defined only once but may be declared many times. The memory is allocated only once during definition, but can be referred at multiple instances through declaration.
Declaration vs Definition
Understanding the difference between declaration and definition is fundamental.
A definition actually creates the variable and allocates memory for it.
int counter = 100;
This statement tells the compiler:
- Create a variable named counter
- Allocate memory
- Where Initialize it with the value 100
On the other hand, extern int counter; does not allocate memory.
Instead, it simply informs the compiler that memory has already been allocated elsewhere.
Think of it this way:
Definition = Creates the variable
Declaration = Introduces the variable
Where is an External Variable Stored?
A common misconception is that variables declared with extern occupy some special memory region. They do not. The memory location depends entirely on where the variable is defined, not where it is declared.
For example, int speed = 50; is stored inside the .data section, because it has been initialized.
.data
speed = 50
If the variable is defined without an initial value,
int speed;
it is stored inside the .bss section.
.bss
speed = 0
Notice that the extern declaration itself consumes no memory.
For example,
extern int speed;
only creates a reference to an already existing variable in the file across the projects.
Memory Layout of a C Program
A typical executable program consists of multiple memory sections.
![]()
External variables are stored in either the DATA or BSS section, depending on whether they are initialized. The extern declaration itself is not stored in any memory section because it is merely a promise/reference that the variable exists elsewhere and compiler is confident that it will get the address location of the variable at later point of time.
Scope, Lifetime, and Linkage
These three terms are often confused.
Scope
Scope defines where the variable can be accessed within the source code.
A global variable declared without static has file scope but external linkage, meaning
it can be accessed from other source files using extern.
Example:
/* file1.c */
int counter;
/* file2.c */
extern int counter;
Both files access the same variable.
Lifetime
External variables have static storage duration.
If they are created before main() begins execution and remain alive until the program terminates.
Unlike local variables, they are not created and destroyed every time a function is called.
Linkage
By default,
int counter;
has external linkage.
However,
static int counter;
has internal linkage.
This means it is visible only inside the current source file.
Even if another file writes
extern int counter;
the linker cannot find it because the symbol is hidden.
How Compilation Actually Works
Many beginners believe that the compiler compiles the entire project at once.
It does not. Each source file is compiled independently.
Suppose we have two files.
system.c
int counter = 10;
application.c
extern int counter;
void Task()
{
counter++;
}
Compilation proceeds independently.
system.c -> Compiler -> system.o -> application.c -> Compiler-> application.o
Notice that the compiler never sees both files together.
This fact explains why the compiler does not immediately know where counter is defined.
Why Doesn’t the Compiler Generate an Error?
This is probably the most frequently asked question.
When the compiler encounters
extern int counter;
it simply records that a variable named counter exists somewhere.
The compiler assumes that another object file will provide the actual definition. Therefore,
compilation succeeds without any errors. Internally, the compiler generates machine
instructions that reference a symbol, not an address.
Conceptually,
LOAD counter
ADD 1
STORE counter
The actual address is still unknown.
The Role of the Symbol Table
Every object file generated by the compiler contains a symbol table. The symbol table
records information about variables and functions.
For example,
system.o
Symbol: counter
Status: Defined
Section: DATA
application.o
Symbol: counter
Status: Undefined
Notice that the second object file simply says, “I need a variable named counter.”
What Happens During Linking?
After compilation, the linker collects all object files.
system.o
application.o
driver.o
uart.o
…
The linker now examines every symbol table.
It finds counter defined in
system.o
and referenced inside
application.o
The linker matches these symbols. Suppose the linker assigns the following address:
Counter-> 0x20001020
Now every instruction referencing counter is updated.
Before linking,
LOAD counter
After linking,
LOAD 0x20001020
This process is called symbol resolution.
Updating the machine instructions with the final addresses is known as relocation.
What Happens if No Definition Exists?
Suppose we write
extern int counter;
but never define
int counter;
Compilation still succeeds.
However, during linking, the linker searches every object file. Since no definition exists, it
reports an error similar to Undefined reference to ‘counter’. Notice that this is a link-time
error, not a compile-time error.
What Happens if Multiple Definitions Exist?
Now consider the opposite situation.
file1.c
int counter;
file2.c
int counter;
Now the linker discovers two definitions. Since only one definition is allowed, it reports
Multiple definition of ‘counter’ This ensures that every global variable has a unique memory location.
External Variables in Embedded Systems
External variables are heavily used in embedded software.
Consider an ARM Cortex-M project.
/* clock.c */
uint32_t SystemCoreClock = 16000000;
Other modules simply declare
extern uint32_t SystemCoreClock;
UART drivers, timer drivers, USB stacks, and communication middleware all use the same
variable.
Only one copy exists in memory. Every module accesses the same address. This reduces
memory consumption and allows different software components to share system
information.
Best Practices
Although external variables are useful, excessive usage can make software difficult to
maintain.
Some recommended practices are:
- Define every global variable in exactly one source file.
- Declare it in a corresponding header using extern.
- Include the header wherever the variable is required.
- Avoid exposing unnecessary global variables.
- Use static whenever a variable should remain private to a source file.
- Minimize global state to improve modularity and testability.
- Prefer accessor functions when appropriate to protect shared data.
Conclusion
External variables are much more than a programming convenience – they are a
cornerstone of how large C applications are built. The compiler treats extern as a
declaration, generating references to symbols without knowing their final addresses. It is
the linker that later connects these references to the single definition, assigns final memory
locations, and updates the generated machine code through symbol resolution and
relocation.
Once you understand this workflow, many concepts in C become much clearer, including
global variables, static variables, object files, symbol tables, link-time errors, startup code,
and even linker scripts used in embedded systems.
For embedded engineers, mastering external variables is an important step toward
understanding the complete software build process – from writing C code to generating a
fully linked executable that runs on the target hardware.
75,221
SUBSCRIBERS
Subscribe to our Blog
Get the latest VLSI news, updates, technical and interview resources



