~ $ cat where-your-program-lives-once-you-hit-run.md

Where your program lives once you hit run

June 27, 2026 10 min read systems · memory · fundamentals

A program sitting on your disk isn’t running. It’s a file — a few kilobytes of machine instructions and constants, doing nothing at all. Typing its name into a shell, or double-clicking it, starts something the operating system does thousands of times a day: the kernel reads that file and builds a process around it.

Most of the confusion about stacks and heaps and “where does this variable actually go” comes from never seeing the map of that process. So let’s draw it. The whole thing fits in your head if you take it one region at a time.

Start with one fact that makes the rest click: a running program has two descriptions, kept in two different places. Your code sees an address space — a map of where its own bytes live. The kernel sees a PCB — a record of facts about the process. Same process, two points of view. Almost every “wait, where does that live?” question is really asking which of the two you mean.

We’ll build both, starting with the one your code can feel.

The illusion every process runs on

Every process gets told the same lie, and the OS never stops telling it: you have all the memory, it’s all yours, and it’s one clean stretch starting at address zero.

It isn’t true. Your laptop has maybe 16 GB of real RAM, shared between a browser, a music app, forty background daemons, and the kernel itself. But each process gets its own virtual address space — a private numbering of memory that behaves as if the process owns the whole machine. Process A’s address 0x6000 and process B’s address 0x6000 are different bytes of real RAM, and neither process can see the other’s. On every memory access, a chip called the MMU plus a kernel lookup table translate these private addresses to real ones — millions of times a second.

You almost never think about this, which is the point. But it’s why the map we’re about to draw is per-process. “The stack is up here, the heap is down there” describes one process’s private map. Every process gets its own copy, and each believes it’s alone.

One process, its private address space. Low addresses at the bottom, high at the top. Here’s the map.

   high addresses
  ┌────────────────────┐
  │       stack        │  local variables, call frames
  │         │          │  grows DOWN ↓ as you call functions
  │         ▼          │
  ├────────────────────┤
  │                    │
  │    (big gap)       │  unused space + mmap'd libraries
  │                    │
  ├────────────────────┤
  │         ▲          │
  │         │          │  grows UP ↑ as you malloc / new
  │        heap        │  dynamic allocations you manage
  ├────────────────────┤
  │     bss + data     │  global / static variables
  ├────────────────────┤
  │       text         │  the program's machine code (read-only)
  └────────────────────┘
   low addresses

Four regions, bottom to top: text, data, heap, stack. The heap and stack grow toward each other across a big gap in the middle — we’ll get to why. Let’s walk them in order.

Text — the code itself

At the bottom is the text segment. The name is misleading: it holds no readable text. It’s the compiled machine instructions — the actual code the CPU runs. Your for loops, your functions, the body of main: after compiling, all of it lives here as raw opcodes.

Two things make this region special, both on purpose.

It’s read-only. A program shouldn’t rewrite its own instructions mid-run, so a stray pointer that tries to scribble over the code gets stopped by the hardware instead of quietly corrupting the program. That’s part of why some bugs crash cleanly instead of doing something worse.

It’s shareable. Open the same app five times and you get five processes running identical code. Since the text segment never changes, the kernel keeps one physical copy in RAM and maps it into all five address spaces. Five private worlds, one shared set of instructions underneath.

Data and BSS — the globals

Just above the code are your global and static variables — the ones that live for the whole run, not just one function call. This area splits in two, and the split is smarter than it looks.

Data holds globals that start with a value: int count = 7; at file scope. The 7 has to come from somewhere, so it’s written into the program file on disk and copied into memory when the program loads.

BSS holds globals that start at zero, or aren’t initialized at all: int total; or static char buffer[4096];. Here’s the trick: a million zeroed bytes don’t need a million zeros stored in your binary. The file just records “BSS needs 4096 bytes here,” and the kernel zeroes that span when it sets up the process. A big zero-filled array costs almost nothing on disk and the full amount in RAM. (BSS is an old assembler abbreviation. The name means nothing useful now — just read it as “the zeros.”)

Both regions are fixed in size the moment the program loads. They don’t grow. For memory that grows, you need the next two — and that’s where most real bugs live.

Heap — the memory you ask for and own

Sometimes you don’t know at compile time how much memory you’ll need. You’re reading a file of unknown length, building a list as requests arrive, loading an image someone just dropped on the window. So you ask for memory at runtime. That request is served from the heap, which grows upward, toward higher addresses, as you ask for more.

In C that’s malloc; in C++ new; in Python, JavaScript, Go, or Java it’s hidden behind every object you create. Underneath, the language’s allocator keeps a pool of heap memory. When it runs low, it asks the kernel to extend the region — historically brk/sbrk, which pushes the top of the heap higher, or mmap for larger chunks. Then it hands you a pointer into that pool.

The defining feature of the heap is that its lifetime is yours to manage. A heap allocation lives until something explicitly releases it — you call free, or a garbage collector proves nothing references it anymore. That freedom is exactly why the famous bugs come from here:

  • A leak: you allocate, lose the pointer, and never free. The memory is still “in use” as far as the kernel knows, but you can’t reach it. Do it in a loop and you slowly eat the machine.
  • A use-after-free: you free memory but keep using the old pointer, which now points at space the allocator may have handed to something else.
  • Fragmentation: after enough allocate/free churn, free space exists but in pieces too small and scattered to satisfy one big request.

None of these can happen to the next region, because its lifetimes aren’t yours to get wrong.

Stack — the memory that manages itself

At the top of the address space is the stack, and it grows downward, toward the heap rising to meet it. The stack handles the bookkeeping of function calls, and it does it automatically.

Every time you call a function, the program pushes a new stack frame: a small block holding that call’s local variables, its arguments, and — most importantly — the return address, the spot in the code to jump back to when the function finishes. Call a function from inside a function from inside a function, and the frames stack up, one per active call, newest on top. When a function returns, its frame is popped — instantly, just by moving one pointer back up — and every local variable in it stops existing.

That last part is the whole reason “return a pointer to a local variable” is a classic mistake. The thing it points at was on a frame that just got popped. The memory is physically still there for a moment, but it no longer belongs to you, and the next call writes right over it.

Two things fall out of this design. The stack is fast, because allocating is just moving a pointer — no searching for free space, no bookkeeping, no fragmentation. And it’s automatic, because the same pointer moving back on return frees everything at once. You never free a local variable; the shape of the call tree frees it for you.

The price is that the stack is bounded. It’s a fixed, fairly small size — often a few megabytes. Recurse too deep without a base case, or declare a giant array as a local, and the stack pointer runs past its limit and collides with the gap below. That’s a stack overflow, and the program dies on the spot. The stack trades the heap’s flexibility for speed and safety: you can’t leak it and you can’t dangle it, but you also can’t make it grow on demand.

Why the gap in the middle

Look at the map again: heap climbing up, stack coming down, a wide unused gap between them. That arrangement is deliberate. Neither region knows in advance how big it’ll get. So instead of guessing a boundary — and risking that one side runs out while the other sits half-empty — they’re placed at opposite ends and allowed to grow into the shared middle. Whoever needs the room takes it. They only fail if they actually meet, which a healthy program never does.

The gap isn’t entirely empty, either. It’s also where the kernel memory-maps things: shared libraries, so the same libc code can be shared across processes the way the text segment is, and large allocations the heap decides to request straight from the kernel instead of from its pool. It’s the breathing room of the whole layout.

That’s the address space — the world as your code sees it. But your code isn’t the only thing that needs to know about your process. The kernel does too, and it keeps a completely separate record.

The PCB — what the kernel keeps

Everything so far lives in the process’s own virtual address space. The Process Control Block does not. The PCB lives in kernel memory, where your code can’t touch it. It’s the kernel’s private record of the process — its answer to “what do I need to remember about this thing to manage it?”

It’s a single struct — Linux calls it task_struct — one per process, holding roughly:

  • The PID, the process’s identity, plus a pointer to its parent.
  • A snapshot of the CPU registers, including the program counter (which instruction is next) and the stack pointer (the current top of that stack). This is how a paused process knows where it was.
  • A pointer to the process’s page table — the lookup map that defines its address space and makes its private 0x6000 resolve to real RAM. The PCB is what owns the map we spent this whole article drawing.
  • The scheduling state — running, ready, or blocked — plus priority, so the kernel knows whether and when to give it the CPU.
  • The open file table: every file, socket, and pipe the process has open.
  • Accounting: CPU time used, memory limits, user and permissions, and more.

The PCB is also the star of a move the OS makes thousands of times a second: the context switch. When the kernel decides to stop running your process and run another, it copies the live CPU registers into your PCB, freezing exactly where you were, then loads the next process’s registers out of its PCB and resumes it as if nothing happened. For a while, your process is nothing but its record. Later the kernel reads that record back into the CPU and you pick up exactly where you left off, none the wiser.

From file to living process

Now the whole lifecycle clicks. On a Unix-like system, starting a program is famously two steps.

fork clones the calling process: same address space, same PCB contents, a new PID. It doesn’t physically copy all that memory — it shares it and only copies a page when one side writes to it. (That’s “copy-on-write,” the kernel being thrifty.) exec then takes the new process and throws its world away: it discards the old address space and builds a fresh one from a new binary on disk — new text segment, fresh data and BSS, an empty heap, a new stack with main’s frame at the top — and rewrites the PCB to match.

That’s it. The kernel read a dead file, carved out a private address space with four regions, opened a record to track it, pointed the CPU at the first instruction, and stepped back. Dead bytes became a running process.

Two views of one thing, both just bookkeeping over the same physical RAM. The address space is the story your program tells itself about where its bytes live. The PCB is the story the kernel tells about your program. Hold both in your head, and “where does this variable actually go?” stops being a mystery. It becomes a question with an address.

← all writing