A running process sees one long, flat array of memory. Where each thing lives in that array — and which direction it grows — explains almost everything about how C behaves. Let's walk the whole address space, top to bottom.
This is virtual memory — a private, contiguous map the kernel hands each process. The hardware MMU translates these addresses to scattered physical pages behind the scenes, so every process gets to believe it owns the machine. Tap any region to inspect it.
Addresses shown are illustrative. On a real system, ASLR randomises the base of the stack, heap, and libraries on every launch, so absolute values differ each run.
The split isn't arbitrary. The kernel groups memory by two properties it cares about: lifetime (does it exist for the whole program, or come and go?) and permissions (readable? writable? executable?).
Your compiled machine instructions live here. It's marked read-only and executable, so a stray pointer can't rewrite your program while it runs, and the same physical pages can be safely shared between multiple processes running the same binary.
Both hold variables that outlive any single function: globals and static locals. The difference is whether they start with a value.
int count = 42;. The value 42 is stored right in the executable file on disk.int total; or static char buf[4096];. Storing four thousand zero bytes in the file would be wasteful — so the file just records "reserve this many bytes and zero them." BSS costs nothing on disk and is cleared at load time.When you call malloc, calloc, or C++ new, you carve a chunk out of the heap. It grows upward toward higher addresses. Its defining trait: you control the lifetime. It stays alive until you free it — forget to, and you leak; free it twice or use it after freeing, and you get some of the nastiest bugs in systems programming.
Every function call gets a fresh stack frame holding its parameters, local variables, and bookkeeping. It grows downward. You never manage it directly — the frame appears on entry and vanishes on return, which is exactly why locals are called "automatic" storage. It's fast (just moving one pointer) but small and short-lived. That's the next section.
A function call is a small, precise ritual on the stack. Watch it happen: step through a real call chain and see frames get pushed on entry and popped on return. The highlighted line is what's executing.
When you look at a single frame, three kinds of things sit inside it, and the order is set by the calling convention (the System V ABI on x86-64 Linux):
call instruction. It's where execution resumes after the callee returns. Overwrite it and you hijack control flow — this is the mechanism behind classic stack-smashing exploits.rbp), stashed so it can be restored on return. This chain of saved pointers is what a debugger walks to produce a backtrace.| argument # | register (integer/pointer) | where it goes |
|---|---|---|
| 1st | rdi | register |
| 2nd | rsi | register |
| 3rd | rdx | register |
| 4th – 6th | rcx, r8, r9 | register |
| 7th onward | — | pushed onto the stack |
sub rsp, N). Freeing them all on return is a single addition. No searching, no free lists, no fragmentation — the price is that everything vanishes the instant the function returns, which is why returning a pointer to a local is a bug.A struct is not just its fields added up. The compiler inserts invisible padding so each field lands on an address that's a multiple of its size — the CPU reads aligned data faster (and on some architectures, misaligned reads simply fault). Pick a struct and watch the bytes fall into place.
#pragma pack(1) — remove all padding
Packing removes padding to save space, but every misaligned field now costs extra CPU work to read — a real tradeoff, not a free win.
Mixed and Reordered above — identical data, different size, purely because of where the padding has to go. This is why field order matters in hot data structures.A single byte is unambiguous. But a multi-byte value like a 4-byte int has to be split across four consecutive addresses — and there are two conventions for which byte goes first. That choice is endianness. Type a value and watch it land in memory both ways.
int to a file or socket on a little-endian machine, read it on a big-endian one, and the value is scrambled. That's why network protocols mandate big-endian ("network byte order") and you call htonl() / ntohl() to convert. The gold-underlined cell marks the most-significant byte in each layout.