AlgoMaster Logo

The Process Address Space

26 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

Two copies of the same backend service can both print a pointer such as:

Yet writing through that pointer in one process does not normally change memory in the other. The numeric address is only part of the story. It must be interpreted inside the process that issued the memory access.

Each process runs inside its own virtual address space: an organized collection of address ranges that the process may read, write, or execute. Some ranges contain program instructions, global variables, dynamic allocations, shared libraries, and thread stacks. Other ranges are deliberately left inaccessible.

This gives every process a private memory view. The program can use pointers as though it owns a large, orderly range of memory, while the operating system controls which parts of that range are valid and what operations each part permits.

A process address space is the process-specific map that gives meaning and access rights to memory addresses.

Understanding that map makes pointer values, segmentation faults, executable layouts, and tools such as /proc/<pid>/maps much easier to reason about.

Process Context for Addresses

Suppose Process A and Process B both contain a variable at address 0x6000:

The equal pointer values do not imply shared storage. Each process has its own address-space map, so the operating system can associate the two uses of 0x6000 with different memory.

This is why one process cannot normally corrupt another process merely by guessing one of its pointer values. A pointer created by Process A has no general meaning when copied as a raw number into Process B.

Deliberate sharing is possible, but it requires both processes to have a suitable shared region in their address spaces. Even then, the shared region does not need to appear at the same numeric address in both processes.

The address-space boundary therefore provides two useful properties:

  • Isolation: ordinary memory in one process is inaccessible to unrelated processes.
  • Independent layout: each process can organize its addresses without coordinating every pointer value with every other process.

Threads inside one process are different. They execute within the same process address space, so they can access the same code, globals, and dynamic memory. Each thread has its own stack, but those stacks are still regions inside one shared process address space.

The Address Space as a Region Map

An address space is not one uninterrupted array that a process may freely use from beginning to end. It is better understood as a set of regions, also called mappings.

Each region describes at least:

  • A starting address
  • An ending address
  • The operations permitted in that range
  • The content or object associated with the range

A region written as:

normally uses a half-open interval. It contains addresses from 0x4000 up to, but not including, 0x5000. Its length is therefore:

The space between valid regions may be unmapped. If a process tries to access an unmapped address, the CPU transfers control to the operating system, which normally reports an invalid memory access to the process. On Linux, this commonly ends in SIGSEGV.

Mapped does not mean “all accesses allowed.” A region can permit reads but reject writes, or permit instruction execution but reject ordinary modification. Both the range and its permissions matter.

This leads to a practical model for every memory access:

All three inputs are required. The same numeric address is valid or invalid depending on which process asks and what it is trying to do.

A Typical Process Layout

The exact layout varies by operating system, CPU architecture, executable format, runtime, and individual execution. Still, a native Linux process commonly contains regions with the following broad organization:

This diagram is a conceptual layout, not a fixed template.

The heap and stack arrows show common growth directions in traditional diagrams. They do not promise that every dynamic allocation sits in one continuously growing heap or that every stack begins at one universal address. Large allocations may appear in separate mapped regions, and additional threads introduce additional stacks.

On a 64-bit machine, the possible address range is enormous compared with most process needs. Large gaps between regions are normal. A process does not gain usable memory merely because a number falls somewhere between the lowest and highest possible addresses.

The diagram shows the process-accessible, user-space portion of the layout. Some architectures reserve another part of the possible address range for the operating-system kernel. Code running in user mode cannot treat kernel memory as another ordinary process region. The exact division and which kernel ranges are present while user code runs are system-specific.

Loading simulation...

Regions Loaded from the Executable

A native executable contains instructions and initial data that the operating system and program loader use to construct part of the address space.

Consider these declarations:

They have different runtime needs.

Executable code

The compiled instructions for functions such as handle_request() are commonly placed in a text region. It is readable and executable, but normally not writable.

Rejecting writes protects the instructions from accidental modification. It also prevents an attacker who has found an ordinary data-writing bug from directly rewriting program code in place.

Multiple processes running the same executable still have independent address spaces. The operating system may optimize storage for identical read-only content behind the scenes, but each process sees the code through its own mapping.

Read-only data

Constants and string literals are commonly placed in a read-only region:

The pointer variable status may itself be writable, depending on where it is declared. The characters in the literal "ready" are not writable through that pointer.

This distinction matters:

The first operation updates the pointer. The type system rejects the second operation. Removing the const qualifier or forcing a cast does not make the literal writable; if the resulting program attempts the store, the read-only mapping will commonly cause it to receive SIGSEGV.

Initialized writable data

Writable global and static variables with explicit nonzero initial values are commonly stored in a data region:

The executable provides the initial bytes. Each newly started process gets writable runtime state initialized from those bytes. Changing active_connections in memory does not normally rewrite the executable file.

Zero-initialized data

Global and static variables without an explicit initializer, or with an initializer of zero, belong to a region traditionally called BSS:

These objects begin filled with zero. The executable does not need to store a megabyte of zero bytes for scratch_buffer. It can record the required size, allowing the process image to receive zero-filled memory at startup.

At runtime, initialized data and BSS are both writable process memory. The distinction mainly explains where their initial contents come from and why a large zero-initialized array does not increase the executable file by the array's full size.

Regions Created While the Process Runs

The address space is not frozen after startup. It changes as the process allocates memory, loads libraries, creates threads, and releases resources.

The traditional heap

The heap is a region used to serve dynamic memory requests. Objects obtained through operations such as malloc() can outlive the function call that created them and remain available until the program releases them.

Here, the pointer variable request may live in the current function's stack frame, while the struct request object it identifies lives in dynamically managed memory.

The traditional heap can expand toward higher addresses. However, modern allocators may also obtain separate anonymous mappings, especially for large requests. “Dynamically allocated” therefore describes an object's lifetime and management; it does not guarantee that the object's address falls inside the single region labeled [heap].

Mapped regions

A process can contain regions associated with shared libraries, files, shared-memory objects, or anonymous memory that has no pathname.

Shared libraries need executable regions for their instructions and data regions for their runtime state. A single library can therefore appear as several adjacent entries with different permissions.

Mapped files make file content available through an address range. Anonymous mappings provide zero-initialized memory without a file identity. At the address-space level, both are ranges with defined permissions and lifetimes.

The essential point here is organizational: not all process memory comes from the executable, stack, or traditional heap. The middle of a typical address space often contains many independently managed mappings.

Thread stacks

Every thread needs a stack for function calls, local variables, saved return locations, and other call state. The initial thread receives a stack during process startup. Creating another thread normally adds another stack region to the same process address space.

For the initial thread, startup information such as command-line arguments and environment strings is also commonly placed near the initial stack.

Stacks often expand toward lower addresses. The operating system or threading runtime normally places inaccessible guard space near a stack boundary so that some stack overflows fail promptly instead of silently running into an adjacent mapping.

A stack is not inherently “the high end of all memory.” It is one mapping with a configured size and boundary. A process with many threads has many stack regions.

Region Roles from Memory Permissions

Operating systems commonly describe region permissions with three letters:

  • r permits reading bytes.
  • w permits modifying bytes.
  • x permits executing bytes as instructions.

The absence of a permission is as important as its presence. A region written as r-x can be read and executed but not modified. A region written as rw- can hold changing data but cannot be used as executable code.

A simplified layout might look like this:

Keeping writable and executable permissions separate reduces both accidental damage and security risk. Code is normally executable but not writable. Stacks and ordinary data are normally writable but not executable.

Permissions are enforced for the operation being attempted. Reading an instruction region may be allowed even though writing it is not. Jumping to an address in a writable stack can fail because instruction execution is not permitted there.

The operating system enforces regions, not C language rules. If two arrays occupy the same writable mapping, writing beyond one array and into the other may not cross a protected boundary. The access can succeed from the operating system's point of view while corrupting the program.

Benefits of Unmapped Gaps and Guard Regions

An unmapped gap is not wasted physical memory. It is simply a range for which the process currently has no valid mapping.

Gaps serve several purposes. They leave room for some regions to change size, make layouts less predictable, separate unrelated areas, and provide boundaries that catch invalid accesses.

Consider a thread stack followed by an inaccessible guard region:

If excessive call depth or a very large local variable pushes the stack into the guard region, the next access fails. The guard does not prevent all possible stack corruption, but it creates a hard boundary that catches growth beyond the configured stack.

The zero address is also normally left unmapped. This helps turn many null-pointer dereferences into immediate failures:

The process tries to read at or near address zero, finds no readable mapping, and is normally terminated rather than silently reading arbitrary data.

Address-Space Construction and Evolution

When a native program begins, the operating system creates a new address space and uses the executable's metadata to establish its initial regions. A runtime loader may add shared libraries. The startup code receives arguments and environment data and then calls the program's entry function.

During execution, the map evolves:

  1. Dynamic allocation may expand an existing region or add a new one.
  2. Releasing memory may make a region reusable or remove a mapping.
  3. Loading a library adds code and data mappings.
  4. Creating a thread adds a stack.
  5. Mapping a file adds a file-associated range.

When the process exits, the operating system discards its address-space mappings. The executable and ordinary files remain on storage, but the process-specific pointers and writable runtime state no longer exist.

Replacing the program image also replaces most of the old address space. The process starts executing the new program with a newly constructed layout; pointers into the previous program image are no longer meaningful.

Instability of Absolute Addresses

Diagrams often show code at a memorable low address and a stack near a memorable high address. Real programs should not depend on those exact values.

Modern systems deliberately vary the placement of major regions between executions. This security technique is called address-space layout randomization, or ASLR. The executable, shared libraries, heap, and stack can appear at different addresses each time a program starts.

For example, the same local variable might appear at:

The variable is still on the stack in each run. Its exact address changes because the stack's placement changes.

Programs work despite this variation because compiled code, loaders, and runtimes use relocation and relative relationships rather than assuming that every object has one permanent absolute address.

ASLR also explains why addresses printed in examples rarely match addresses on your machine. The region roles and permission patterns are more useful than memorizing particular hexadecimal values.

Inspecting a Process Address Space on Linux

Linux exposes a process's current mappings through:

Each line describes one mapped address range. A shortened example looks like this:

The fields have the following meanings:

FieldMeaning
Address rangeStart address and exclusive end address
PermissionsRead, write, execute, plus private or shared mapping mode
OffsetLocation within the associated file where the mapping begins
Device and inodeIdentity of the backing file, when one exists
Path or labelFile name or a label such as [heap] or [stack]

In the permissions field, p means updates use private mapping semantics, while s means updates use shared mapping semantics. A dash means the corresponding capability is absent.

The example executable appears several times because its code, read-only data, and writable data need different permissions. The C library appears as its own set of mappings. Heap and stack entries have descriptive labels rather than ordinary backing-file paths.

An empty pathname is normal for anonymous memory. Not every stack is necessarily labeled [stack], and exact labeling depends on the kernel and process state.

The maps file describes ranges, not individual variables. To classify a printed pointer, find the line whose start address is less than or equal to the pointer and whose end address is greater than the pointer.

Hands-On: Placing C Objects in the Map

The following program prints addresses representing the major parts of a native process. It then waits so that you can inspect its mappings from another terminal.

Save the file as address_space_demo.c and compile without optimization so that the example remains easy to inspect:

Run it in the first terminal:

The program prints a PID and waits. In a second terminal, replace 31415 with that PID:

Match each printed address to a range:

  • code_marker should fall in an executable mapping for the program.
  • read_only_global and the string literal should fall in a read-only program mapping.
  • The initialized and zero-initialized globals should fall in a writable program mapping.
  • The heap allocation will fall in writable dynamic memory, commonly [heap] for this small request.
  • The heap_buffer pointer variable and stack_local should fall in the stack mapping.

The pointer variable and the object it points to are intentionally printed separately:

They belong to different regions because they are different objects with different lifetimes.

Run the program several times and compare the addresses. Their exact values will likely change, but their region types and permission requirements remain consistent.

If a compiler or allocator places an object differently from the expected entry, trust the observed mapping. The C language does not require a particular Linux section name or heap implementation. The experiment shows a common implementation, not a language guarantee.

Press Enter in the first terminal when you are finished.

Executable Sections vs. Runtime Mappings

Tools such as readelf show sections recorded in an ELF executable:

You may see names such as .text, .rodata, .data, and .bss. These describe how the executable's code and data are organized for linking and loading.

/proc/<pid>/maps, by contrast, shows the address ranges that exist in a running process. It reports mapping boundaries and permissions. Several compatible executable sections can be covered by one runtime mapping, and alignment can leave padding between meaningful content.

The useful relationship is:

ELF sections describe content in the program file, and the loader turns them into process mappings that describe accessible ranges at runtime.

Do not expect one readelf section line to correspond to exactly one /proc/<pid>/maps line. They describe related objects at different levels.

Address-Space Maps vs. Memory-Usage Totals

The address-space map answers which ranges exist and how the process may access them. It does not say that every byte in every listed range currently occupies a unique byte of main memory.

Some mapped content may not have been accessed. Some read-only content may be safely shared. A large reserved range may contain only a small amount of actively used data.

For that reason, subtracting the first address in /proc/<pid>/maps from the last address is not a meaningful measure of memory consumption. Even summing every mapped range answers only how much address range is mapped, not how much main memory the process is actively using.

Keep the distinction precise:

The address space describes the process's view and access rules; it is not a direct inventory of occupied RAM.

Using the Map to Diagnose an Invalid Pointer

Suppose a crash report identifies a faulting address:

The value is close to zero, which suggests that code may have dereferenced a null pointer and then accessed a field at offset 0x10. The process map confirms that no readable region covers the address.

For a less obvious address, use the same disciplined approach:

  1. Find whether any mapping contains the address.
  2. Check whether that mapping permits the attempted operation.
  3. Identify the region's role: code, stack, dynamic memory, library, or file mapping.
  4. Compare that role with what the program expected the pointer to identify.

No containing region suggests a wild pointer, null-derived pointer, released mapping, or corrupted address. A permission mismatch suggests an operation such as writing to read-only data or executing data-only memory.

A containing writable region does not prove the program is correct. A dangling pointer can still refer to an address that remains mapped, and an out-of-bounds access can land inside the same writable region as another object. Address-space protection operates at region boundaries; language-level ownership and bounds require separate correctness.

Summary

A process address space is a process-specific map of address ranges. It gives pointer values meaning, isolates ordinary memory between processes, and records whether each mapped region may be read, written, or executed. Unmapped ranges and permission boundaries turn many invalid accesses into controlled process failures.

A typical native process contains executable code, read-only constants, writable initialized and zero-initialized globals, dynamic memory, shared-library and file mappings, and one stack per thread. This organization is common rather than universal: exact addresses and placement vary across systems and executions.

On Linux, /proc/<pid>/maps reveals the current runtime layout. Matching a pointer to its containing range and permissions is a practical first step when reasoning about object placement or diagnosing an invalid access.

Quiz

The Process Address Space Quiz

5 quizzes