Virtual Memory and Process Maps
An address belongs to a process
Section titled “An address belongs to a process”Imagine running a word processor and a web browser on a machine where both programs can read and write physical RAM directly, with no protection between them. Both need somewhere to store their data. If they choose the same location, a browser tab could overwrite part of the document you’re editing. Giving each program a different area would help, but a bad pointer could still take it outside that area. We’d be relying on every program to stay inside its own boundaries.
What we want is for each process to have its own view of memory, with boundaries the machine enforces. Hardware-supported virtual memory lets the operating system provide that view. A program still reads and writes using addresses, just as our C programs did with pointers, but the processor translates those addresses before accessing physical RAM. The operating system controls the translations and the permissions that go with them. The kernel’s page-table documentation describes how the hardware and operating system divide that work.
The addresses a process can use make up its virtual address space. Suppose our word processor stores a character at 0x7ffd12340000, and the browser stores a counter at that same address in its own process. Those addresses can translate to different locations in RAM, so updating the counter leaves the character alone. Copying the word processor’s pointer into the browser wouldn’t give the browser access to the document: the number would still be interpreted using the browser’s address space.
The word virtual doesn’t mean the data is imaginary, or that every process gets a separate set of RAM chips. The processes use the machine’s physical memory, and the operating system manages which parts each process can reach. It can also arrange for processes to share memory deliberately. Separate address spaces give it control over that relationship.
So when we inspect an address, we need to know which process it belongs to before the number tells us anything useful. In this lesson we’ll inspect a running process’s memory map, connect its address ranges to the executable and libraries, and check how much of that mapped space is actually present in RAM. First, we need to see how the translation works.
Pages and page tables
Section titled “Pages and page tables”Virtual memory is implemented with pages and page tables. The course VM normally uses 4096-byte base pages, the units used for mappings and protection here. Other page sizes exist. A C allocator can hand a program an object smaller than a page. Lesson 2 had us masking addresses with 0xfff, and pages are what all that arithmetic was for:
printf 'page 0x%x offset 0x%x\n' $((0x7ffd12340a58 & ~0xfff)) $((0x7ffd12340a58 & 0xfff))The upper part names a virtual page and the lower part is a position inside it. Only the page part gets translated, so a byte doesn’t move around relative to the page holding it.
The processor translates a virtual page through the current address-space page tables, often reusing a cached translation. A page table is a data structure recording translations and permissions; it does not allocate memory or fetch files.
If an access needs a missing translation or violates page permissions, the hardware raises a page fault. Linux checks whether the address and access are valid. It may attach a zero-filled page, map cached file data, or read data into memory, then update the tables and retry the instruction. Invalid accesses receive a different outcome. The kernel’s page-table explanation separates hardware translation from fault handling.
A mapping can exist before any physical page is attached to it. Linux can also reclaim resident pages. A valid access may bring the needed page back, while a protection violation or unmapped address normally results in a signal such as SIGSEGV. A fault is an event to explain; it does not by itself mean the program is broken.
Three consequences follow, and this lesson collects evidence for all three:
- Permissions live in the page tables, so a page can be readable without being writable, and the hardware enforces that instead of trusting the program to behave.
- A missing page-table entry causes a fault. Linux checks whether a valid mapping permits it to supply a page; the missing entry alone doesn’t establish that the address is invalid.
- Two processes can point their own entries at one physical page. That allows processes mapping the same library file to share its unchanged resident pages. Different library versions and modified private pages need not share storage.
The kernel’s Linux page tables documentation separates stored translation entries from the code handling faults. That distinction is useful when an access has no present mapping yet.
Create a process long enough to inspect
Section titled “Create a process long enough to inspect”Before you start it, predict which mapping labels you expect to find, and predict whether two independent runs will use identical executable addresses.
sleep 7200 &sleep_pid=$!printf 'PID: %s\n' "$sleep_pid"Display its mappings:
cat "/proc/$sleep_pid/maps"Each line describes one range, and a typical line looks something like this:
55a1c0000000-55a1c0009000 r-xp 00002000 08:02 12345 /usr/bin/sleepEach of those lines is a mapping: a range of virtual addresses Linux has attached to something, and a record of what it attached and on what terms. The something is usually a region of a file, and it can also be memory with no file behind it at all. So the whole file is a list of the address ranges that currently mean anything in this process, and every address outside them is a hole.
Your exact addresses will differ from mine, and that’s fine. Read the fields as evidence:
| Field | Meaning |
|---|---|
start-end |
Half-open virtual-address range: start included, end excluded |
rwx |
Read, write, and execute permissions; - means absent |
p or s |
Private or shared mapping |
| file offset | Where the mapped bytes begin in the backing file |
| device and inode | Filesystem identity of a backing file |
| path | Backing file or a kernel label such as [stack] |
The live rows now give the diagram a concrete reference:
Subtract the start from the end and you get the mapping size, which is much more useful than either address alone. Those values are hexadecimal, so reach for Python whenever the arithmetic stops being pleasant:
python3 -c 'print(hex(0x2000 - 0x1000), 0x2000 - 0x1000)'So how much memory is sleep actually using?
Section titled “So how much memory is sleep actually using?”Add up the mapped ranges and you’ll get a total in megabytes. Does that mean sleep has that much physical memory attached to it right now?
ps reports virtual size, resident size, and fault counts side by side:
ps -p "$sleep_pid" -o pid,vsz,rss,min_flt,maj_flt,commVSZ is the size of the virtual address space in kilobytes, and RSS is the resident set size, meaning the part of it currently backed by physical memory. For an illustrative example, consider 3124 kB of address space and 1664 kB resident, with 132 minor faults and one major fault. Check your own measurements; these example numbers are not a required result.
That gap is the page-fault mechanism doing its ordinary job. Linux can establish file-backed mappings without eagerly reading every mapped byte. Faults populate pages as needed, and a resident set can later shrink too. Fault counts are not a one-to-one history of the pages currently in RSS. A minor fault is one Linux satisfied without going to disk, and a major fault needed a read from the filesystem. The counters describe faults over the process’s lifetime, while RSS measures its current resident memory.
That word fault sounds like a failure, and the great majority of them are ordinary bookkeeping. A segmentation fault is the same mechanism reaching a different conclusion. The address has no mapping at all, or the access breaks the permissions recorded for its page, and Linux has nothing legitimate to attach. Linux sends SIGSEGV, whose default action terminates the process. A fault can arise from a missing translation or a permission violation. Linux decides whether the requested access can legitimately proceed.
0x63534ebb0000r-xp · /usr/bin/sleep + 0x20000x5b442042c000r-xp · /usr/bin/sleep + 0x2000[heap] pagerw-p · mapped, never touchedThe bottom row is why a mapping and a page of RAM are separate things. The top two rows are why running sleep twice doesn’t cost twice the physical memory, and we’ll collect the evidence for that claim at the end of the lesson.
Connect the live map to the ELF file
Section titled “Connect the live map to the ELF file”Show only mappings backed by the executable:
grep "$(readlink "/proc/$sleep_pid/exe")" "/proc/$sleep_pid/maps"Compare their file offsets and permissions with the LOAD segments from the previous lesson:
readelf -lW "$(readlink "/proc/$sleep_pid/exe")"Those numbers probably won’t look identical at first glance, and they aren’t supposed to. Program headers describe the ELF loading requirements, while /proc/PID/maps describes the page-aligned live mappings that actually resulted from them. A page table entry covers a whole page or nothing, so a segment beginning partway into a page still produces a mapping starting on the page boundary below it. Alignment and PIE relocation both affect the addresses you’re seeing here. The relationship between them is evidence to work out, and it was never a promise of matching rows.
For a PIE image, the load bias is the runtime placement added on top of the ELF virtual-address relationships. Take a mapping whose file offset corresponds to a LOAD segment (any of them will do), align both values to the page boundary, and then calculate:
load bias = runtime mapping start - page-aligned ELF VirtAddrruntime address = load bias + ELF virtual addressCheck that result against a second executable mapping. If it doesn’t hold, confirm you matched the same file offset and the same page alignment first. The subtraction only has meaning when both addresses refer to the corresponding file location.
The other file-backed mappings usually belong to the dynamic loader and to shared libraries. Lines without a normal pathname may represent anonymous memory created at runtime, and kernel labels like [heap], [stack], [vvar], and [vdso] identify special roles.
Observe randomization
Section titled “Observe randomization”Start a second process:
sleep 7200 &second_pid=$!grep '/sleep$' "/proc/$sleep_pid/maps"grep '/sleep$' "/proc/$second_pid/maps"With address-space layout randomization enabled, the executable mappings of a PIE binary will normally begin at completely different virtual addresses. The relative layout inside each loaded image stays meaningful even while the base wanders around between runs.
That’s exactly why offsets from a module base stay useful, even when the absolute process addresses change.
One physical copy, two private maps
Section titled “One physical copy, two private maps”Randomization shows the two address spaces are independent. It doesn’t show they’re expensive, and this is where a reasonable mental model tends to assume a second full copy of everything:
grep -E '^(Rss|Pss|Shared_Clean):' "/proc/$sleep_pid/smaps_rollup" "/proc/$second_pid/smaps_rollup"Rss totals the physical memory attached to that process, and Shared_Clean is the unmodified part of the total some other process also maps. Pss is the proportional set size, and it divides every shared page by the number of processes mapping it. As an illustrative calculation, suppose each process reports about 1848 kB resident, 1736 kB shared, and a Pss near 205 kB. Use the smaps field definitions to interpret your own values.
In that illustrative case, PSS is roughly a tenth of RSS because most resident pages are shared with other processes. Unchanged resident pages from the same executable and libraries can be shared across mappings at different virtual addresses. RSS counts those pages in each process; PSS apportions them across the processes sharing them. The programs can use those pages without managing the sharing themselves.
Clean up:
kill "$sleep_pid" "$second_pid"wait "$sleep_pid" "$second_pid"Verify the cleanup with ps -p "$sleep_pid,$second_pid". No target rows should be left, and then you can run unset sleep_pid second_pid.
Common failures and recovery
Section titled “Common failures and recovery”/proc/PID/mapsis missing: either the process ended or the PID has gone stale. Start a newsleep 7200, replace the variable, and verify withpsbefore anything else.grepfinds no executable rows: resolve/proc/$sleep_pid/exeand search for that exact path. Don’t assume the display ends in/sleepon every supported update.- The load-bias arithmetic doesn’t work out: match the file offsets and the page alignment before you subtract any addresses.
- The address values changed: that’s expected across independent PIE runs. Compare the relative offsets and the mapping permissions instead.
RSSchanged between two runs ofps: also expected. A resident set grows as the process touches more pages, and Linux can reclaim pages later. Read it as a measurement taken at a moment, not a fixed property of the program.smaps_rollupisn’t there: the file needs a reasonably modern kernel. Read/proc/PID/smapsand total the same field across its entries, or check theVSZandRSScomparison alone and note which evidence you couldn’t collect.- A practice process is still hanging around: verify its PID and command, send
TERM, wait for it, and then prove the row has gone.
What this lets us investigate
Section titled “What this lets us investigate”You can now read a live address-space map, explain its permissions, separate the address space from the physical memory behind it, and connect the file-backed regions back to ELF loading metadata. The first lab asks you to reconstruct that whole relationship for an executable you didn’t build.
Check your understanding
Section titled “Check your understanding”Knowledge check
Question 1 of 2
Practice complete
You've checked the key ideas. Revisit any explanation you need, then continue when it makes sense.