Page cover

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 n is the memory location itself

  • The value is stored directly at the address assigned to n

  • When 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.

  • str contains a memory address

  • That address points to where 'h', 'e', 'l', 'l', 'o', '\0' are stored

  • The 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):

  1. OS allocates stack memory - finds free virtual memory pages for your program's stack

  2. OS sets the initial stack pointer - loads the stack pointer CPU register with the address of the top of the stack

  3. 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

  1. Isolation: Programs cannot interfere with each other's memory

  2. Consistency: Programs work the same regardless of what else is running

  3. Simplicity: Programmers and compilers can use predictable addresses

  4. 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:

  1. Virtual address arrives: Program wants to access 0x00401000

  2. MMU splits the address: Divides it into page number + offset within page

  3. Page table lookup: Looks up the page number in the page table

  4. Finds physical frame: Gets the corresponding physical page frame number

  5. Combines address parts: Physical page frame + offset = complete physical address

  6. 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:

  1. First checks the TLB (very fast)

  2. If found (TLB hit): uses the cached translation

  3. 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:

  1. Access violation: Program tries to access memory it doesn't have permission for

  2. Page not present: The page is valid but hasn't been loaded into RAM yet (swapped to disk)

  3. First access: Page hasn't been allocated yet (lazy allocation)

When a page fault occurs:

  1. MMU stops the memory access

  2. CPU switches to OS kernel mode

  3. OS handles the fault (loads page, terminates program, etc.)

  4. 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:

  1. OS checks available memory: Examines its tracking structures to find free physical pages

  2. Allocates pages: Assigns enough free physical pages for your program's needs

  3. Creates page tables: Sets up mappings from virtual addresses to physical addresses

  4. 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

  1. Compiler assigns virtual address for n (e.g., stack_pointer + 8)

  2. Generates machine code with these address calculations embedded

  3. Creates an executable file

Program Load Time

  1. OS creates a new virtual address space for your program

  2. OS finds free physical memory pages

  3. OS creates page tables mapping virtual → physical addresses

  4. OS loads the page table address into the CPU

Runtime

  1. CPU executes: MOV [stack_pointer + 8], 1

  2. MMU translates virtual address to physical address using page tables

  3. Value 1 is stored in physical RAM

  4. Similar 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

  1. Variable storage is decided at compile time using virtual addresses

  2. The operating system maps virtual addresses to available physical memory

  3. The CPU's MMU translates addresses automatically during execution

  4. Each program is isolated in its own virtual address space

  5. Physical memory conflicts are impossible because the OS manages all allocations

Last updated