Memory Layout of a C Program
Every running C program's memory is organized into a small number of distinct regions — text, data, BSS, heap, and stack — and understanding what lives where explains behavior (and bugs) that otherwise seem mysterious.
When a C program runs, its compiled instructions, its global data, its local variables, and any memory it allocates on the fly all have to live somewhere — and they don't all live in the same place. Operating systems and compilers conventionally divide a running process's address space into a handful of regions with names like text, data, BSS, heap, and stack, each with its own rules about who puts what there and how long it stays. None of this is required by the C standard itself, but it is how essentially every real system works, and it is the single best mental model for understanding why a local variable disappears when a function returns, why a pointer can go dangling, why a string literal can't be modified, and why a buffer overflow can corrupt memory that has nothing to do with the buffer that overflowed. This page walks the whole picture end to end: what each region is for, which kind of variable ends up where, how function calls and malloc() actually use that memory, and the classic bugs that only make sense once you can picture the layout underneath them.
Program Memory vs. C Objects
C defines objects, storage duration, lifetime, scope, and linkage — but nowhere does the language standard require that every implementation organize memory into exactly a text segment, a data segment, a BSS segment, a heap, and a stack. That five-region layout is an extremely common convention used by operating systems, compilers, and executable file formats, not a rule the C language itself hands down.
So when you hear a statement like "globals are stored in the data segment," what's really being said is: on conventional systems, objects with certain properties (file scope, static storage duration, an explicit nonzero initializer) commonly end up in a data-related region of memory. The exact physical placement is always implementation-dependent — but the convention is so widespread and so useful for reasoning about programs that the rest of this page uses it as the default mental model.
A Typical Process Memory Map
Laid out from high addresses down to low addresses, a typical process looks roughly like the picture below: the stack at the top, growing downward as functions are called; a heap somewhere below it, growing upward as memory is allocated; unused free space in between; and the BSS, data, read-only data, and text regions sitting at the bottom, in that order, holding everything that exists before a single byte of dynamic memory is allocated.
This is the conventional picture, not something the C standard requires. The stack growing down and the heap growing up toward each other is a widely used convention on many architectures, but it is not guaranteed — some systems lay memory out quite differently.
High addresses
+---------------------+
| |
| Stack | <- local variables, function-call frames
| (grows downward) |
| | |
| v |
+---------------------+
| |
| unused / free |
| space |
| ^ |
| | |
| (grows upward) |
| Heap | <- malloc(), calloc(), etc.
+---------------------+
| BSS | <- zero-initialized / uninitialized static data
+---------------------+
| Data | <- initialized static data
+---------------------+
| Read-only data | <- string literals, constant data
+---------------------+
| Text | <- compiled machine instructions
+---------------------+
Low addressesThe Text (Code) Segment
The text segment, also called the code segment, holds the executable machine instructions the compiler produced from your functions — the compiled body of add(), main(), and everything else. It sits near the bottom of the conventional layout because its size and contents are fixed once the program is compiled; nothing about it changes while the program runs.
Because the instructions never need to change at runtime, the text segment is commonly mapped readable and executable, but not writable. That is a deliberate protection: it stops both accidental bugs and malicious code from rewriting the program's own instructions while it runs.
Read-only Data
message itself is a pointer variable (automatic if declared inside a function), but the characters "Hello" it points at are a string literal, and string literals typically live in a read-only data region rather than in message's own storage.
This region commonly holds string literals and other constant data the compiler can identify ahead of time. As with everything else on this page, the exact placement is implementation-dependent, and const alone does not guarantee that an object physically sits in read-only memory — that distinction gets its own section further down.
const char *message = "Hello";String Literals and Why You Can't Modify Them
char *p = "Hello"; declares p as an ordinary pointer — automatic if it's local — but the literal "Hello" it points to has static storage duration and is not modifiable, regardless of what p's own storage duration is.
The difference is what each declaration actually creates. char *p = "Hello"; makes p point directly at the literal's own storage, which the implementation is free to place in read-only memory. char text[] = "Hello"; instead copies the literal's characters into a brand-new array that belongs to text, so writing into text is perfectly ordinary array modification.
Writing through a pointer to a string literal, as in p[0] = 'J';, is undefined behavior even though the compiler often lets it compile without complaint. If you need a modifiable copy of literal text, always declare it as an array, e.g. char text[] = "Hello";, never as a pointer initialized directly from a literal.
char *p = "Hello";
p[0] = 'J'; // undefined behavior: modifying a string literal
char text[] = "Hello";
text[0] = 'J'; // fine: text is a real, writable arrayThe Initialized Data Segment
Both variables have static storage duration and an explicit, nonzero initializer, so their initial values have to be represented somewhere in the program itself — that's what the initialized data segment (often just called "data") is for.
Anything with static storage duration and a nonzero initial value commonly ends up here. The values 100 and 50 aren't computed at startup from nothing — they're baked into the program image, ready to be copied into place before main() runs.
int global_count = 100;
static int limit = 50;The BSS Segment
Neither variable has an explicit initializer, so C guarantees both start out zeroed. That guarantee is about the value you observe, not about how the executable stores it — this is exactly what the BSS segment is for.
BSS commonly holds uninitialized (or explicitly zero-initialized) objects with static storage duration. Because every one of them is guaranteed to start at zero, the executable file doesn't need to store a thousand zero bytes for buffer — it can simply record "reserve this much zeroed storage" and let the loader fill it in. BSS is really an executable-format and implementation concept for saving space, not something the C language mandates by name.
int global_count;
static int buffer[1000];Data vs. BSS, Side by Side
All four variables have static storage duration, but they split cleanly into two groups based on one thing only: whether they were given an explicit, nonzero initializer.
a and x carry the values you actually wrote in source, stored as part of the program image in .data. b and y never got an initializer, so both start at 0 in .bss — guaranteed by C, but stored far more cheaply than a and x are.
int a = 100;
int b;
static int x = 10;
static int y;The Stack
On conventional implementations, x and y live inside function()'s own stack frame — a chunk of memory set aside for this one call, holding its automatic (local) objects. The exact layout inside that frame, right down to whether x comes before y in memory, is determined by the compiler and the platform's calling convention (its ABI), not by C itself.
void function(void) {
int x = 10;
int y = 20;
}Automatic Variables
A local variable like int x = 10; has automatic storage duration: it typically exists only while execution is somewhere inside its enclosing block, and it typically stops existing the moment that block is left. The word "automatic" refers to that lifetime — the compiler manages it for you without any explicit request.
It's tempting to treat "automatic storage duration" and "lives on the stack" as the same thing, but they aren't. The compiler is free to keep a local variable in a CPU register the entire time, or to optimize it away completely if it can prove the value is never actually needed — automatic storage duration only promises a lifetime, not a specific physical location.
Function Calls and Stack Frames
When main() calls calculate(), calculate() conceptually gets its own stack frame — a region holding its parameters, its local variables, and enough bookkeeping to know where to return to. When calculate() returns, everything in that frame stops existing, and the space it occupied is free to be reused by the next call.
main() calls calculate(3, 4); a new stack frame is set up above main()'s own frame.
Inside that frame, a and b hold 3 and 4, and result is computed as 7.
calculate() returns 7 to main(); its stack frame is no longer active.
total receives 7; nothing about calculate()'s a, b, or result is still reachable.
int calculate(int a, int b) {
int result = a + b;
return result;
}
int main(void) {
int total = calculate(3, 4);
return 0;
}Nested Calls: Frames Stacking and Unwinding
first() is called; its frame goes on top of main()'s frame.
first() calls second(); second()'s frame goes on top of first()'s.
second() calls third(); third()'s frame goes on top of second()'s — three frames are now stacked.
third() returns; its frame is no longer active, and control resumes in second() right after the call.
second() finishes and returns; then first() finishes and returns — each frame unwinds in the reverse order it was pushed.
The frames stack up in call order and unwind in exactly the reverse order — the same last-in-first-out shape that gives "the stack" its name.
#include <stdio.h>
void third(void) {
printf("in third\n");
}
void second(void) {
printf("in second, before third\n");
third();
printf("in second, after third\n");
}
void first(void) {
printf("in first, before second\n");
second();
printf("in first, after second\n");
}
int main(void) {
first();
return 0;
}Which Way Does the Stack Grow?
On many common architectures, each new stack frame is placed at a lower address than the one before it, so the stack is often described as growing "downward" toward lower addresses as calls get deeper.
This is a convention of common hardware and calling conventions, not a requirement of the C language. Never write code that assumes a particular stack growth direction — comparing the addresses of two local variables to infer call depth, for instance, is not portable behavior.
Heap and Stack Growing Toward Each Other
In the conventional picture, the stack starts near the top of the address space and grows downward, while the heap starts lower down and grows upward — leaving a pool of unused free space between them that shrinks from both ends as the program calls deeper functions and allocates more dynamic memory.
It's a common misconception that the stack and heap are required to grow toward each other. That layout is simply one widely used convention meant to let both regions expand without a fixed boundary between them — C makes no such requirement, and some systems arrange memory quite differently.
Stack Overflow
Because the stack has finite space, pushing too many frames — through very deep or unbounded recursion, or through a single frame with a very large local array — can exhaust it. This condition is commonly called a stack overflow.
Whether a given piece of code actually triggers a stack overflow, and what happens when it does, depends entirely on the implementation and how much stack space is available — it is not something the C standard defines or guarantees detecting. A function that recurses without a base case, or that declares a multi-megabyte local array, is a classic way to run into this.
The Heap and malloc()
This line creates two separate things: p, an ordinary pointer variable, and the memory malloc() hands back, which has allocated storage duration and conceptually lives in the heap. p and the object it points to are not the same thing, and they can even live in entirely different regions.
p itself is a local variable sitting on the stack, but the value stored inside p is the address of a completely separate int object that malloc() carved out of the heap.
The C standard only specifies the semantics of dynamic allocation — that malloc() returns memory of at least the requested size, valid until freed. It does not guarantee that memory physically lives in a region literally named "the heap"; that word is simply the near-universal name implementations use for wherever dynamic allocation actually happens.
int *p = malloc(sizeof(int));Stack and Heap Together in One Function
x is created on function()'s stack frame and set to 10.
p, also on the stack, is set to the address of a new int that malloc() allocated on the heap.
*p writes 20 into that heap object through p.
free(p) releases the heap object back to the allocator; the object's lifetime ends.
function() returns; x and p, both stack-frame objects, cease to exist too — but that happened only because the frame ended, which is a separate event from the free() call.
#include <stdio.h>
#include <stdlib.h>
void function(void) {
int x = 10;
int *p = malloc(sizeof(int));
if (p == NULL) {
return;
}
*p = 20;
printf("x=%d *p=%d\n", x, *p);
free(p);
}
int main(void) {
function();
return 0;
}Dangling Pointers
free(p) ends the allocated object's lifetime, but it doesn't change the value stored in p — p still holds the same address it did a moment ago. Using that stale address as if it still pointed to a valid object is undefined behavior.
A simple defensive habit is to write p = NULL; immediately after free(p);. That doesn't restore or protect the freed memory in any way, but it does turn any later accidental use of p into an immediate, detectable null-pointer access instead of a silent, harder-to-diagnose use of stale memory.
int *p = malloc(sizeof(int));
*p = 10;
free(p);
/* p is now dangling: it still holds an address, but that address
no longer refers to a live object */
*p = 20; // undefined behaviorNever Return the Address of a Local Variable
x has automatic storage duration tied to get_value()'s own stack frame. By the time the caller can use the returned pointer, that frame is gone, and the pointer refers to memory the program may already be reusing for something else entirely.
Both fixes avoid handing back the address of an object whose lifetime is already over: one allocates on the heap and documents that the caller must free() it, the other has the caller supply its own storage up front so nothing needs to be returned by address at all.
This is one of the most common pointer bugs in C, and it always comes from the same mistake: returning &local_variable from a function. The moment the function returns, local_variable's storage duration is over, no matter how confident the returned address looks.
int *get_value(void) {
int x = 10;
return &x; // invalid: x's storage ends when get_value returns
}#include <stdio.h>
#include <stdlib.h>
int *get_value_heap(void) {
int *p = malloc(sizeof(int));
if (p != NULL) {
*p = 10;
}
return p; /* caller now owns this memory and must free() it */
}
void get_value_out(int *result) {
*result = 10; /* caller supplies storage, so nothing is returned by address */
}
int main(void) {
int *heap_value = get_value_heap();
if (heap_value != NULL) {
printf("heap: %d\n", *heap_value);
free(heap_value);
}
int local_value;
get_value_out(&local_value);
printf("out param: %d\n", local_value);
return 0;
}Static Local Variables
count is declared inside counter(), so its scope is limited to that function — but the static keyword gives it static storage duration, meaning it is created once, keeps its value between calls, and isn't destroyed and recreated on every call the way an ordinary local variable would be.
First call: count starts at 0 (only once, ever), increments to 1, and prints 1.
Second call: count already holds 1 from before, increments to 2, and prints 2.
Third call: count already holds 2, increments to 3, and prints 3.
This is a clean demonstration that scope and storage duration are two different properties: count is only visible inside counter(), yet it exists for the entire run of the program, unlike an ordinary stack-frame local.
#include <stdio.h>
void counter(void) {
static int count = 0;
count++;
printf("count = %d\n", count);
}
int main(void) {
counter();
counter();
counter();
return 0;
}Global Variables
Both have file scope and static storage duration, so both exist for the entire run of the program — but the presence or absence of an explicit initializer is what decides which conventional region each one lands in.
system_state, with its explicit nonzero value, is commonly placed in a data-related region. uninitialized_state, with no initializer, is commonly placed in BSS instead, and is guaranteed to start at zero. As always, the exact physical placement is implementation-dependent.
It's a common misconception that all global variables end up in BSS. Only the ones without an explicit initializer (or explicitly initialized to zero) typically do; a global given a nonzero initial value typically goes into the data segment instead, exactly like a local static variable does.
int system_state = 1; // has an explicit initializer
int uninitialized_state; // has no initializerStatic File-Scope Variables and Internal Linkage
This variable has file scope, static storage duration, and internal linkage. Because it's nonzero-initialized, it commonly lands in the data segment, just like a non-static global would.
The word static here does two very different jobs depending on context: at file scope, it primarily restricts linkage so this name can't be referred to from other translation units — it isn't primarily a statement about "static memory," even though the object also happens to have static storage duration.
static int internal_state = 10;const Does Not Guarantee Read-only Memory
const is a type qualifier: it stops you from modifying value through a const-qualified lvalue at compile time. It says nothing at all about the physical memory value ends up in.
A common misconception is that const always means the object lives in true read-only memory (ROM). In reality, a compiler is free to place a const object in genuinely read-only memory, in ordinary writable memory, or to optimize it away entirely and substitute its value directly wherever it's used — C guarantees the compile-time restriction, not any particular physical placement.
const int value = 100;Registers and Optimized-Away Variables
A compiler is completely free to keep result in a CPU register for the whole function, or even to skip creating result at all and compute a + b directly into the return value. Source-level variables don't have to correspond one-to-one with physical memory locations.
This is a useful reminder that C describes the behavior a program must exhibit, not the machine-level mechanics used to achieve it — result behaves exactly like a normal local variable from the program's point of view no matter which of these the compiler actually does underneath.
int calculate(int a, int b) {
int result = a + b;
return result;
}Pointer Variables vs. the Objects They Point To
x and p are two entirely separate objects, at two separate addresses. x holds the number 10; p holds the address of x. Neither one's storage overlaps the other's.
It's easy to slip into thinking of a pointer as somehow being the memory it points to, but a pointer is its own object, with its own address and its own storage duration, completely distinct from whatever it points at. p can keep existing long after x is gone, and p's own value can be copied, changed, or reassigned without touching x at all.
int x = 10;
int *p = &x;Where Does a Pointer Itself Live?
p is a pointer variable, and like any other local variable it lives on the stack. In the first case it happens to point at another stack object, x. In the second case, q is still on the stack, but the memory it points at came from malloc() and lives on the heap instead — the pointer's own storage and the storage it refers to don't have to be in the same region at all.
q, the pointer variable, is on the stack. *q, the object it points to, is on the heap. They are governed by completely different lifetime rules even though one refers directly to the other.
void function(void) {
int x = 10;
int *p = &x; // p and x both on the stack
int *q = malloc(sizeof(int)); // q is on the stack; *q is on the heap
*q = 20;
free(q);
}Arrays in Memory
C guarantees that array elements are laid out contiguously and in order — values[0] is immediately followed by values[1], then values[2], then values[3], with no gaps and no reordering.
If int is 4 bytes on this implementation, the four elements sit at four consecutive addresses. The exact addresses and int's exact size are both implementation-dependent, but the contiguous, in-order layout is guaranteed by C.
int values[4] = {10, 20, 30, 40};Structures and Padding
The members appear in memory in the order they're declared, but the compiler is allowed to insert padding bytes between them, so sizeof(struct Data) can end up larger than the simple sum of sizeof(char) + sizeof(int) + sizeof(short).
value needs to start at an address divisible by 4 on this implementation, so three padding bytes are inserted after flag to push it into alignment — bytes that hold no meaningful data of their own.
struct Data {
char flag;
int value;
short count;
};Memory Alignment
Many CPUs require, or strongly prefer, that certain types begin at addresses that are multiples of their size — a 4-byte int, for instance, commonly needs to start at an address divisible by 4. That's exactly why a char followed by an int in a struct usually needs padding bytes in between: without them, the int would start at a misaligned address.
_Alignof(type), available via <stdalign.h>, reports a type's alignment requirement directly, so code can reason about padding and alignment without guessing. As ever, the exact numbers are implementation-dependent.
#include <stdalign.h>
#include <stdio.h>
int main(void) {
printf("alignment of int: %zu\n", _Alignof(int));
return 0;
}Program Startup, Before main()
By the time your main() function's first line actually runs, a surprising amount of work has already happened. On a typical hosted implementation, that work looks roughly like this:
The exact sequence and the names of the steps depend on the platform, the executable format, and the runtime, but the guarantee C does give you is firm: every object with static storage duration is properly initialized before main() is reached, so you never have to initialize globals or file-scope statics yourself at the top of main().
program image loaded into memory
|
v
runtime / loader initialization
|
v
static-storage-duration objects initialized
(data segment values copied in, BSS zeroed)
|
v
library and runtime setup
|
v
main()
|
v
normal program executionStatic Initialization Happens Automatically
Before your own code executes even one instruction, global_a is already 0, global_b is already 10, and global_c is already 20. You never write code that explicitly performs these assignments — the guarantee that static-storage-duration objects are initialized before main() takes care of it, unlike an automatic local variable, which you always have to initialize yourself if you want a defined value.
int global_a;
int global_b = 10;
static int global_c = 20;Automatic Variables Are Not Initialized For You
x's value is indeterminate until something assigns to it — it is not guaranteed to be zero, or any other particular value. y, by contrast, is explicitly given the value 0 right in its declaration.
Reading x before assigning it a value is undefined behavior, not simply "probably garbage." Contrast this with static int x; at file or block scope, which is guaranteed to be zero-initialized precisely because it has static storage duration rather than automatic storage duration — the two kinds of local variable behave completely differently here.
void function(void) {
int x; // indeterminate value
int y = 0; // explicitly initialized
}A Complete Example: Every Region in One Program
Every kind of storage duration this page has discussed shows up in one small program: global_value (data), static_value (BSS), local_value (stack, automatic), persistent_value (a static local, surviving across the two calls to function()), and dynamic_value's target (heap, freed before the function returns).
This is a simplified conceptual view — a snapshot of where each variable lives, not the precise layout any real implementation would use.
#include <stdio.h>
#include <stdlib.h>
int global_value = 42; /* initialized static storage duration -> data */
static int static_value; /* uninitialized static storage duration -> BSS */
void function(void) {
int local_value = 7; /* automatic storage duration -> stack */
static int persistent_value = 0; /* static storage duration -> data */
int *dynamic_value = malloc(sizeof(int)); /* allocated storage duration -> heap */
persistent_value++;
if (dynamic_value != NULL) {
*dynamic_value = local_value * persistent_value;
printf("local=%d persistent=%d dynamic=%d\n",
local_value, persistent_value, *dynamic_value);
free(dynamic_value);
}
}
int main(void) {
printf("global_value=%d static_value=%d\n", global_value, static_value);
function();
function();
return 0;
}Storage Duration: The Real Categories
It helps to keep the actual categories the C standard defines separate from the conventional regions they usually map onto:
The word "typical" matters in every row of this table: C defines the semantics of each storage duration precisely, but it never mandates the physical region an implementation must use to provide them.
Storage duration Typical conventional home
----------------- --------------------------------------------
Automatic stack, a CPU register, or optimized away
Static data, BSS, read-only data, or other static
storage the implementation sets aside
Allocated heap, or another dynamic-allocation area
Thread thread-local storageStorage Duration Is Not the Same as Scope
Scope answers the question "from where in the source code can I refer to this identifier?" Storage duration answers a completely different question: "how long does the underlying object actually exist?" It's easy to conflate them because for an ordinary local variable, the two happen to line up — but they don't always.
count can only be named from inside counter() — that's its scope. But it exists for the entire run of the program, from before main() starts to after the program ends — that's its storage duration. A single declaration gives it a narrow scope and a wide-open lifetime at the same time.
void counter(void) {
static int count = 0; // block scope, static storage duration
count++;
}Storage Duration Is Not the Same as Linkage
At file scope, this one declaration sets three genuinely separate properties at once.
counter has file scope (it can be referred to from anywhere later in this same file), static storage duration (it exists for the whole program run), and internal linkage (other translation units cannot refer to this same object, even by the same name). Linkage specifically answers "can the same identifier in a different file refer to this same entity?" — a question that scope and storage duration don't address at all.
static int counter;Memory Leaks
The heap allocation malloc() returned still exists — nothing has freed it — but the only pointer that knew its address has just been overwritten. There is no longer any way for the program to reach that memory or free it.
This is a memory leak: allocated storage that is still "alive" as far as the allocator is concerned, but permanently unreachable by the program. A single leak like this is harmless, but the same pattern repeated in a loop or over a long-running program's lifetime can slowly exhaust available memory.
int *p = malloc(sizeof(int));
p = NULL; // the allocated int is now unreachableUse-After-Free
Dereferencing p after free(p) has already ended that object's lifetime is undefined behavior, regardless of whether the printed value happens to look reasonable. The allocator is free to reuse that exact memory for a completely unrelated allocation the moment it's freed, so a use-after-free can appear to work by coincidence and then fail unpredictably later.
int *p = malloc(sizeof(int));
*p = 10;
free(p);
printf("%d\n", *p); // undefined behavior: use-after-freeBuffer Overflow
values only has valid indexes 0 through 4; values[5] reaches one element past the end of the array into memory the program never allocated to it. Depending on what happens to sit there, this can silently corrupt an unrelated variable, corrupt bookkeeping the allocator or the stack relies on, or crash the program outright — and precisely because it writes into memory that legitimately belongs to something else, it's exactly this page's picture of memory regions and object boundaries that explains why it's so dangerous.
int values[5];
values[5] = 100; // out of bounds: valid indexes are 0 through 4Memory Protection and Page Permissions
Modern operating systems commonly assign different access permissions to different regions of a process's memory, enforced by the hardware itself rather than by the C language.
A well-secured system generally avoids ever marking the same page both writable and executable at once — that combination is exactly what many code-injection attacks rely on, so keeping write and execute permissions mutually exclusive closes off a whole category of exploits.
Region Typical permissions
------------------ --------------------
Text (code) Read + Execute
Read-only data Read
Writable data / BSS Read + Write
Heap Read + Write
Stack Read + WriteVirtual Memory
The addresses your program works with — the ones that show up when you print a pointer with %p — are generally virtual addresses, not physical RAM locations. The processor's memory management unit (MMU), working together with the operating system's page tables, translates each virtual address into wherever the corresponding data actually sits in physical memory.
This translation layer is what lets every process behave as though it has its own private, contiguous address space, even though many processes are really sharing the same physical RAM, each mapped into its own separate view of that memory.
Pages
Virtual memory is commonly divided into fixed-size chunks called pages — 4096 bytes is a common size, though the exact figure varies by architecture and operating system. Memory permissions, like the read/write/execute settings from a few sections up, are typically applied at this same page granularity: an entire page of code memory is marked read-and-execute, an entire page of heap memory is marked read-and-write, and so on.
Address Space Is Not the Same as RAM Usage
A process can be given an enormous virtual address space — many terabytes on a modern 64-bit system — without anywhere near that much physical RAM actually being used at any given moment. Virtual memory is a promise about addresses, not a reservation of physical RAM.
Several OS-level mechanisms sit underneath this: demand paging only brings a page of memory into physical RAM the first time it's actually touched; identical pages can be shared between processes instead of duplicated; memory-mapped files let file data be addressed without being fully loaded up front; copy-on-write lets two processes share the same physical pages until one of them writes and forces a private copy; and pages that haven't been used recently can be swapped out to disk and paged back in later. All of this happens beneath the C program's memory model — from inside the program, memory just appears to be there when you use it.
Memory-Mapped Files
Operating systems commonly offer a way to map a file's contents directly into a process's virtual address space, so the program can read (and sometimes write) that data through ordinary memory addresses instead of making explicit read() and write() calls. Touching a mapped address can transparently pull the corresponding bytes in from disk behind the scenes.
This is a platform-level facility built on top of the same virtual memory and paging machinery described above — it isn't something standard C itself provides or guarantees, but it's a natural extension of the same address-translation idea once you understand pages and virtual memory.
Memory Layout on Embedded Systems
The traditional text/data/BSS/heap/stack picture assumes an environment with a loader, an operating system, and typically virtual memory underneath it — none of which is guaranteed on smaller embedded systems. A microcontroller might instead have two very different kinds of physical memory to work with: flash and RAM.
Because RAM loses its contents when power is removed, any writable data still needs an initial value stored somewhere permanent — so its starting image lives in flash and gets copied into RAM during startup, before that data is ever touched by running code. The exact arrangement depends heavily on the specific microcontroller, its linker script, its startup code, and its toolchain.
Flash (non-volatile) RAM (volatile)
--------------------------- --------------------
Interrupt / vector table Data (copied from flash at startup)
Program code (text) BSS (zeroed at startup)
Read-only data Heap
Initial image of writable data StackEmbedded Startup: Copying Data, Zeroing BSS
This is exactly the same static-initialization guarantee C makes everywhere else — data ends up with your specified initial values, and BSS ends up zeroed, before main() runs — but on an embedded target, the startup code has to perform that copying and zeroing explicitly itself, since there's no operating system loader doing it beforehand.
reset / power-on
|
v
copy initialized-data image from flash to its RAM address
|
v
zero the BSS region in RAM
|
v
other platform / runtime initialization
|
v
main()Why Memory Layout Matters
Nearly every genuinely confusing memory bug in C traces back to one of the ideas on this page. A local variable disappears the moment its function returns because that's exactly what automatic storage duration ending means. malloc()'d memory survives a function returning because it has an entirely different lifetime, tied to an explicit free() rather than to any particular block of code. A pointer can go dangling because the object it points to can cease to exist while the pointer itself keeps holding the same stale address. A string literal can't be modified because C forbids it and implementations commonly place literals in memory that reinforces that rule physically. And a buffer overflow can corrupt data that looks completely unrelated to the buffer that overflowed, because every object and every region occupies a specific, finite range of addresses, and writing past the end of one simply walks into whatever happens to sit next door.
Common Memory Errors at a Glance
Every one of these already showed up earlier on this page in its own specific example — this table is just a single place to see the whole family side by side. Most of them share the same consequence under the C standard: undefined behavior, meaning the program is no longer guaranteed to do anything sensible at all.
Buffer overflow access beyond an object's own bounds
Use-after-free accessing an object after its lifetime has ended
Dangling pointer a pointer that still refers to an object that is gone
Memory leak allocated storage that becomes permanently unreachable
Uninitialized read reading a value that was never actually assigned
Double free releasing the same allocation more than once
Stack overflow exhausting the stack's available space
Invalid pointer access dereferencing a pointer that was never valid to begin withThe Memory Map, Revisited
With every region now covered individually, it's worth looking at the same layered picture from the very start of this page one more time, this time reading it as a single mental model rather than a list of separate topics:
It's worth repeating one last time exactly what this diagram is and isn't: an enormously useful, enormously common conventional implementation model — not the C language's own memory model, which only ever talks about objects, storage duration, lifetime, scope, and linkage.
STACK function calls, automatic objects (grows downward)
| (free space)
v
^ (free space)
|
HEAP allocated storage (grows upward)
BSS zero-initialized static objects
DATA initialized static objects
READ-ONLY DATA string literals, constant data
TEXT executable instructionsQuick Reference by Region
As a closing reminder: this table names the regions and lifetimes in the conventional sense used throughout this page. The exact regions that exist, and exactly what gets placed where, are always implementation-dependent — but this is the shared vocabulary nearly every C programmer, compiler, and operating system uses to talk about it.
Why doesn't the C standard actually require that memory be organized into exactly a text, data, BSS, heap, and stack layout?
C only defines objects, storage duration, lifetime, scope, and linkage as language-level concepts. The text/data/BSS/heap/stack picture is a convention used by operating systems, compilers, and executable formats to implement those semantics — an extremely common one, but not something the standard mandates by name.
Given int global_count = 100; and int global_count; declared separately, why would the two typically end up in different regions of memory?
Both have static storage duration, but only the first has an explicit nonzero initializer, so it commonly goes into the initialized data segment. The second has no initializer and is guaranteed to start at zero, so it commonly goes into BSS instead, which can represent zeroed storage without needing to store the zero bytes explicitly.
Why is p[0] = 'J'; undefined behavior after char *p = "Hello";, while text[0] = 'J'; is perfectly fine after char text[] = "Hello";?
char *p = "Hello"; makes p point directly at the string literal's own storage, which has static storage duration and is not modifiable. char text[] = "Hello"; instead copies the literal's characters into a brand-new, ordinary array belonging to text, which is fully writable like any other array.
A function returns &x, where x is declared as an ordinary local variable inside that function. Why is using the returned pointer afterward unsafe?
x has automatic storage duration tied to the function's own stack frame, and that storage duration ends the moment the function returns. The returned pointer still holds x's old address, but nothing guarantees that address still holds x's value, or even still belongs to this program's data at all.
Why can a process be given a virtual address space many times larger than the computer's actual physical RAM without running out of memory immediately?
Virtual addresses are just a promise about address space, not a reservation of physical RAM. Mechanisms like demand paging only bring a page into physical memory the first time it's actually touched, and pages can be shared between processes or swapped out to disk, so far less physical RAM is used at any moment than the address space alone would suggest.
Write a C program that declares a global initialized variable and a global uninitialized variable, then prints both of their values from main() before assigning anything else to them.
global_initialized carries the explicit value 100 that was baked into the program's data segment, while global_uninitialized has no initializer and is guaranteed by C to start at zero because it has static storage duration — conventionally represented in the BSS segment rather than the data segment, since BSS can record 'reserve this much zeroed space' without storing explicit zero bytes.
Write a C program that calls the same function twice and prints the address of a local variable each time, to show that two separate calls can reuse the same stack-frame location.
Each call to show_address() gets its own fresh stack frame, but because the previous call's frame was already torn down before the next call started, the new frame is very often placed at exactly the same address as before — which is why local_value can print the same address on both calls, even though it is logically a brand-new object each time.
Write a C program with three functions, first(), second(), and third(), where first() calls second() and second() calls third(), printing a message on entry to and just before returning from each function, to trace how their stack frames stack up and unwind.
The printed order shows the frames stacking up in call order (first, then second, then third all report entering before any of them reports leaving) and then unwinding in exactly the reverse order (third leaves, then second, then first) — the same last-in-first-out behavior that gives the call stack its name.
Write a C program that dynamically allocates memory for an integer with malloc(), checks the result for failure, stores a value in it, prints it, and then frees it.
malloc() hands back the address of a new int with allocated storage duration living on the heap, separate from p's own storage on the stack; checking for NULL before use guards against allocation failure, and free(p) releases the heap object once it is no longer needed, ending its lifetime.
Write a C program that demonstrates the dangling-pointer defensive pattern: allocate memory, free it, set the pointer to NULL, and then check for NULL before any further use.
free(p) ends the allocated object's lifetime but leaves p still holding that same stale address, which is what makes p a dangling pointer; immediately setting p = NULL; doesn't restore anything, but it turns any later accidental dereference into a checkable NULL condition instead of undefined behavior from reusing a stale address.
Write a C program with a corrected version of a function that would otherwise incorrectly return the address of a local variable, using a caller-supplied output parameter instead.
The unsafe version returns the address of a local variable whose automatic storage duration ends the instant the function returns, leaving the caller with a pointer to memory that is no longer guaranteed to hold anything meaningful. The fixed version has the caller supply its own storage (value, declared in main()) and simply writes into it through a pointer parameter, so nothing is ever returned by the address of an object whose lifetime has already ended.
Write a C program with a function containing a static local variable that keeps a running count of how many times the function has been called, and call it several times from main().
call_count is initialized to 0 exactly once, the very first time the program reaches that declaration, because it has static storage duration rather than automatic storage duration; every later call to track_calls() finds call_count already holding whatever value the previous call left it at, which is what lets it accumulate a running total across separate calls.
Write a C program that declares a struct with a char, an int, and a short member (in that order), then prints sizeof of the struct alongside the sum of the individual members' sizes to reveal any padding.
On most common implementations sizeof(struct Data) comes out larger than the plain sum of its members' individual sizes, because the compiler inserts padding bytes so that value (an int) starts at a properly aligned address — alignment requirements, not any rule about member order, are what create the extra size.
Write a C program that allocates memory for several integers in a loop and frees every one of them before the program ends, with a comment noting what would happen to that memory if the free() call were left out.
Each loop iteration allocates one heap object and stores its address in the pointers array; freeing every one of those addresses in the final loop releases all five allocations back to the allocator. If any free() call were skipped, that particular allocation would remain reserved forever with no pointer left able to reach it once pointers[i] goes out of scope or is reused — exactly what a memory leak is.
Write a C program that uses recursion with a clear base case to print the current depth on the way down and again on the way back up, illustrating how each recursive call gets its own stack frame that is later unwound.
Each call to recurse() gets its own stack frame holding that call's own depth parameter, so the 'entering' messages print in increasing order as frames stack up toward the base case, and the 'leaving' messages then print in decreasing order as each frame is unwound in turn — the same call-stack behavior explored earlier with first(), second(), and third(), just self-referential and bounded by max_depth instead of unbounded, which is exactly the kind of bound a real recursive function needs to avoid a stack overflow.
Region Holds Lifetime
------------------ ---------------------------------- ------------------------
Text executable instructions program lifetime
Read-only data string literals, constant data program lifetime
Data initialized static objects program lifetime
BSS zero / uninitialized static objects program lifetime
Heap dynamically allocated objects until explicitly freed
Stack function-call / automatic storage block / function execution
Thread-local storage thread-local objects thread lifetime