Skip to content
Courses

Virtual Memory and Process Maps

How Programs WorkLesson 3 of 790 minutesLessonBuilds on: Reading an ELF FileLab next: Executable Mapping Lab

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.

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:

Terminal window
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.

Before you start it, predict which mapping labels you expect to find, and predict whether two independent runs will use identical executable addresses.

Terminal window
sleep 7200 &
sleep_pid=$!
printf 'PID: %s\n' "$sleep_pid"

Display its mappings:

Terminal window
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/sleep

Each 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:

A process receives a private virtual address space made of mappings with different jobs and permissions
higher virtual addressesone process
[stack]read + write · grows down
unmapped gap
shared librariesfile-backed mappings
layout varies
[heap]dynamic allocation pool
executable mappingscode · read-only data · writable data
lower virtual addressesconceptual · not to scale

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:

Terminal window
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:

Terminal window
ps -p "$sleep_pid" -o pid,vsz,rss,min_flt,maj_flt,comm

VSZ 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.

Two processes translate different virtual pages through their own page tables, and Linux can point both entries at one physical page
Virtual page
Page table entry
Physical memory
Process A0x63534ebb0000r-xp · /usr/bin/sleep + 0x2000
presentread + execute
one physical page4096 bytes of sleep code · both entries point here
Process B0x5b442042c000r-xp · /usr/bin/sleep + 0x2000
presentread + execute
Process A[heap] pagerw-p · mapped, never touched
no entry yetcounted in VSZ, absent from RSS
nothing attachedfirst access faults · Linux attaches a page and retries

The 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.

Show only mappings backed by the executable:

Terminal window
grep "$(readlink "/proc/$sleep_pid/exe")" "/proc/$sleep_pid/maps"

Compare their file offsets and permissions with the LOAD segments from the previous lesson:

Terminal window
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 VirtAddr
runtime address = load bias + ELF virtual address

Check 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.

Start a second process:

Terminal window
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.

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:

Terminal window
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:

Terminal window
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.

  • /proc/PID/maps is missing: either the process ended or the PID has gone stale. Start a new sleep 7200, replace the variable, and verify with ps before anything else.
  • grep finds no executable rows: resolve /proc/$sleep_pid/exe and search for that exact path. Don’t assume the display ends in /sleep on 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.
  • RSS changed between two runs of ps: 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_rollup isn’t there: the file needs a reasonably modern kernel. Read /proc/PID/smaps and total the same field across its entries, or check the VSZ and RSS comparison 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.

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.

Knowledge check

Question 1 of 2

Two processes use the same virtual address. What can you conclude about the physical page?