Step 69. Virtual Memory — Every Process’s Sweet Illusion
Level 1 — Programming and the Computer’s Interior | Difficulty ★★★☆☆ | Estimated time: 3 hours
Prerequisites: Step 68 complete; you confirmed by experiment that fork clones processes and memory passes over as a copy. You know the concepts of pointers and addresses (Steps 58–60).
- What you need: a Linux terminal (WSL or Ubuntu) and gcc. Today is the most abstract topic so far, but I’ve prepared four experiments, so let’s understand it by touching it with our hands.
- Caution: today’s practice is 100% safe. There’s an experiment that only "allocates" a huge amount of memory — since we never actually use that memory, your computer won’t slow down.
Last time, cloning processes with fork, we learned that "each has independent memory." But think it over and something’s odd. The program you made printed a variable’s address, and that address was somewhere around 0x7fff…. Yet running the same program on my computer shows a similar address. Different computer, different RAM — why are the addresses similar? The answer: because that address is not real RAM’s address. Every process lives inside a grand illusion that makes it believe "I’m using all of this computer’s memory by myself," and the device creating this illusion is today’s protagonist, virtual memory.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain the difference between virtual and physical addresses, and the role of the page table connecting them
- Confirm by experiment that "same virtual address ≠ same physical memory"
- Read a process’s virtual address map with /proc/self/maps
- Explain what lazy allocation is, using the malloc experiment
- State why virtual memory is the security castle wall called process isolation
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | C language + Linux terminal (verified on WSL Ubuntu 24.04, 7.5 GB RAM) |
| Today’s commands | cat /proc/self/maps (virtual address map), free -h (physical memory and swap), getconf PAGESIZE (page size), setarch -R (run with ASLR off), tr/cut/uniq -c (count permission letters) |
| Today’s C elements | printing addresses with &variable, the %p format, requesting big memory with malloc() |
| Concepts needed | Virtual/physical addresses, pages (4 KB), page tables, MMU, swap, ASLR |
| Today’s deliverable | A virtual memory picture essay — a diagram connecting two processes’ address spaces to RAM |
2-1. Two Kinds of Addresses
There are exactly two key terms to learn today.
- Virtual address: the address a process sees and uses. Every pointer value inside a program is one of these. Each process has its own address space starting from address 0.
- Physical address: the real location on the actual RAM chip. There’s only one of these per computer.
Let’s use a restaurant analogy. A virtual address is a "table number," and a physical address is "the actual counter position in the kitchen." The guest (process) only needs to know table 3, and only the restaurant staff (the operating system and CPU) know where in the kitchen that table 3 connects. Even if guests at two tables each say "number 3," their food can come out of different counters.
2-2. Pages and Page Tables
Converting virtual addresses to physical addresses one byte at a time would make the table too big, so memory is cut into fixed-size pieces for translation. That piece is a page — 4096 bytes (4 KB) on the computer we verify on today. And the comparison table recording "virtual page number N connects to physical page number M" is the page table. This table exists separately per process, so the same virtual address connects to this side of RAM in process A and to that side in process B.
The conversion itself is done lightning-fast by hardware inside the CPU called the MMU (memory management unit). The program doesn’t even know translation is happening. It just pokes an address, and the MMU finds the real spot on its own.
2-3. Three Miracles This Device Creates
- Isolation: process A’s page table simply has no entries for process B’s physical pages. So no matter what strange address A pokes, it can’t reach B’s memory. Step 67’s segmentation fault, too, was the MMU’s refusal: "that virtual address isn’t in your page table."
- Sharing: conversely, if two processes’ page tables point at the same physical page, they can share memory. Since every process uses the C standard library, a single copy is placed in physical memory and registered in everyone’s page table.
- Extending the illusion: when RAM runs short, pages unused for a while are sent to disk temporarily (this is swap), then brought back when needed. The process never knows part of its memory took a trip to disk.
3. Follow Along
3-1. Predict — Same Program, Two Runs
Let’s make a simple address-printing program.
Input (addr.c)
#include <stdio.h>
int global_var = 42;
int main(void) {
int local_var = 7;
printf("global variable address: %p\n", (void*)&global_var);
printf("local variable address: %p\n", (void*)&local_var);
return 0;
}
Will the addresses be the same or different on each run? And how will the shapes of the global variable’s and local variable’s addresses differ from each other? Write down your predictions.
Input
gcc -o addr addr.c
./addr
./addr
Output (verified 2026-09-09):
== run 1 ==
global variable address: 0x5b573eb02010
local variable address: 0x7ffca2c9f354
== run 2 ==
global variable address: 0x5eb1d06a2010
local variable address: 0x7ffe4b3990b4
How to read it: two things to read. First, the trailing digits 2010 of the global variable’s address are identical in both runs — meaning the relative position inside the program is the same every time. Second, the front part changed from 0x5b57… to 0x5eb1…. That’s because the operating system randomly moves the map’s starting point on every run (ASLR — covered in detail in Step 70). And the local variable (stack) sits in a completely different high band, 0x7ff….
Why: the very fact that "the program runs fine whether the addresses change or not" is the first hint that these addresses aren’t real RAM coordinates.
3-2. The Fixed-Address Experiment — Proof of Identical Virtual Addresses
This time, let’s turn off the starting-point shuffling (ASLR) and run twice. setarch -R is a command meaning "run just this program without random placement" — it applies only to that one program and doesn’t change system settings.
Input
setarch -R ./addr
setarch -R ./addr
Output (verified 2026-09-09):
global variable address: 0x555555558010
local variable address: 0x7fffffffe6b4
global variable address: 0x555555558010
local variable address: 0x7fffffffe6b4
How to read it: the two runs’ addresses are completely identical. Now think. Even right now, dozens of processes are running simultaneously on this computer, and with ASLR off, all those processes have exactly the same virtual addresses like this. Yet there’s only one RAM chip. The same virtual address 0x555555558010 is being connected to dozens of different physical locations. This is direct evidence of today’s key sentence: "same virtual address ≠ same physical memory."
Why: the step of grasping the sense that an address is "a dream-world coordinate of each process." Whether your prediction was right or wrong, you should now be able to explain why.
3-3. Looking Inside a Process’s Memory Map
Linux has a virtual file that shows each process’s virtual address map.
Input
cat /proc/self/maps | head -n 6
cat /proc/self/maps | grep stack
getconf PAGESIZE
Output (verified 2026-09-09):
5eee5da28000-5eee5da2a000 r--p 00000000 08:30 1510 /usr/bin/cat
5eee5da2a000-5eee5da2f000 r-xp 00002000 08:30 1510 /usr/bin/cat
5eee5da2f000-5eee5da31000 r--p 00007000 08:30 1510 /usr/bin/cat
5eee5da31000-5eee5da32000 r--p 00008000 08:30 1510 /usr/bin/cat
5eee5da32000-5eee5da33000 rw-p 00009000 08:30 1510 /usr/bin/cat
5eee7de77000-5eee7de98000 rw-p 00000000 00:00 0 [heap]
7ffdc9c7e000-7ffdc9c9f000 rw-p 00000000 00:00 0 [stack]
4096
How to read it: each line is one segment of the virtual address space. The first two columns are the start and end addresses; the next is permissions (r: read, w: write, x: execute, p: private to this process). The code region (r-xp, executable), data region, heap ([heap]), and stack ([stack]) you learned in Step 60 are all laid out in address order. And look at the segment boundaries — they all end in …000, because they’re multiples of the 4096 bytes (0x1000) that getconf PAGESIZE reported. The map’s grid lines are pages.
Why: the step of confirming that the "memory map" learned only as a picture actually exists and that the operating system manages it. Replace self with another PID (/proc/PID/maps) and you can see that process’s map too.
3-4. Counting Permission Letters — The Color Distribution of the Map
Let’s extract just the permission column (second) of the maps output and count by kind.
Input
cat /proc/self/maps | tr -s " " | cut -d" " -f2 | sort | uniq -c | sort -rn
Output (verified 2026-09-09):
23 r--p
9 rw-p
4 r-xp
1 r--s
How to read it: tr -s " " squeezes consecutive spaces into one, cut -d" " -f2 extracts only the second column (permissions), and sort | uniq -c counts by kind. r-xp (read+execute, code segments), rw-p (read+write, data/heap/stack), and r–p (read-only) are clearly distinguished.
Why: "keeping executable regions and writable regions separated" isn’t mere tidiness — it’s security design. The code region must have writing blocked so an attacker can’t overwrite machine code, and the data region must have execution blocked so an attacker can’t run planted code. These permission letters are the substance of the defensive technology called DEP/NX you’ll learn later.
3-5. Checking Physical Memory and Swap
This time, let’s look outside the illusion — at real RAM.
Input
free -h
Output (verified 2026-09-09):
total used free shared buff/cache available
Mem: 7.5Gi 621Mi 6.0Gi 3.6Mi 1.1Gi 6.9Gi
Swap: 2.0Gi 0B 2.0Gi
How to read it: the Mem row is actual RAM (7.5 GB), and the Swap row is emergency memory space set aside on disk (2 GB). The reason the total doesn’t overflow even when many processes each act "as if they own" a large address space is exactly sharing and swap.
Why: the step of getting a feel for how virtual memory’s promise ("each of you, act as if you own a large space") connects to actual resources (7.5 GB RAM + 2 GB swap). The next experiment pokes that boundary directly.
3-6. Predict — Can You Borrow Memory Larger Than RAM?
The final experiment. This computer’s RAM is 7.5 GB. If a program requests 9 GB, 10 GB, and 12 GB with malloc, will each succeed or fail? Write down your prediction and check.
Input (big2.c)
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
long gb = atol(argv[1]);
size_t size = (size_t)gb * 1024 * 1024 * 1024;
char *p = malloc(size);
if (p == NULL) printf("%ldGB request -> failed (NULL)\n", gb);
else printf("%ldGB request -> succeeded (address %p)\n", gb, (void*)p);
return 0;
}
Input
gcc -o big2 big2.c
./big2 9; ./big2 10; ./big2 12
free -h | head -n 2
Output (verified 2026-09-09):
9GB request -> succeeded (address 0x7ff90d5ff010)
10GB request -> failed (NULL)
12GB request -> failed (NULL)
total used free ...
Mem: 7.5Gi 666Mi 5.8Gi ...
How to read it: three discoveries. First, a 9 GB allocation larger than RAM (7.5 GB) succeeded — because the operating system only promised "sure, it’s yours" without handing over a single page of actual RAM. This is lazy allocation. Real RAM is assigned page by page only at the moment you first write a value to that memory. Second, free -h’s used (666Mi) right after the run barely grew — again evidence that only promises were exchanged. Third, from 10 GB up, requests were refused. Even promises aren’t limitless — in this lab, around 9.5 GB, the sum of RAM and swap, was the limit of promising.
Why: an experiment showing that virtual memory is an "economy of promises." It hands out address space generously, gives the substance (physical pages) only at the moment of need, and still guards the boundary of the total. If your prediction missed, that very miss is today’s harvest.
4. Missions & Exercises
Mission — A Virtual Memory Picture Essay
- On paper or in a drawing tool, draw "process A’s virtual address space" on the left and "physical RAM" on the right
- Divide the virtual space into code/data/heap/stack (Step 60 review), and draw arrows from each region to arbitrary locations in RAM. This bundle of arrows is the page table
- Draw one more process, B, and add arrows going from the same virtual addresses as A’s to different RAM locations
- For just the C library region, draw A’s and B’s arrows pointing to the same RAM location to express "sharing"
- Below the picture, write three sentences: ① why the same virtual address points to a different substance ② how this structure creates process isolation ③ where swap belongs in this picture
Exercises
Q1. Explain the difference between virtual and physical addresses using the restaurant "table number and counter" analogy or one of your own.
Q2. In the 3-2 experiment, explain from the page-table perspective why all processes having the same virtual addresses with ASLR off doesn’t cause collisions.
Q3. In the maps output, explain why segment boundaries all end in …000, connecting it to page size.
Q4. Explain the 2026-09-09 verification — 9 GB malloc succeeding and 10 GB failing — from the perspective of an "economy of promises." How much actual RAM did the successful 9 GB use?
5. Model Answers & Completion Criteria
Mission Model Answer
The picture’s checkpoints and an example of the three sentences:
[Picture check] A's code/data/heap/stack → arrows to scattered locations in RAM.
B likewise has arrows from a virtual space of the same shape to different RAM locations.
Do A's and B's identical virtual addresses point to different RAM slots? For just the
library region, do A's and B's arrows point to the same RAM slot? If both are drawn, success.
[Three-sentence example]
① Because page tables exist separately per process, so even for the same virtual
address, the physical page written in the table differs.
② Because B's page table simply has no entries for A's physical pages,
no matter what virtual address B pokes, it can't reach A's memory — this is isolation.
③ Swap is the case where an arrow's destination is a waiting area on disk
instead of RAM. The page table marks it "currently on disk."
How to verify: ① Does the picture have arrows from same virtual address → different physical addresses? ② Is one shared region drawn? ③ Are the three sentences written in your own words? If all three are "yes," it’s complete. Completing this picture means you’ve digested an entire chapter of an operating systems course.
Exercise Solutions
Q1 solution. A virtual address is the "table number" a process sees and uses; a physical address is the "counter," the real location on the RAM chip. The guest (process) only needs to know its own table number, and only the restaurant staff (the operating system’s page table + the CPU’s MMU) know which counter the number connects to. Exact terminology: virtual address, physical address.
Q2 solution. Because page tables exist separately per process. Even for the same virtual address 0x555555558010, process A’s table has this side of RAM written in it, and process B’s table has that side. The moment translation happens, they become different physical locations, so no collision (verified 2026-09-09: with setarch -R, two runs’ addresses became completely identical).
Q3 solution. Because memory segments are cut in page units (4096 bytes in this lab, verified with getconf PAGESIZE on 2026-09-09). Since 4096 is 0x1000 in hexadecimal, a page-boundary address always ends in three hex zeros. Every segment boundary in maps ending in …000 is evidence that the map’s grid lines are pages.
Q4 solution. malloc first makes a promise — "I’ll give you address space" — and assigns real RAM (physical pages) only at the moment a value is first written to that memory (lazy allocation). So even right after "borrowing" 9 GB, the actual RAM used is effectively 0 — free -h’s used staying at 666Mi is the evidence. However, the total of promises also has a limit: the 10 GB request, exceeding the sum of RAM and swap (about 9.5 GB in this lab), was refused outright.
Completion Criteria Checklist
- [ ] I can explain the difference between virtual and physical addresses with an example
- [ ] I can draw the page table’s role as a picture
- [ ] I confirmed by experiment that "same virtual address ≠ same physical memory"
- [ ] I can find the code/heap/stack segments in /proc/self/maps
- [ ] I can explain what lazy allocation is, using the 9 GB experiment
- [ ] I can explain that separating executable and writable regions is security design
- [ ] Mission: I completed the virtual memory picture essay
6. Common Pitfalls & Fixes
Wall 1. The concepts are too abstract
Symptom: words like virtual address and page table won’t enter your head.
Cause: normal. Virtual memory is inherently an invisible device, so it can’t help being abstract.
Fix: hold onto today’s one key sentence — "same virtual address ≠ same physical memory" — and recall the 3-2 experiment (with ASLR off, the addresses became completely identical). The experiment scene is the concept.
Wall 2. /proc/self/maps output is too much
Symptom: dozens of lines pour out and you don’t know where to look.
Cause: each individual library occupies its own segments.
Fix: at first, view just the top with head, and finding the two lines marked [heap] and [stack] is enough. Pass over the rest as "those are libraries."
Wall 3. A big malloc fails
Symptom (verified 2026-09-09): ./big2 10 produces 10GB request -> failed (NULL).
Cause: the operating system refuses requests beyond the total limit of promises (RAM + swap, about 9.5 GB in this lab). Each system has a different overcommit policy (how much excess promising to allow), so limits differ too.
Fix: failure is also a normal result and a good observation. Varying the size by 1 GB at a time to find the boundary of "how far will my computer promise?" is itself an excellent experiment.
Wall 4. Two runs’ addresses are completely different
Symptom: in the 3-1 experiment, the leading digits are entirely different (0x5b57… → 0x5eb1…).
Cause: a security feature called ASLR (address space layout randomization) moves the map’s starting point on every run. In Step 70 you’ll confirm this phenomenon with an observation tool of your own making.
Fix: different is fine. Two things to look at — are the trailing digits preserved (relative position unchanged), and the fact that neither is a real RAM location. Recording the run-to-run variation in your observation log makes good material too.
Wall 5. setarch -R doesn’t work
Symptom: setarch -R ./addr errors out, or addresses still change.
Cause: some environments (certain containers, older WSL kernels) don’t allow turning ASLR off.
Fix: the 3-2 experiment’s conclusion ("same virtual address → different physical") can be understood well enough from 3-1’s trailing-digit preservation and the maps experiment. In an environment where it doesn’t work, note that fact and move on.
7. Summary
Today’s Concepts
| Concept | One-line description |
|---|---|
| Virtual address / physical address | The dream-world coordinate a process uses / the real location on the RAM chip |
| Page | The unit piece of address translation — 4096 bytes in this lab (verified 2026-09-09) |
| Page table | The virtual → physical comparison table — exists separately per process |
| MMU | The device inside the CPU that translates at hardware speed |
| Swap | The disk waiting area for pages unused for a while |
| Lazy allocation | malloc promises first; real RAM is assigned at the moment of writing |
| ASLR | Randomization moving the map’s starting point each run — trailing digits are preserved |
Today’s Commands
| Command | What it does |
|---|---|
cat /proc/self/maps |
View this process’s virtual address map |
cat /proc/PID/maps |
View another process’s map |
free -h |
Status of physical memory and swap |
getconf PAGESIZE |
Check the page size |
setarch -R ./program |
Run one process with ASLR off |
tr -s " " | cut -d" " -f2 | sort | uniq -c |
Extract a column and count by kind |
The Instinct That Matters More Than Commands
As of today, you see the word "address" in two layers. The number inside a pointer was never the world’s real coordinate — it was each process’s dream-world coordinate. Only someone who knows the bridge connecting dream and reality (the page table) can truly understand memory-related attacks and defenses. One sentence to remember — "same virtual address ≠ same physical memory."
The security connection: virtual memory is a security castle wall giving each process an independent world. Yet there are also doors that cross this wall "legitimately" — a debugger (gdb) can read and write another process’s memory because the operating system opens a special door called ptrace only to permitted processes. Attackers, conversely, try to cross this wall through page-table configuration mistakes or kernel vulnerabilities. Every memory observation in today’s experiments happened only inside your own processes. ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
Once every box is checked, Step 69 is complete. Click the checkbox in the sidebar to save your progress.