THE Stack vs. your stacks

An interactive tour of process memory on x86-64 Linux โ€” with real-looking addresses. All addresses below are typical values you would see in gdb (ASLR changes them each run, but the shape is always this).

0Your three questions, answered first

๐Ÿ’ญ "I can write many stack data structures in Cโ€ฆ so are there many stacks in memory?"

Yes and no. A Stack you write with malloc is just bytes in the heap โ€” you can make 1000 of them. But THE stack is one special region the kernel creates per thread, and the CPU itself helps manage it: the RSP register and the push / pop / call / ret instructions are hardware support for exactly one stack at a time. See section 4.

๐Ÿ’ญ "How will my multi-threaded app look?"

One stack per thread. Thread stacks are just mmap'd blocks in the middle of the address space. All threads share the heap, globals and code โ€” each thread only gets a private stack + private registers. See section 3.

๐Ÿ’ญ "How can ONE stack hold all my functions and all my data?!"

It doesn't โ€” that's the trick. Functions live in the code segment, not the stack. The stack only holds the frames of calls that haven't returned yet โ€” usually a chain of 5โ€“50 frames, a few KB. Every ret frees a frame instantly, and the same bytes get reused by the next call. Big data goes to the heap. Watch it happen in section 2.

1The whole picture: one process's virtual address space

This is the "ultimate shape" you asked for. Hover / tap each segment. High addresses on top, like in your slide.

0xFFFFFFFFFFFFFFFF
๐Ÿšซ Kernel spaceyour code can never touch this
0xFFFF800000000000+
0x00007FFFFFFFFFFF  โ† top of user space (~128 TiB)
โ–ผ Main thread stack (8 MiB) argv & envp sit at the very top; grows DOWN
0x00007FFFFFFFF000 0x00007FFFFFFDE000
๐Ÿ“š mmap region shared libs (libc.so, ld.so), mmap'd filesโ€ฆ and thread stacks!
โ‰ˆ 0x00007FFFF7D8A000 (libc.so lands here)
โ€ฆan astronomically huge unmapped gapโ€ฆ~127 TiB of nothing โ€” it's virtual, it costs zero RAM
โ–ฒ Heap malloc / free live here; grows UP
0x000055555555A000 โ†’
๐ŸŒ Globals โ€” .data & .bss global / static variables
0x0000555555558000
โš™๏ธ Code โ€” .text the machine instructions of main, add, printfโ€ฆ functions LIVE here
0x0000555555554000
unmapped โ€” this is why NULL dereference crashes
0x0000000000000000

๐Ÿ‘† Hover a segment

  • Every process gets its own private address space like this โ€” your program thinks it owns all 128 TiB.
  • It's virtual: the 8 MiB stack is only a reservation; physical RAM pages appear the first time you touch them.
  • ASLR slides the segments to random bases each run โ€” the order never changes.

2Watch ONE stack run a whole program

main() calls square_sum(3,4) which calls add(3,4). Step through and watch frames get pushed, used, and recycled. Keys: โ† โ†’

C source

x86-64 assembly (gcc -O0 style)

THE stack  ยท  grows downward โฌ‡ (each row = 8 bytes)

libc main frame square_sum frame add frame RSP (top of stack) RBP (frame base)

CPU registers

2ยฝSo what if the calls never return? โ†’ stack overflow

The stack survives because frames are constantly freed by ret. Break that contract with infinite recursion and 8 MiB fills up:

void boom(int n) { boom(n + 1); }  // no base case, ~48 bytes per frame
depth = 0 calls  ยท  stack used = 0 KiB / 8192 KiB
SIGSEGV โ€” Segmentation fault (stack overflow)
RSP walked past the bottom of the stack region and touched the guard page โ€” an intentionally unmapped page the kernel places below every stack. The CPU faulted, the kernel killed the process. ~174,000 frames deep. Normal programs are 10โ€“50 frames deep โ€” that's why 8 MiB is plenty.

3Multi-threading: one stack per thread, everything else shared

Click pthread_create() and watch new stacks appear in the mmap region. Each thread runs a different call chain at the same time โ€” that's only possible because each has a private RSP.

threads: 1 (just main)
Main thread stack (8 MiB)
RSP = 0x00007FFFFFFFDF38 ยท running: main โ†’ square_sum โ†’ add
0x00007FFFFFFFF000 0x00007FFFFFFDE000
๐Ÿ“š shared libraries (libc.so โ€ฆ)
โ‰ˆ 0x00007FFFF7D8A000
โ€ฆhuge unmapped gapโ€ฆ
๐Ÿค Heap โ€” SHARED by all threads
0x000055555555A000
๐Ÿค Globals (.data/.bss) โ€” SHARED
0x0000555555558000
๐Ÿค Code (.text) โ€” SHARED (all threads run the same functions)
0x0000555555554000

What this means

  • Private per thread: stack + registers (RSP, RIP, โ€ฆ). So local variables are naturally thread-safe.
  • Shared: heap, globals, code. So two threads touching one global/heap object need a mutex.
  • Thread stacks are created by pthread_create with plain mmap() โ€” they're ordinary memory, just used as a stack because the new thread's RSP points into them.
  • Each stack has a guard page below it, so overflowing one thread's stack faults instead of silently smashing its neighbor.

4Your Stack struct vs. THE stack

The word "stack" is overloaded โ€” this is the root of your confusion. Both of these exist at the same time, in different places:

your C code

typedef struct {
long data[8];
int top;
} Stack;
Stack *s1 = malloc(sizeof *s1); // heap!
Stack *s2 = malloc(sizeof *s2); // heap!
// make 1000 more if you want โ€” they are
// just bytes YOU interpret as a stack.
// push() = data[++top] = v (your code)
// push = CPU instruction (THE stack)

s1 โ†’ lives in the HEAP at 0x000055555555A2A0

s2 โ†’ another one, also HEAP, at 0x000055555555A310

7+0x00
99+0x08
as many of these as malloc allows โ€” millions

vs. THE stack

Created by the kernel (not by you) ยท one per thread ยท managed by the CPU's RSP register and push/pop/call/ret instructions ยท used automatically for every function call, local variable, and return address. You never malloc it and never free it.

5Recap โ€” the mental model to keep

๐Ÿง  Functions don't live on the stack

Machine code sits in .text forever. A function only borrows stack space (a frame) while a call to it is in flight, and gives it back at ret. 10,000 functions in your app โ‰  10,000 frames โ€” only the current call chain exists.

โ™ป๏ธ The stack is a recycler, not a warehouse

You saw the same addresses (0x7FFFFFFFDF38โ€ฆ) reused by add's dead frame, then overwritten later. That reuse is why one small region handles millions of calls โ€” and why reading uninitialized locals gives garbage.

๐Ÿ—บ๏ธ The ultimate shape

Per process: code + globals + one heap + mmap area. Per thread: one stack + one register set. Your 1000 Stack structs? All just heap bytes. THE stack is the one with hardware and kernel support.