Memory management
How C Variables Are Stored and Accessed in Memory
Introduction
Understanding how variables are stored in memory is fundamental to programming in C. This document explains the key differences between how integers and strings are stored, how the program knows where to find variables, and how modern operating systems manage memory to prevent conflicts between programs.
The Fundamental Difference: Integers vs Strings
Integer Storage
When you declare an integer in C:
int n = 1;The value 1 is stored directly in the memory location allocated for variable n. There is no separate pointer or indirection happening automatically.
The variable
nis the memory location itselfThe value is stored directly at the address assigned to
nWhen you access
n, you're directly reading that memory location
String Storage
Strings work differently because they're actually character arrays:
char *str = "hello";Here, the variable str stores an address (pointer) that points to where the actual characters are stored in memory.
strcontains a memory addressThat address points to where
'h','e','l','l','o','\0'are storedThe string data is stored separately from the pointer variable
Why This Difference Exists
Strings need pointers because:
They have variable length (unknown at compile time)
They're arrays of multiple characters, not a single value
Passing them efficiently requires just passing an address rather than copying all the data
Integers don't need pointers because:
They're fixed size (typically 4 bytes for
int)They fit directly in a register or memory location
Direct storage and access is more efficient
Note: You can use pointers with integers if you explicitly choose to:
But this is optional and explicit—it's not happening automatically like with string literals.
How the Program Knows Where Variables Are Located
The Compilation Process
When you compile your program, the compiler determines where each variable will be stored:
For local variables:
Stored on the stack
Position calculated relative to the stack pointer
Example: "this variable is at stack_pointer + 8 bytes"
Important: The compiler doesn't know what the actual stack pointer value will be - it just generates code using relative offsets
For global variables:
Stored in a fixed data segment
Assigned fixed virtual addresses in the program's address space
When Is the Stack Pointer Actually Set?
The stack pointer is not determined at compile time. Instead:
At Compile Time:
The compiler generates code with relative offsets like "stack_pointer + 8"
It doesn't assign an actual address value to the stack pointer
It just knows the relative positions of variables
At Runtime (when your program starts):
OS allocates stack memory - finds free virtual memory pages for your program's stack
OS sets the initial stack pointer - loads the stack pointer CPU register with the address of the top of the stack
OS may randomize the location - on modern systems with ASLR (Address Space Layout Randomization), the stack location is different each time you run the program
Example of ASLR in action:
Running this multiple times on a modern system:
The stack location is randomized each run for security reasons - this makes it harder for attackers to exploit vulnerabilities.
Key Point: The stack pointer is a CPU register initialized by the operating system at runtime, not a value decided during compilation. The compiler just generates instructions that work with offsets from wherever the OS decides to place the stack.
Runtime Variable Access
Here's the key insight: the compiled machine code already contains the information about where each variable is located. The program doesn't need to "look up" addresses at runtime.
Example C code:
This compiles to machine code roughly like:
Notice that [stack_pointer + 8] appears directly in the instructions. The address calculation is baked into the compiled code itself.
Key Points
Variable addresses are determined at compile time
The compiled instructions contain the information needed to locate variables
No separate "address lookup table" exists at runtime
The CPU directly accesses memory locations based on the compiled instructions
Virtual Memory: Solving the Address Conflict Problem
The Problem
If the compiler assigns specific addresses to variables (like 0x00401000), what happens if another program tries to use the same address? Won't the programs conflict?
The Solution: Virtual Memory
Modern operating systems use virtual memory to give each program its own isolated address space. The addresses used by the compiler are virtual addresses, not physical RAM addresses.
How Virtual Memory Works
Each program gets:
Its own complete virtual address space
The illusion that it has all the memory to itself
The same virtual addresses every time it runs
The operating system:
Creates a separate virtual address space for each program
Maps virtual addresses to actual physical RAM locations
Uses hardware (the MMU - Memory Management Unit) to translate addresses automatically
Example of Address Translation
Both programs use the same virtual address 0x00401000, but they access completely different physical memory locations.
Benefits of Virtual Memory
Isolation: Programs cannot interfere with each other's memory
Consistency: Programs work the same regardless of what else is running
Simplicity: Programmers and compilers can use predictable addresses
Multiple instances: You can run multiple copies of the same program simultaneously
The MMU: The Hardware That Makes It All Work
What Is the MMU?
The MMU (Memory Management Unit) is a hardware component built into the CPU that automatically translates virtual addresses to physical addresses. It's the critical piece of hardware that makes virtual memory possible.
Every time your program accesses memory, the MMU intercepts that access and performs address translation:
This happens automatically and incredibly fast (nanoseconds) - your program doesn't even know it's happening.
How the MMU Works
The MMU uses page tables (data structures created by the operating system) to perform translations:
Virtual address arrives: Program wants to access 0x00401000
MMU splits the address: Divides it into page number + offset within page
Page table lookup: Looks up the page number in the page table
Finds physical frame: Gets the corresponding physical page frame number
Combines address parts: Physical page frame + offset = complete physical address
Accesses RAM: Performs the actual memory operation at the physical location
Example of MMU Translation
The TLB: Making Translation Fast
The MMU includes a special cache called the TLB (Translation Lookaside Buffer):
Purpose: Stores recent address translations
Speed: Extremely fast - avoids having to read page tables from memory
Importance: Without the TLB, every memory access would require multiple additional memory reads just to translate the address
Typical size: 64-512 entries
Hit rate: Usually 95-99% for typical programs
When the MMU needs to translate an address:
First checks the TLB (very fast)
If found (TLB hit): uses the cached translation
If not found (TLB miss): reads from page tables in memory, then caches the result
Memory Protection by the MMU
Page tables don't just store address mappings - they also include permission bits:
Read permission: Can the program read this page?
Write permission: Can the program write to this page?
Execute permission: Can the program execute code from this page?
The MMU enforces these permissions automatically:
This is what prevents:
One program from accessing another program's memory
Programs from writing to read-only code sections
Execution of data sections (security feature)
MMU vs Operating System Responsibilities
MMU (Hardware):
Performs address translation billions of times per second
Enforces memory permissions on every access
Triggers page faults when problems occur
Maintains the TLB cache
Works at hardware speed (nanoseconds)
Operating System (Software):
Creates and manages the page tables
Handles page faults (decides what to do when they occur)
Allocates and tracks physical memory
Sets up the initial page table configuration
Updates page tables when programs allocate/free memory
The MMU is the executor; the OS is the manager.
What Happens Without an MMU?
Older systems and some embedded systems operate without an MMU:
Without MMU:
Programs use physical addresses directly
No memory protection between programs
One buggy program can overwrite another program's memory
Simpler hardware but much less safe
Crashes are more catastrophic
With MMU:
Virtual memory isolation
Memory protection
Each program in its own sandbox
System stability even when programs crash
Page Faults: When the MMU Asks for Help
Sometimes the MMU encounters a problem during translation and triggers a page fault. Common reasons:
Access violation: Program tries to access memory it doesn't have permission for
Page not present: The page is valid but hasn't been loaded into RAM yet (swapped to disk)
First access: Page hasn't been allocated yet (lazy allocation)
When a page fault occurs:
MMU stops the memory access
CPU switches to OS kernel mode
OS handles the fault (loads page, terminates program, etc.)
If resolved, MMU retries the access
The MMU in Action: Complete Example
What actually happens:
All of this happens in nanoseconds, completely transparent to your program.
Why the MMU Is Essential
The MMU is what makes modern computing possible:
Security: Programs cannot interfere with each other
Stability: One crashed program doesn't bring down the system
Simplicity: Programmers can use predictable virtual addresses
Efficiency: Multiple programs can run simultaneously
Flexibility: OS can move pages in physical memory without programs knowing
Without the MMU, we'd still be in the era of single-tasking systems where one program runs at a time and has access to all memory.
How the OS Manages Physical Memory
Memory Tracking
The operating system maintains data structures to track physical memory usage:
Page frame table or bitmap
Tracks every "page" of physical RAM (typically 4KB chunks)
Each page is marked as either "free" or "allocated to a specific process"
When Your Program Starts
The following steps occur:
OS checks available memory: Examines its tracking structures to find free physical pages
Allocates pages: Assigns enough free physical pages for your program's needs
Creates page tables: Sets up mappings from virtual addresses to physical addresses
Configures CPU: Loads the page table address into a special CPU register (like CR3 on x86 processors)
Example Memory Allocation
During Program Execution
The CPU's MMU (Memory Management Unit) translates addresses automatically
Every memory access goes through: Virtual Address → Page Table Lookup → Physical Address
The CPU doesn't decide which physical memory to use—it follows the mappings the OS created
The OS manages what's free or used; the CPU just executes the translations
Division of Responsibilities
Operating System:
Maintains the "big picture" of physical memory usage
Decides which physical pages to allocate to which programs
Creates and manages page tables
Handles memory allocation and deallocation
CPU (specifically the MMU):
Performs fast address translation using the page tables
Translates virtual addresses to physical addresses automatically
Does not make allocation decisions—just follows OS instructions
Putting It All Together
Let's trace what happens when you run a simple program:
Compile Time
Compiler assigns virtual address for
n(e.g., stack_pointer + 8)Generates machine code with these address calculations embedded
Creates an executable file
Program Load Time
OS creates a new virtual address space for your program
OS finds free physical memory pages
OS creates page tables mapping virtual → physical addresses
OS loads the page table address into the CPU
Runtime
CPU executes:
MOV [stack_pointer + 8], 1MMU translates virtual address to physical address using page tables
Value
1is stored in physical RAMSimilar process for the addition and final return
Meanwhile
Other programs are running with their own virtual address spaces
They might even use the same virtual addresses
But the OS keeps them completely isolated through different page table mappings
No program can access another program's physical memory
Summary
Integers are stored directly in memory; strings use pointers to character data
Compiled code contains the information needed to locate variables—no runtime lookup needed
Virtual memory gives each program its own isolated address space using the same virtual addresses
The OS manages physical memory and creates mappings; the CPU's MMU performs translations
This system allows multiple programs to run simultaneously without conflicts
Key Takeaways
Variable storage is decided at compile time using virtual addresses
The operating system maps virtual addresses to available physical memory
The CPU's MMU translates addresses automatically during execution
Each program is isolated in its own virtual address space
Physical memory conflicts are impossible because the OS manages all allocations
Last updated