Process Memory Layout

On this page

A process address space is a collection of mapping regions and their permissions, not a fixed continuous partition of "code, data, heap, stack." /proc/PID/maps describes which addresses are currently mapped, while smaps supplements this with statistics on residency and sharing. Only by distinguishing virtual addresses, underlying files or anonymous storage, and actual resident pages can memory usage be correctly interpreted.

64-bit Does Not Mean All 64-bit Addresses Are Available

Pointer width, the number of address bits implemented by the CPU, kernel page table configuration, and user-space address space range are distinct concepts. x86-64 commonly uses four-level or five-level page table configurations, while arm64 has different combinations of virtual address widths and page granularities. It is incorrect to state as a cross-architecture fact that "every Linux process is 48-bit or 57-bit."

The address space contains unmapped holes, reserved regions, and ranges restricted by policy. Even after a mapping succeeds, independent physical memory is not necessarily allocated for every page; page faults, zero pages, file caches, and copy-on-write gradually determine the actual page state.

View virtual mappings and physical residency separately: large reservation does not mean RAM is fully occupied Mapping A: File Code r-xp, length 16 KiB Mapping B: Anonymous Data rw-p, length 16 KiB Resident Resident Resident Example: each cell is 4 KiB; gray cells are not counted as resident in this example, but actual page states can vary. maps indicates addresses and permissions; RSS/PSS in smaps help explain physical usage and sharing.

Reading a Line in maps

70000000-70004000 r-xp 00002000 08:01 12345 /opt/demo

This is a manually constructed format example. The address range is half-open, with a length of 0x4000; the mapping starts from file offset 0x2000. r/w/x indicate read/write/execute permissions, p indicates private, and s indicates shared. Device numbers and inodes help identify the underlying object; the path may include "deleted," and the name itself is not a stable identity.

If the accessed address is 0x70000120, the corresponding offset for this file-backed region is 0x2000 + 0x120 = 0x2120. However, parts like .bss that do not correspond to file bytes cannot be uniformly treated as readable disk offsets. proc_pid_maps(5)

ELF files are loaded according to program header segments; you cannot map each VMA line one-to-one with a section. A single PT_LOAD may be split into multiple lines due to permission adjustments, etc. The tail of .bss may be mixed with the last page of the file, followed by anonymous zero-filled mappings. Interpret this using readelf -lW's offset, filesz, and memsz.

private Does Not Always Mean Exclusive Physical Pages

MAP_PRIVATE indicates that modifications will not be written back to other mapping users in the manner of shared mappings. Private anonymous pages after fork usually share physical content initially, copying only upon write; unmodified pages of private file mappings can also share file caches.

Therefore, ordinary private anonymous regions obtained by malloc using mmap may still undergo COW after fork. Using mmap is not a sufficient condition for "fork-friendly so no COW." For an example of MAP_SHARED where modifications are truly shared, see Shared Memory and Semaphores.

Distinguishing Private vs. Shared Modifications Using Parent and Child Processes

Below, we first create MAP_PRIVATE and MAP_SHARED anonymous mappings for two pages, write the same initial value of 7, and then fork. The child process writes 9 to both locations; the parent process waits for the child to finish and then reads. The private page remains 7, while the shared page becomes 9. Memory relationships of fork

#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    long page_size = sysconf(_SC_PAGESIZE);
    if (page_size <= 0) return 1;
    size_t size = (size_t)page_size;
    unsigned char *private_page = mmap(NULL, size, PROT_READ | PROT_WRITE,
                                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (private_page == MAP_FAILED) return 1;
    unsigned char *shared_page = mmap(NULL, size, PROT_READ | PROT_WRITE,
                                    MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (shared_page == MAP_FAILED) { munmap(private_page, size); return 1; }
    private_page[0] = shared_page[0] = 7;
    pid_t child = fork();
    if (child == 0) {
        private_page[0] = 9;
        shared_page[0] = 9;
        _exit(private_page[0] == 9 && shared_page[0] == 9 ? 0 : 1);
    }
    int failed = child == -1;
    if (!failed) {
        int status;
        pid_t waited;
        do { waited = waitpid(child, &status, 0); }
        while (waited == -1 && errno == EINTR);
        failed = waited != child || !WIFEXITED(status) || WEXITSTATUS(status) != 0;
        if (!failed) {
            printf("parent: private=%u shared=%u\n",
                   (unsigned)private_page[0], (unsigned)shared_page[0]);
            failed = private_page[0] != 7 || shared_page[0] != 9;
        }
    }
    if (munmap(private_page, size) == -1) failed = 1;
    if (munmap(shared_page, size) == -1) failed = 1;
    return failed;
}

Save as cow-demo.c, execute cc -Wall -Wextra cow-demo.c -o cow-demo, and then run it. The expected output is parent: private=7 shared=9. Parent and child processes see the same virtual pointer values, but can see different contents via private mappings; therefore, comparing address values alone cannot determine if two processes share modifications.

This example uses waitpid to confirm the child process has finished writing, ensuring the parent process does not concurrently poll during the child's write phase. Actual shared memory queues require inter-process synchronization; you cannot treat "mapping as shared" as automatically completing publish, ordering, and notification. The experiment verifies visibility semantics and cannot directly measure how many physical pages the kernel actually copied from these two bytes.

Stack: Main Thread vs. pthread Stack

The main thread stack is established during exec, often identified by [stack], and can expand on eligible page faults, subject to resource limits, adjacent mappings, and protection rules. It is not allocated by brk, and arbitrary access to addresses near the stack is not automatically accepted by the kernel.

pthread stacks are usually established by the thread library using anonymous mappings. Their size and guard policies can be adjusted via attributes, so the main stack's automatic growth model cannot be simply copied. Default sizes vary based on startup limits, platform, and thread library; they are not fixed at 8 MiB.

Ordinary unnamed anonymous VMAs may belong to thread stacks, allocator arenas, JIT, or other objects. Modern maps no longer universally display [stack:TID] for each thread stack; you cannot confirm a rw-p region is definitely a stack based solely on its position.

Heap: malloc Objects Are Not All in [heap]

[heap] usually corresponds to the traditional heap range managed by the program break. malloc also uses other anonymous mappings for arenas or large block allocations, so counting this single line does not yield the total dynamic allocation amount.

Small allocations may come from thread caches or existing free blocks in the arena, not calling brk every time. Whether to use independent mmap depends on allocation size, current free blocks, thresholds, runtime history, and the specific allocator; you cannot apply a fixed 128 KiB rule universally. malloc Internals

vDSO, vvar, and Historical vsyscall

vDSO is a small ELF code segment provided by the kernel to user space, accelerating specific calls on supported architectures and kernels; related read-only data mappings often appear as vvar. These are special kernel-provided mappings and should not be treated as ordinary malloc anonymous pages.

The set of functions, symbol versions, and whether to fall back to system calls are determined by the implementation. Even though clock_gettime often uses vDSO, you cannot infer that no clock or kernel will ever execute a syscall. Locate them via auxiliary vectors and other conventions; do not hardcode addresses. vdso(7)

The historical vsyscall on x86-64 uses a fixed address range, which may be emulated or disabled by the kernel, depending on configuration. It is not the same interface as the randomizable vDSO, and arm64 does not copy this fixed address.

Observing Your Own Actual Layout

The following program prints the addresses of several objects and reads its own maps. In cat /proc/self/maps, self refers to the cat process, not the shell that launched it; when viewing the target, use the target PID, or read it internally as done here.

#include <stdio.h>
#include <stdlib.h>

static int global_value = 7;

int main(void) {
    int local_value = 3;
    void *allocated = malloc(4096);
    if (allocated == NULL) return 1;
    printf("global=%p stack=%p allocated=%p\n",
           (void *)&global_value, (void *)&local_value, allocated);
    FILE *maps = fopen("/proc/self/maps", "r");
    if (maps == NULL) { free(allocated); return 1; }
    char line[1024];
    int failed = 0;
    while (fgets(line, sizeof line, maps) != NULL) {
        if (fputs(line, stdout) == EOF) { failed = 1; break; }
    }
    if (ferror(maps)) failed = 1;
    if (fclose(maps) == EOF) failed = 1;
    free(allocated);
    if (fflush(stdout) == EOF) failed = 1;
    return failed;
}

After compiling, find which interval each of the three addresses falls into. The base addresses may change across independent launches, but specific address differences are not guaranteed by language semantics. Allocations that are not initialized and where every page is not accessed are not suitable as experiments for "allocating X increases RSS by exactly X."

RSS, PSS, and Virtual Size

VmSize describes the scale of the virtual address range, while RSS counts resident pages. Shared pages may be double-counted in the RSS of multiple processes. PSS apportions sharing, helping to compare the resident contribution of a group of processes. smaps_rollup provides summaries, while smaps retains per-mapping details.

These metrics still do not equal the allocator's in-use bytes, nor do they equal cgroup accounting; page tables, kernel objects, shared caches, and timing of statistics create different perspectives. Mappings and counts change as the process runs; reading across multiple /proc files does not automatically form a single atomic snapshot. Kernel /proc Documentation

After COW: RSS of two processes may remain unchanged, but total PSS increases Before Write: Sharing 4 Resident Pages Parent/Child RSS each 16 KiB, PSS each 8 KiB After Child Copies One Page Parent/Child RSS still each 16 KiB, PSS each 10 KiB Physical Pages: 4 → 5; Combined RSS of Both: 32 → 32 KiB Combined PSS: 16 → 20 KiB, closer to the actual total resident pages in this model Assumes page size 4 KiB, only two sharers, and all are resident; ignores page tables, kernel, and other mappings.

Explain PSS Using Page Accounting, Not as Another malloc Size

Assume parent and child processes jointly map 4 resident pages of 4 KiB each, with no other sharers. Each process's RSS calculates the full 4 pages, while PSS calculates half per page. After the child process undergoes COW on one of these pages, leaving 3 common pages plus 1 private page each for parent and child. smaps Metrics

from fractions import Fraction
page = 4096
before_rss_each = 4 * page
before_pss_each = 4 * Fraction(page, 2)
after_rss_each = (3 + 1) * page
after_pss_each = 3 * Fraction(page, 2) + page
assert before_rss_each == after_rss_each == 16384
assert before_pss_each == 8192
assert after_pss_each == 10240
assert 2 * before_pss_each == 4 * page
assert 2 * after_pss_each == 5 * page

This model explains why, after copying occurs, RSS might show no change. Real smaps also encounters factors like changing sharers, page reclamation, huge pages, and statistical precision; do not treat this 2 KiB change as the mandatory whole-process difference that must appear in every run of cow-demo.

Reverse-Querying VMAs from Printed Addresses Requires Verifying Interval Inclusion

The global, stack, and allocated addresses from the previous program can be compared one-by-one with the start and end addresses in maps, using start ≤ address < end. If an address exactly equals end, it no longer belongs to that line. Path names can assist in explaining the mapping source but cannot replace this check, nor can they prove the object is still alive.

Address LocationConfirmed InformationInformation Still Needed
Inside File MappingFile region and permissionsELF segments, symbols, and whether private modifications occurred
Inside [heap]Located in traditional program break regionWhich allocation object it corresponds to, and whether it has been freed
Inside Unnamed rw-pPrivate writable anonymous regionThread stack, arena, or other purposes
No MappingNo ordinary VMA for this address at this timeAddress origin, lifecycle, and mapping changes

Why Did Physical Pages Increase When RSS Didn't Change?

Treat colored blocks with the same name as two mappings to the same physical page. Each time COW occurs, the child process still maps four pages, so RSS remains unchanged; but one page is no longer shared with the parent, changing PSS from half-page accounting to full-page accounting. This only accounts for these four pages and cannot be directly equated to the difference in the real process's entire smaps.

Preparing the visual
Why Did Physical Pages Increase When RSS Didn't Change?

COW splits originally shared physical pages into two private copies. The RSS of both processes can remain unchanged, while PSS and the total number of independent physical pages increase.

Locating Problems from Addresses

First confirm whether the faulting address has a mapping, then check permissions, owning object, offset, and lifecycle; "the address is inside an rw-p mapping" does not prove the object access is legal, as it might still be an out-of-bounds access within the same page or use-after-free.

Understanding the layout narrows the scope for locating issues, but object-level errors still require debug symbols and memory detection tools. The protective effects of randomization and execution permissions are covered in ASLR and Security Hardening.

Keywords: virtual memory, VMA, maps, smaps, RSS, PSS, stack, brk, mmap, COW, vDSO, ASLR