Step 60. Memory Layout — The Map of a Running Program
Level 1 — Programming and the Computer’s Inner Workings | Difficulty ★★★★☆ | Estimated time: 4 hours
Prerequisites: Steps 56–59 complete. You’ve confirmed pointers and arrays through addresses.
- What you need: a Linux terminal, gcc, paper and a pen for drawing a map.
- Caution: today’s exercises are experiments inside your own computer, so they’re safe. Addresses change on every run — today, that itself is one of the topics.
So far we’ve looked at the slots of individual variables. Today we unfold the whole map. When a program runs, the operating system grants it a large tract of land called memory — and that land is not a free-for-all; it’s divided into districts by purpose. A district where machine code lives, a district for long-lived variables, a district functions use briefly, a district you borrow yourself.
Why does this map matter? Listen to the names of famous security attack techniques — stack overflow, heap overflow. They’re all place names on this map. Depending on which district an incident happens in, both technique and defense change. Today we print variables’ addresses and confirm those districts with our own eyes. Looking at a map and drawing a map are different things. Today, we draw.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain that a running program’s memory is divided into four regions (code/data/heap/stack)
- Know which region global, static, and local variables and malloc-borrowed slots each live in
- Print variables’ addresses and confirm each region’s position with your eyes
- State the directions in which the stack and heap grow
- Distinguish what survives even when ASLR changes the addresses (the order of the regions)
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | C language, Linux terminal, gcc -Wall (verified: Ubuntu 13.3.0, 64-bit) |
| Today’s syntax | Global, static, and local variables, malloc/free, recursive function calls, comparing addresses with %p |
| Concepts needed | The four memory regions (code/data/heap/stack), a variable’s lifetime = its region, the growth directions of stack and heap, ASLR |
2-1. The Four Districts
The memory of a running program (a process) divides broadly into four.
- Code region: where the translated machine instructions live. The contents of the executable we made are loaded here. It’s read-only, so you can’t change it carelessly.
- Data region: where variables that stay alive as long as the program lives — global variables and static variables — reside.
- Heap region: land the program borrows itself mid-run by asking "please lend me this much." It grows from low addresses toward high ones.
- Stack region: where local variables pile up neatly each time a function is called. When the function ends, it’s all cleaned up. It grows from high addresses toward low ones.
It helps to remember the heap and stack as growing toward each other.
2-2. A Variable’s ID Card — How Its Home Is Decided
Which district a variable lives in is decided by "how it was made." Global variables made outside functions and static variables marked with static live in the data region (they share the program’s fate). Local variables made inside functions live on the stack (they share the function’s fate). What you borrow with malloc lives on the heap (until I return it myself).
Lifetimes differ, and those lifetimes are divided into districts. Knowing the district is knowing the lifetime.
2-3. malloc — Borrowing Land from the Heap
A function we use for the first time today. malloc(sizeof(int)) is "lend me one int’s worth from the heap," and it returns the borrowed slot’s address. When you’re done, you return it with free(address). This manual management — borrow and return — is the heap’s defining trait: forget to return and land leaks away (a memory leak).
2-4. ASLR — Addresses That Change Every Time
Addresses changing every time you print them — we saw this in Steps 58–59. It’s due to a protection mechanism called ASLR (Address Space Layout Randomization). On every run, it randomly shuffles where the regions start, making it hard for an attacker to predict "that address." For now, it’s enough to know "the regions’ relative positions are preserved, but the starting points change every time."
3. Follow Along
Every address today changes on every run. Don’t look at the numbers — look at the groupings (which leading digits cluster together) and the order (who is higher).
3-1. Four Statuses of Variables, Printing Addresses
Input (memmap.c)
#include <stdio.h>
#include <stdlib.h>
int global_var = 100; /* global variable */
static int static_var = 200; /* static variable */
int main(void) {
int local_var = 300; /* local variable */
int *heap_var = malloc(sizeof(int)); /* a slot borrowed from the heap */
*heap_var = 400;
printf("code (main's address): %p\n", main);
printf("data (global): %p\n", &global_var);
printf("data (static): %p\n", &static_var);
printf("heap (malloc): %p\n", heap_var);
printf("stack (local): %p\n", &local_var);
free(heap_var);
return 0;
}
Compile and run (run it twice)
gcc -Wall memmap.c -o memmap
./memmap
(Run 1)
code (main's address): 0x5dfb098fe1a9
data (global): 0x5dfb09901010
data (static): 0x5dfb09901014
heap (malloc): 0x5dfb1e41a2a0
stack (local): 0x7ffefc90e31c
(Run 2)
code (main's address): 0x5a59993a71a9
data (global): 0x5a59993aa010
data (static): 0x5a59993aa014
heap (malloc): 0x5a59bfca92a0
stack (local): 0x7ffc4cb2848c
(Verified 2026-09-09. Addresses differ on every run.)
How to read it: compare the leading digits of the addresses. Code and data are neighbors starting with 0x5d... (0x5a... in run 2), and the heap is near them too. But only the stack sits far away at the much higher 0x7ff.... You can see from the addresses that the regions are genuinely divided. Global (…010) and static (…014) sit side by side, 4 bytes apart — evidence they’re neighbors in the same data region.
Compare the two runs and, though ASLR changed every address, the groupings and the order are unchanged. Code < data < heap < stack. This is the skeleton of the map that never changes.
Why: these five lines of output are today’s map. Having seen with your eyes the picture "globals/statics crowd on the low side, locals on the high-side stack" is the harvest.
3-2. The Stack Piles Up with Every Call
Input (stackgrow.c)
#include <stdio.h>
void deeper(int depth) {
int here = depth;
printf("Local variable address at depth %d: %p\n", depth, &here);
if (depth < 3) {
deeper(depth + 1);
}
}
int main(void) {
deeper(1);
return 0;
}
Compile and run
gcc -Wall stackgrow.c -o stackgrow
./stackgrow
Local variable address at depth 1: 0x7ffec80f3064
Local variable address at depth 2: 0x7ffec80f3034
Local variable address at depth 3: 0x7ffec80f3004
(Verified 2026-09-09. Addresses differ on every run.)
How to read it: each time the function calls itself (a recursive call), a new local variable is born — and they stack toward lower addresses. The endings go 64 → 34 → 04, shrinking by 0x30 (48) in hexadecimal each time. This is live footage of the stack growing "from top to bottom." When the function ends, these slots are cleaned up automatically.
Why: the stack’s growth direction is one axis of the map you need to understand the incident called stack overflow.
3-3. Lifetime Experiment — A Local Variable After Its Function Ends
Input (life.c)
#include <stdio.h>
int *make(void) {
int temp = 42;
printf("temp address inside the function: %p\n", &temp);
return &temp;
}
int main(void) {
int *p = make();
printf("Returned address: %p\n", p);
return 0;
}
Compile and run
gcc -Wall life.c -o life
./life
(compile warning)
life.c: In function 'make':
life.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr]
6 | return &temp;
| ^~~~~
(run result)
temp address inside the function: 0x7ffd85081434
Returned address: (nil)
(Verified 2026-09-09.)
How to read it: look at two things. First, the compiler warns — "you’re trying to return a local variable’s address, but that variable disappears when the function ends?" Because stack variables share the function’s fate. Second, the run result is (nil). Modern compilers (verified on gcc 13) detect this mistake and flat-out change the code to return address 0 (NULL) instead. The compiler stopped you twice, so to speak.
The returned address ends up pointing at "a spot already vacated." A pointer pointing at a vanished spot is a famous kind of bug, and a flagship example of how "looks like it works" differs from "is correct."
Why: a region is a lifetime. Stack = dies with the function, data = lives with the program, heap = until I return it myself. This trichotomy is a variable’s ID card.
3-4. The Data Region Lives Until the Program Ends
An experiment to confirm a global variable’s lifetime.
Input (global_life.c)
#include <stdio.h>
int counter = 0;
void visit(void) {
counter++;
printf("Visit number %d, counter address %p\n", counter, &counter);
}
int main(void) {
visit();
visit();
visit();
return 0;
}
Compile and run
gcc -Wall global_life.c -o global_life
./global_life
Visit number 1, counter address 0x618ad75a9014
Visit number 2, counter address 0x618ad75a9014
Visit number 3, counter address 0x618ad75a9014
(Verified 2026-09-09. Addresses differ on every run.)
How to read it: even though the function is called three times, counter’s address is the same, and its value keeps surviving and growing. A data-region variable does not share a function’s fate — it lives with the whole program. Had it been a local variable, it would have started over from 0 in a fresh slot every time. This contrast is live footage of "region = lifetime."
Predict: what happens to the output if you move counter inside visit as a local variable? And what changes again if you attach
staticto that local variable? Run both experiments and your understanding of the data region is complete.
3-5. The Heap’s Growth Direction — Borrow Twice
Input (malloc2.c)
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *a = malloc(sizeof(int));
int *b = malloc(sizeof(int));
printf("First heap slot: %p\n", a);
printf("Second heap slot: %p\n", b);
free(a);
free(b);
return 0;
}
Compile and run
gcc -Wall malloc2.c -o malloc2
./malloc2
First heap slot: 0x5e4cb59202a0
Second heap slot: 0x5e4cb59202c0
(Verified 2026-09-09. Addresses differ on every run.)
How to read it: the second slot (…2c0) is at a higher address than the first (…2a0). Live footage of the heap growing "upward from the low side." The opposite direction from the stack (3-2, growing downward) — the picture of the two growing toward each other is confirmed by measurement.
3-6. Drawing the Map — Organizing by Hand
Draw one tall rectangle on paper and fill it from top to bottom like this.
High addresses ┌──────────┐
│ stack │ (grows downward)
│ ↓ │
│ ... │
│ ↑ │
│ heap │ (grows upward)
│ data │
Low addresses │ code │
└──────────┘
How to read it: this picture matches what you saw in 3-1’s output — the stack (0x7ff…) was the highest, code/data (0x5d…) were low. Also engrave the way the heap (3-5, grows upward) and stack (3-2, grows downward) face each other. This one picture is the summary of everything today and the wallpaper behind countless topics to come.
4. Missions & Exercises
Mission — My Own Memory Map Report
- Extend memmap.c to print six addresses: the main function, a global variable, a static variable, two local variables (check the distance between them), and a malloc-borrowed slot.
- Looking at the output, list the addresses in descending order.
- Check that the order matches the 3-6 map picture, draw the map in your notebook, and write the actual addresses beside it.
- Run the same program three times and confirm that even as ASLR changes the addresses, the "order" is preserved.
- Organize the results into a report: each variable’s status (which region), its actual address, and observations of change between runs.
Exercises
Problem 1. State what lives in each of the four regions — code/data/heap/stack — and which direction each region grows.
Problem 2. Explain the sentence "a region is a lifetime" using three examples: a local variable, a global variable, and a malloc-borrowed slot.
Problem 3. Running the same program twice changed every address, but something was preserved. What changes and what is preserved, and what is the name and purpose of this mechanism that changes addresses every time?
Problem 4. Explain in terms of "the stack’s lifetime" why a function must not return the address of its own local variable, and state how the compiler reacted to this mistake in testing (two ways).
5. Model Answers & Completion Criteria
Mission Model Answer
The skeleton is memmap.c with one more local variable added:
#include <stdio.h>
#include <stdlib.h>
int global_var = 100;
static int static_var = 200;
int main(void) {
int local_a = 300;
int local_b = 400;
int *heap_var = malloc(sizeof(int));
*heap_var = 500;
printf("code (main): %p\n", main);
printf("data (global): %p\n", &global_var);
printf("data (static): %p\n", &static_var);
printf("heap (malloc): %p\n", heap_var);
printf("stack (local a): %p\n", &local_a);
printf("stack (local b): %p\n", &local_b);
free(heap_var);
return 0;
}
How to verify: ① listed in descending order, it must come out stack > heap > data > code (3-1 verification: the stack is the 0x7ff… group, the rest the 0x5d… group). ② The two local variables should be at nearby addresses within the stack. ③ If the order table survives across three runs even as addresses change every time, you’ve confirmed "structure fixed, starting points random." ④ If each variable in the report has its region name (code/data/heap/stack) written beside it, it’s complete.
Exercise Answers
Problem 1 answer. The code region holds translated machine code, the data region holds global and static variables, the heap holds malloc-borrowed slots, and the stack holds functions’ local variables. The stack grows downward from high addresses; the heap grows upward from low addresses — the two grow toward each other.
Problem 2 answer. A local variable lives on the stack and disappears with its function; global and static variables live in data and remain until the program ends (in 3-4, both address and value were preserved); a malloc-borrowed slot lives on the heap and stays until I return it with free. Where it lives is how long it lives.
Problem 3 answer. What changes is every region’s starting address; what is preserved is the regions’ grouping and order (code < data < heap < stack). This mechanism is ASLR (Address Space Layout Randomization), a protection feature meant to keep attackers from predicting target addresses in advance.
Problem 4 answer. A local variable lives on the stack, and when the function ends, its slot is cleaned up. An address you return merely points at "a spot already vacated." In testing, the compiler ① warned at compile time with warning: function returns address of local variable, and ② at run time went ahead and made it return (nil) (NULL) — stopping you twice, so to speak.
Completion Criteria Checklist
- [ ] I can name the four memory regions and their roles
- [ ] I can explain which region global/static/local/heap variables each live in
- [ ] I can confirm regions’ positions (high side/low side) from address output
- [ ] I verified by measurement that the stack grows downward and the heap upward
- [ ] I know the regions’ order is preserved even as ASLR changes addresses
- [ ] I can explain why returning a local variable’s address is wrong
- [ ] I completed my own memory map report
6. Common Pitfalls & Fixes
Wall 1. Addresses Change Every Time, So "What Did I Do Wrong?"
Symptom: confusion because the addresses differ on every run.
Cause: a normal protection feature called ASLR. It’s not a malfunction. In the 3-1 verification, too, run 1 (0x5d…) and run 2 (0x5a…) had completely different addresses.
Fix: look not at absolute addresses but at "groupings and order." Code/data/heap on the low side, stack on the high side — this structure is preserved every time. On a map, what matters is not the numbers but the relative positions.
Wall 2. Forgetting & When Printing a Pointer (or the Reverse)
Symptom: you try to print a local variable’s address and get a strange value.
Cause: you printed plain local_var with %p instead of &local_var, or the reverse. A pointer variable (heap_var) already holds an address as its value, so you print it as-is; a plain variable takes &.
Fix: ask "what I want to print — is it the slot this variable lives at, or the slot written inside this variable?" The former is &variable; the latter is the pointer variable itself.
Wall 3. Adding Parentheses While Printing a Function’s Address
Symptom: trying to print main’s address, you write printf("%p", main()) and something unintended happens.
Cause: adding parentheses means "run the function." Write just the name and you get the address where that function lives (the code region).
Fix: remember that a function’s name, too, is an address. The same principle as an array name being the first slot’s address. If the compiler demands a cast, wrapping it as (void*)main works.
Wall 4. Taking the "Returning a Local’s Address" Trap Lightly
Symptom: you see 3-3’s warning and shrug, "but it ran?"
Cause: the address of a vanished spot can look like it still holds a value — "for now." Until another function overwrites that spot. (In testing, the compiler went ahead and changed it to (nil), but that’s this compiler’s kindness, not a rule.)
Fix: "it runs" and "it’s correct" are different. Code pointing at a spot that may be overwritten at any time is a time bomb. The principle is to fix code that triggers warnings.
Wall 5. Calling malloc but Forgetting free
Symptom: you borrow but never return.
Cause: the heap has no automatic cleanup. Returning is the borrower’s responsibility.
Fix: write malloc and free together like a pair. Forget to return and land leaks away little by little while the program runs (a memory leak). Today’s free(heap_var); lines are the start of that habit.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Code region | Where translated machine code lives — read-only |
| Data region | Where global/static variables live — sharing the program’s fate |
| Heap region | Land you borrow yourself with malloc — grows upward, return with free |
| Stack region | Where functions’ local variables pile up — grows downward, cleaned up with the function |
| ASLR | A protection that shuffles regions’ starting points each run — order preserved, addresses random |
Today’s Syntax
| Syntax | What it does |
|---|---|
int global_var = 100; (outside functions) |
A global variable living in the data region |
static int s = 200; |
A static variable — data region |
int local = 300; (inside a function) |
A local variable living on the stack |
malloc(sizeof(int)) |
Borrow one slot from the heap — returns the address |
free(pointer) |
Return a borrowed heap slot |
%p |
Print an address — read regions by grouping and order |
A Sense More Important Than Syntax
Today you took the map of a running program into your hands. The habit of looking at a variable and thinking "ah, this fellow lives on the stack" — that is the completed form of eyes that see memory. Technique names like "stack-based buffer overflow" and "heap spray" will now start reading as place names on the map.
A map is not something to memorize but something to unfold. When deciding a variable’s lifetime (if it must outlive the function, the stack is not the answer); when you meet a strange bug ("which region is this pointer pointing into, and what is that region’s lifetime?"). Stick the map you drew on paper next to your desk. It’s a picture you’ll keep using to the end of this book. And if the addresses in the experiments differ from yours, that’s perfectly fine — what matters is not the numbers but the groupings, the order, and the directions.
Once every box is checked, Step 60 is complete. Click the checkbox in the sidebar to save your progress.