Dataslope logoDataslope

Process Memory Layout

Text, data, BSS, heap, and stack — where every variable in a C program lives

A running C program is not just "code." Its address space is partitioned into several regions, each with different rules about who can write, how it grows, and when its memory is reclaimed. Understanding the map is the foundation for understanding malloc, the stack, and every memory bug class.

The classic four-segment map

On a typical Unix-style process the layout looks like this:

RegionWhat lives hereLifetimeWritable?
Text (code)Compiled machine instructions, often string literalsprogram lifetimeno
DataGlobals / static with non-zero initializersprogram lifetimeyes
BSSGlobals / static initialized to zeroprogram lifetimeyes
Heapmalloc / calloc / realloc allocationsuntil freeyes
StackFunction parameters and local variablesfunction call lifetimeyes

The WASI runtime uses a similar layout in linear memory; the addresses just happen to be inside the WebAssembly module's memory rather than your laptop's physical RAM.

Seeing the regions

You can roughly identify which region a variable lives in by printing its address.

Code Block
C 17 (201710L)

The exact addresses will differ run to run (modern OSes randomize them with ASLR), but you should see a rough ordering: code and literals at the lowest addresses, then globals, then the heap allocation somewhere higher, and the stack local at the highest address of all.

Text and read-only data

The text segment holds the compiled instructions of every function in your program. It is typically mapped read-only — the CPU will fault if you try to write to it. Many systems also put string literals and other const data in a read-only segment called .rodata.

This is why writing through char *s = "hello"; s[0] = 'X'; is undefined behavior: the literal "hello" lives in .rodata, and the OS will not let you modify it.

Data and BSS — the globals

C splits globals into two pieces purely as a file-size optimization:

  • .data holds globals with explicit non-zero initializers. Those initial bytes have to be stored in the executable file.
  • .bss ("Block Started by Symbol") holds globals initialized to zero. They take no space in the executable file — the OS just zeroes a region of memory at load time.
Code Block
C 17 (201710L)

A common surprise: declaring a 10-million-element zero-initialized global does not make your binary 40 MB larger, because BSS is "described by metadata" rather than stored as bytes.

Storage classes for variables

C has several ways to control where a variable lives:

DeclarationWhere it livesLifetime
int x; inside a functionStackuntil function returns
int x; at file scopeBSS (or .data if initialized)program
static int x; inside a functionBSS / .dataprogram (but only visible to that function)
static int x; at file scopeLike a global, but only visible to that fileprogram
int *p = malloc(...);Heap (the bytes) + wherever p is declared (the pointer)until free
register int x;Hint to keep in a CPU registeruntil function returns

static inside a function is a great way to give a function its own private "memory across calls":

Code Block
C 17 (201710L)

The counter is not on the stack — if it were, it would be reset to zero on every call.

Stack and heap: the two dynamic regions

The two regions you will think about most are the stack and the heap. They are also the two that grow and shrink at runtime.

StackHeap
Allocates withfunction call (push frame)malloc/calloc/realloc
Frees withreturn (pop frame)free
Speedvery fast (just move a pointer)slower (bookkeeping)
Size limitsmall (usually a few MB)large (gigabytes)
Lives across function calls?noyes
Risk classstack overflow, dangling pointer to localleaks, double-free, use-after-free

The next two pages explore the stack and heap in detail. Before then, one critical rule:

Never return the address of a local variable

A local variable lives in your stack frame. When you return, the frame is gone. A pointer to it now refers to whatever the next function call uses — almost always not what you wanted.

Code Block
C 17 (201710L)

The correct fix is to either (a) allocate on the heap and let the caller free it, or (b) have the caller pass in a buffer.

Practice: place variables in the right region

Challenge
C 17 (201710L)
Make next_id persist across calls

Implement int next_id(void) so that calling it five times in a row returns 1 2 3 4 5 (one per line). Use a function-local variable, not a global. Hint: static changes the storage class.

Test your understanding

QuestionSelect one

In which segment does a large zero-initialized global like int big[100000]; live, and what does that mean for the executable file size?

.data — the binary grows by ~400 KB to store the zeros.

.bss — only metadata is stored; the OS zeroes the region at load time, so the binary stays small.

The heap — malloc is called implicitly at program start.

The stack — the runtime pushes the array on entry to main.

QuestionSelect one

Why is it dangerous to return &local_variable from a function?

Because the address may be the same as another local on the heap.

The local lives in the function's stack frame, which is destroyed when the function returns; the returned address is now dangling.

Because the compiler aliases it to NULL automatically.

Because the OS swaps the variable out of memory to disk.

QuestionSelect one

What changes if you add static to a variable declared inside a function, like static int counter = 0;?

The variable becomes shared across threads.

It becomes const and cannot be modified.

Its storage class changes from automatic (stack) to static (BSS/.data); it is initialized once and keeps its value across calls.

The variable is moved to the heap.

On this page