Executable Layout and Process Memory
When a program runs, the operating system gives it a private, imaginary memory — its address space — and arranges things inside it in regions: one for the machine code, one for constants, one for global variables, a growing area for things created at run ti…
Learning Objectives
Distinguish a program image, a process and a thread.
Describe a typical virtual address space: text, rodata, data, bss, heap, memory mappings, shared libraries and stack.
Locate environment variables, command-line arguments and the initial process state.
Explain ASLR, guard pages and read/write/execute permissions.
Recognise how layouts differ across operating systems, architectures, toolchains and embedded systems (linker scripts, flash versus RAM).
Plain-Language Explanation
When a program runs, the operating system gives it a private, imaginary memory — its address space — and arranges things inside it in regions: one for the machine code, one for constants, one for global variables, a growing area for things created at run time (the heap), an area for each function call's temporary values (the stack), and places where shared libraries and files are mapped. Each region has permissions: code can be executed but not written, data can be written but not executed.
Precise Technical Explanation
Program image versus process The program image is the executable file: headers plus sections. A process is a running instance: an address space built from that image, plus registers, open files, a process ID and other kernel state. Ten copies of a program are ten processes sharing one image on disk (and, thanks to page sharing, mostly one copy of the code in RAM).
Process versus thread A thread is one flow of execution: its own registers, program counter and stack. All threads of a process share the address space — code, globals, heap and mappings. A process starts with one thread; more can be created (Module 14).
Virtual address space Each process sees addresses from 0 to a platform maximum. These are virtual addresses: the hardware memory-management unit translates them to physical RAM page by page (Module 12). Two processes can use the same virtual address for different data.
Typical process layout (platform-dependent) On x86-64 Linux with a position-independent executable (low addresses at the top of this list):
Typical address range (example) 0x55… / 0x56… constants, string literals Executable data + bss globals and statics just above the bss small malloc blocks mmap area / shared libraries libc, ld-linux, large allocations, mapped files, thread stacks Main thread stack call frames, argv, environment near the stack kernel-provided fast system calls
Windows, macOS, 32-bit systems and embedded targets arrange regions differently; only the *kinds* of region are universal.
Text/code segment Machine instructions. Mapped read-and-execute, shared between all processes running the same file.
Read-only data (.rodata) String literals, const globals with constant initialisers, jump tables. Mapped read-only, so an attempt to write a string literal crashes with a segmentation fault instead of silently corrupting a shared constant.
Initialized global/static data (.data) Globals and statics with non-zero initialisers. Their initial values are stored in the file and copied (actually mapped copy-on-write) into the process.
Zero-initialized global/static data (.bss) Globals and statics without initialisers or initialised to zero. The file stores only the size; the kernel provides zero-filled pages.
Heap and dynamic allocations malloc obtains memory from the OS in large chunks and divides it. glibc extends the classic heap with the brk system call for small requests and uses separate mmap regions for large requests (by default ≥ 128 KiB). The heap therefore is not one contiguous region in modern systems.
Stack and call frames Each function call pushes a frame with its saved return address, saved registers and locals (Module 5). On x86-64 and most architectures the stack grows toward *lower* addresses. The main thread's stack is limited by ulimit -s (commonly 8 MiB on Linux, 1 MiB by default on Windows).
mmap maps files or anonymous memory anywhere in the address space: shared libraries, large heap blocks, thread stacks, memory-mapped files and shared memory all live here.
Each shared library is mapped as several regions — code (r-x), read-only data (r--), and writable data (rw-) — exactly like the executable itself.
Environment variables and command-line arguments On Linux the kernel copies the argument strings, environment strings and an *auxiliary vector* (page size, entry point, random bytes for stack canaries…) to the top of the new stack. _start passes them to main as argc, argv and envp.
Initial process state At the first instruction (_start): registers mostly zero, the stack pointer pointing at argc, the dynamic loader already run (for dynamic executables), and the C library not yet initialised. Start-up code initialises the C library, runs constructors and calls main (Module 10).
Address-space layout randomization (ASLR) The kernel randomises the base of the executable (for PIE), heap, mmap area, libraries and stack at every run, so attackers cannot predict addresses. You can see it by running the same program twice (below).
Unmapped or inaccessible pages placed next to stacks so that running off the end causes an immediate fault instead of silently overwriting a neighbouring region. Thread libraries put a guard page below each thread stack; Linux keeps a gap below the main stack.
Read, write and execute permissions Each mapping has r/w/x bits (r-xp in /proc/self/maps; p = private, s = shared). The W^X policy — never writable and executable at once — is enforced by the NX/XD hardware bit. JIT compilers must flip permissions or use dual mappings to generate code.
Memory layout differences across operating systems, architectures and toolchains
32-bit processes have only 2–4 GiB of address space, so regions are much closer together.
Windows places the image near 0x7ff6… for 64-bit PE files with ASLR and uses a different heap manager.
Some architectures (historically PA-RISC) grow the stack upward.
Non-PIE executables load at a fixed address (0x400000 on x86-64 Linux).
Embedded linker-script sections and memory maps On a microcontroller there is no loader: the linker script assigns every section a physical address range.
Flash versus RAM placement in embedded systems Code and constants execute from flash; .data has *two* addresses — its load address (LMA) in flash, where the initial values are stored, and its run address (VMA) in RAM, where start-up code copies them. .bss is cleared in RAM. Module 16 implements this start-up sequence.
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K /* illustrative STM32-style values */
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : { *(.text*) *(.rodata*) } > FLASH
.data : { *(.data*) } > RAM AT > FLASH /* runs from RAM, initial values stored in flash */
.bss : { *(.bss*) *(COMMON) } > RAM
}Why It Matters in Real Systems
Crash addresses tell you which region was touched: 0x0 (null), 0x55… (your code or globals), 0x7ffc… (stack), 0x7f… (a library or big allocation). Memory budgets, security hardening and every "where does this variable live?" question rely on the layout.
Key Terms
The range of virtual addresses a process can use Segment / mapping
A contiguous range with one set of permissions .text, .rodata, .data, .bss code, constants, initialised data, zeroed data
Memory managed by the allocator (malloc/new) Per-thread memory for call frames
Position-independent executable, loadable at any base Randomised placement of regions at each run
Inaccessible page that turns overruns into faults Never writable and executable at the same time
Load address / run address of a section (embedded)
Memory Layout and Execution Flow
memory-mapSyntax
Placement in sections is a common implementation pattern, not a language rule. C defines storage durations (static, automatic, allocated, thread); "stack" and "heap" are how implementations usually provide them.
const int table[] = {1, 2, 3}; /* typically .rodata */
int counter = 1; /* typically .data */
int total; /* typically .bss */
void f(void) {
static int calls; /* .bss, static storage duration */
int local; /* automatic: register or stack frame */
int *p = malloc(sizeof *p); /* allocated: heap */
}Complete Runnable Example
The program prints one address from every region and then the process's own memory map. GCC 11.2, x86-64 Linux (PIE, ASLR on).
Section sizes, section permissions and ASLR, inspected with binutils:
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const int ro_table[4] = {1, 2, 3, 4}; /* typically .rodata */
int global_initialized = 10; /* typically .data */
int global_zeroed; /* typically .bss */
static int file_private = 20; /* static storage, internal linkage */
static void show(const char *what, uintptr_t address)
{
printf("%-34s 0x%012" PRIxPTR "\n", what, address);
}
int main(int argc, char *argv[])
{
static int static_local; /* static storage duration, .bss */
int local = 30; /* automatic storage duration */
int *small = malloc(sizeof *small); /* small request: brk heap */
char *large = malloc(1024 * 1024); /* 1 MiB: glibc uses a separate mmap */
if (small == NULL || large == NULL) {
return 1;
}
*small = 40;
show("code main", (uintptr_t)&main);
show("rodata string literal", (uintptr_t)"literal");
show("rodata const ro_table", (uintptr_t)ro_table);
show("data global_initialized", (uintptr_t)&global_initialized);
show("data file_private", (uintptr_t)&file_p
…echo "--- size (text includes rodata) ---"; size m03_layout
echo "--- selected sections: name, type, address, size, flags ---"
readelf -S -W m03_layout | awk '$2 ~ /^\.(text|rodata|data|bss)$/ {print $2, $3, $4, $6, $8}'
echo "--- the symbols we placed ---"
nm m03_layout | grep -E ' (ro_table|global_initialized|global_zeroed|file_private|static_local)'
echo "--- ASLR: main and the stack move on every run ---"
for i in 1 2 3; do ./m03_layout | grep -E 'code main|stack local'; doneLine-by-Line Explanation
show prints addresses as uintptr_t — converting a function pointer to void * is not portable ISO C, converting to an integer type is implementation-defined and supported by GCC.
ro_table and the literal land in the read-only region; global_initialized and file_private in .data; global_zeroed and static_local in .bss — all close together, just above the code.
small comes from the brk heap (the [heap] line); large is above 128 KiB, so glibc gives it its own anonymous mapping near the libraries.
&printf points into libc.so.6's r-x mapping.
local and the argv[0] string are in [stack].
/proc/self/maps is Linux's live view of the address space: range, permissions, offset, device, inode and file.
In readelf -S flags, AX = allocate+execute, A = allocate (read-only), WA = writable; .bss has type NOBITS — no bytes in the file.
Expected Output
Output (one real run — addresses change on every run because of ASLR):
code main 0x611a3a49e26f
rodata string literal 0x611a3a49f03f
rodata const ro_table 0x611a3a49f010
data global_initialized 0x611a3a4a1010
data file_private 0x611a3a4a1014
bss global_zeroed 0x611a3a4a1024
bss static_local 0x611a3a4a1028
heap small malloc 0x611a6e14b2a0
mmap 1 MiB malloc 0x7150402d6010
libc printf 0x715040060770
stack local 0x7ffeecf22bc4
stack top argv[0] string 0x7ffeecf24572
values: 1 10 0 30 40 argc=1
/proc/self/maps:
611a3a49d000-611a3a49e000 r--p 00000000 00:46 275564002199744734 m03_layout
611a3a49e000-611a3a49f000 r-xp 00001000 00:46 275564002199744734 m03_layout
611a3a49f000-611a3a4a0000 r--p 00002000 00:46 275564002199744734 m03_layout
611a3a4a0000-611a3a4a1000 r--p 00002000 00:46 275564002199744734 m03_layout
611a3a4a1000-611a3a4a2000 rw-p 00003000 00:46 275564002199744734 m03_layout
611a6e14b000-611a6e16c000 rw-p 00000000 00:00 0 [heap]
715040000000-715040028000 r--p 00000000 08:30 12497
…--- size (text includes rodata) ---
text data bss dec hex filename
3321 672 24 4017 fb1 m03_layout
--- selected sections: name, type, address, size, flags ---
.text PROGBITS 0000000000001150 0003e7 AX
.rodata PROGBITS 0000000000002000 00018c A
.data PROGBITS 0000000000004000 000018 WA
.bss NOBITS 0000000000004018 000018 WA
--- the symbols we placed ---
0000000000004014 d file_private
0000000000004010 D global_initialized
0000000000004024 B global_zeroed
0000000000002010 R ro_table
0000000000004028 b static_local.0
--- ASLR: main and the stack move on every run ---
code main 0x5e9bca13f26f
stack local 0x7fffe50e7b14
code main 0x5ad9dfd9826f
stack local 0x7ffccbf46b14
code main 0x5f9261a1426f
stack local 0x7ffc737b8684Step-by-Step Execution Trace
The kernel maps the five executable mappings (headers, r-x code, r-- rodata, r-- RELRO, rw- data) at a random base, maps ld-linux, builds the stack with argv/envp/auxv, and jumps to the loader.
The loader maps libc.so.6, relocates, and jumps to _start → __libc_start_main → main.
malloc(4) makes the first brk call: the [heap] mapping appears right after the bss.
malloc(1 MiB) calls mmap for a fresh anonymous region.
Reading /proc/self/maps makes the kernel generate the listing on the fly.
Note that the 1 MiB block's mapping appears as an anonymous rw-p line next to the libraries.
Interactive Visualization
The memory map explorer above lets you click each region to see an example item, its address range, size, permissions, lifetime and explanation. Switch between "Typical Linux x86-64" and a customised conceptual layout; positions and growth directions are illustrative.
Common Mistakes and Failure Modes
Modifying a string literal (char *s = "abc"; s[0] = 'X';) — crashes because .rodata is read-only.
Returning a pointer to a local variable — the frame is gone after return (Module 4).
Assuming the heap is one contiguous region or that addresses are stable between runs.
Assuming 8 MiB of stack is available on every platform and in every thread.
Allocating huge arrays as locals (char buf[16 << 20];) — stack overflow.
Corrected / Safe Pattern
char s[] = "abc"; /* a writable array initialised from the literal */
s[0] = 'X';
char *big = malloc(16u << 20); /* large buffers come from the heap, with a NULL check */Debugging and Diagnostic Techniques
/proc/<pid>/maps, /proc/<pid>/smaps (per-mapping RSS), and pmap -x <pid> on Linux; vmmap on macOS; VMMap (Sysinternals) on Windows.
size, readelf -S, objdump -h for section sizes; nm --size-sort for the biggest symbols.
A crash address can be matched against the maps listing to find the region.
Performance Implications
Code and read-only data are shared between processes, so fifty copies of a program cost little extra RAM. Large heap requests via mmap are returned to the OS on free; small ones are reused inside the process. Big .data sections cost start-up time; big .bss sections cost only when touched (demand paging, Module 12).
Security and Safety Implications
ASLR, W^X/NX, read-only relocations (RELRO), guard pages and stack canaries are layered defences that make memory-corruption bugs much harder to abuse. They do not fix bugs; they only raise the cost of exploitation. Keep them enabled.
Platform and Language Caveats
The order and addresses above are what this Linux build produced. Windows, macOS, Android, 32-bit systems and different kernels place regions differently.
C's storage durations are the language concept; sections and the stack/heap are implementation mechanisms. An optimizer may keep local only in a register and never give it a stack address — taking its address (&local) forces it into memory.
Java, C#, JavaScript and Python processes have the same OS-level regions plus a garbage-collected managed heap allocated inside mmap regions.
Real-World Application
Embedded engineers read linker map files to check flash and RAM budgets; operations teams read /proc/<pid>/smaps to explain why a service uses 2 GB; security teams verify that binaries are PIE with full RELRO and NX.
Best Practices Checklist
[ ] Keep large buffers off the stack.
[ ] Never write through pointers to string literals; declare them const char *.
[ ] Do not hard-code addresses on hosted systems.
[ ] Keep PIE, NX, RELRO and ASLR enabled in release builds.
[ ] On embedded targets, check the map file after every build.
Exercises
E1 · Beginner. In which region do these live on Linux: (a) const char *s = "hi"; — the characters, (b) the pointer s if it is a global, (c) static int n; inside a function?
Answer: (a) .rodata, (b) .data (it has a non-zero initial value — the address), (c) .bss.
E2 · Intermediate. The example's 1 MiB block was not inside [heap]. Why, and what changes if you allocate 64 KiB instead?
Answer: glibc's M_MMAP_THRESHOLD defaults to 128 KiB; larger requests get their own mmap. A 64 KiB request is served from the brk heap and appears inside [heap].
E3 · Advanced challenge. Run the program with setarch -R ./m03_layout (or setarch $(uname -m) -R) and compare the addresses across several runs. What does -R do, and why must you never ship software that depends on it?
Answer: -R (ADDR_NO_RANDOMIZE) disables ASLR for that process, so the addresses repeat. Depending on fixed addresses breaks under normal ASLR and removes an important security mitigation.
Quiz
? Where does a zero-initialised global variable usually live?
- .data :: .data holds non-zero initial values stored in the file.
+ .bss :: .bss is zero-filled at load time and takes no file space.
- .rodata :: Only read-only data goes there.
- The stack :: Globals have static storage duration, not automatic.
? Which statements about ASLR are true? (select all that apply)
+ It randomises the base addresses of regions at each run. :: That is its definition.
- It prevents buffer overflows. :: It only makes exploiting them harder.
+ PIE executables are needed to randomise the program's own code. :: Non-PIE executables load at a fixed address.
- It changes addresses while the program runs. :: Addresses are chosen at load/mapping time.
? What do the permissions r-xp on a mapping mean?
+ Readable, executable, not writable, private :: Typical for machine code.
- Readable, writable, executable :: There is no w.
- Read-only data shared between processes :: p means private; x means executable.
- A guard page :: Guard pages have no access (---p).Summary
A process has a private virtual address space divided into regions with permissions: code, read-only data, initialised data, bss, heap, mmap regions with shared libraries, and one stack per thread. The layout is platform-dependent and randomised by ASLR. Embedded systems fix the layout in a linker script and split it between flash and RAM.
References and Further Reading
man 5 proc (the /proc/[pid]/maps format) and man 2 mmap.
Bryant & O'Hallaron, *CS:APP*, chapters 7 (Linking) and 9 (Virtual Memory).
*ELF-64 Object File Format* specification; Arm "Linker script" documentation for GNU ld.