Memory Layout & Architecture

Overview

Every running process has a virtual address space divided into segments with different purposes and permissions. Understanding this layout is the foundation of exploit development — you need to know where your input lands, where the return address lives, and which memory regions are executable.

This file covers x86-64 Linux process memory. The concepts apply broadly to x86 (32-bit) and other Unix-like systems with minor differences in address sizes and calling conventions.

Process Memory Segments

  High addresses
┌──────────────────────┐
│     Kernel space     │  (inaccessible from userland)
├──────────────────────┤
│        Stack         │  ↓ grows downward
│     (local vars,     │
│      return addrs,   │
│      saved regs)     │
├──────────────────────┤
│          ↕           │  (unmapped gap)
├──────────────────────┤
│        Heap          │  ↑ grows upward
│     (malloc/free)    │
├──────────────────────┤
│        BSS           │  (uninitialized globals, zeroed)
├──────────────────────┤
│        Data          │  (initialized globals)
├──────────────────────┤
│        Text          │  (executable code, read-only)
├──────────────────────┤
  Low addresses

Text Segment

Contains the compiled machine code of the program. Mapped read-only and executable. On non-PIE binaries, the text segment loads at a fixed address (typically 0x400000 on x86-64 Linux).

Data and BSS Segments

  • Data — initialized global and static variables (int x = 42;)
  • BSS — uninitialized global and static variables (int y;), zeroed at load

Both are readable and writable but not executable.

Heap

Dynamically allocated memory via malloc(), calloc(), realloc(). Grows upward from low addresses. Managed by the allocator (glibc ptmalloc2 on Linux).

Stack

Stores function call frames — local variables, saved registers, return addresses. Grows downward from high addresses. Each function call pushes a new frame; each return pops it.

Examining Memory Layout

# View memory mappings of a running process
cat /proc/<pid>/maps

# Or use pmap for a cleaner view
pmap <pid>

Example output from /proc/self/maps:

00400000-00401000 r--p  /tmp/binary     ← ELF header
00401000-00402000 r-xp  /tmp/binary     ← text (executable)
00402000-00403000 r--p  /tmp/binary     ← rodata
00403000-00404000 r--p  /tmp/binary     ← data (read-only relocations)
00404000-00405000 rw-p  /tmp/binary     ← data/bss (writable)
00405000-00426000 rw-p  [heap]          ← heap (brk-based)
7ffff7d00000-...  r-xp  /lib/libc.so.6  ← shared library
7ffffffde000-...  rw-p  [stack]         ← stack

The permission flags (r-xp, rw-p) show read/write/execute for each region.

With GDB

# GDB
# https://www.gnu.org/software/gdb/
gdb ./binary

# Inside GDB:
(gdb) info proc mappings
(gdb) maintenance info sections

With pwndbg loaded, the vmmap command provides a colored, annotated memory map:

# pwndbg
# https://github.com/pwndbg/pwndbg
# Inside GDB with pwndbg:
pwndbg > vmmap

x86-64 Registers

General-Purpose Registers

Register Purpose Caller/Callee Saved
RAX Return value, syscall number Caller
RBX General purpose Callee
RCX 4th argument (function calls), counter Caller
RDX 3rd argument Caller
RSI 2nd argument Caller
RDI 1st argument Caller
RBP Base pointer (frame pointer) Callee
RSP Stack pointer Callee
R8 5th argument Caller
R9 6th argument Caller
R10-R11 General purpose Caller
R12-R15 General purpose Callee

Special Registers

  • RIP — instruction pointer (next instruction to execute)
  • RFLAGS — status flags (zero, carry, sign, overflow)

In exploit development, controlling RIP is the primary goal — it determines what code executes next.

System V AMD64 Calling Convention (Linux)

Function arguments are passed in registers, then on the stack:

  1. RDI — 1st argument
  2. RSI — 2nd argument
  3. RDX — 3rd argument
  4. RCX — 4th argument
  5. R8 — 5th argument
  6. R9 — 6th argument
  7. Additional arguments pushed onto the stack (right to left)

Return value goes in RAX (and RDX for 128-bit returns).

Linux Syscall Convention

Syscalls use a different register mapping than function calls:

Register Purpose
RAX Syscall number
RDI 1st argument
RSI 2nd argument
RDX 3rd argument
R10 4th argument (not RCX)
R8 5th argument
R9 6th argument

Invoke with the syscall instruction. Return value in RAX.

Common syscall numbers (x86-64):

Syscall Number Arguments
read 0 fd, buf, count
write 1 fd, buf, count
open 2 filename, flags, mode
execve 59 filename, argv, envp
dup2 33 oldfd, newfd
socket 41 domain, type, protocol
connect 42 sockfd, addr, addrlen

Stack Frame Layout

When a function is called, the stack frame looks like this (x86-64):

  High addresses (bottom of stack)
┌──────────────────────┐
│   Caller's frame     │
├──────────────────────┤
│   Arguments > 6      │  (pushed right to left, if any)
├──────────────────────┤
│   Return address     │  ← pushed by CALL instruction
├──────────────────────┤
│   Saved RBP          │  ← pushed by function prologue
├──────────────────────┤  ← RBP points here
│   Local variable 1   │
│   Local variable 2   │
│   ...                │
│   Buffer             │
├──────────────────────┤  ← RSP points here
  Low addresses (top of stack)

Function Prologue and Epilogue

; Prologue — set up frame
push rbp           ; save caller's base pointer
mov  rbp, rsp      ; set new base pointer
sub  rsp, 0x40     ; allocate 64 bytes for local variables

; ... function body ...

; Epilogue — tear down frame
leave              ; equivalent to: mov rsp, rbp; pop rbp
ret                ; pop return address into RIP

The ret instruction pops the top of the stack into RIP. If an attacker overwrites the saved return address on the stack, ret will jump to the attacker-controlled address.

GDB Quick Reference for Memory Inspection

# GDB
# https://www.gnu.org/software/gdb/
gdb ./binary

# Set Intel syntax (easier to read for exploit dev)
(gdb) set disassembly-flavor intel

# Disassemble a function
(gdb) disassemble main
(gdb) disassemble vulnerable

# Set a breakpoint
(gdb) break *0x401150
(gdb) break vulnerable

# Run with arguments
(gdb) run AAAA

# Examine registers
(gdb) info registers
(gdb) info registers rsp rbp rip

# Examine memory
(gdb) x/20xw $rsp          # 20 words in hex at RSP
(gdb) x/10xg $rsp          # 10 giant (8-byte) words at RSP
(gdb) x/s $rdi             # string at RDI
(gdb) x/10i $rip           # 10 instructions at RIP

# Step execution
(gdb) si                    # single instruction
(gdb) ni                    # next instruction (skip calls)
(gdb) c                     # continue

# Print expression
(gdb) print $rbp - $rsp     # distance between frame pointers
(gdb) print/x $rsp          # RSP in hex

x86 vs x86-64 Key Differences

Feature x86 (32-bit) x86-64 (64-bit)
Address size 4 bytes 8 bytes
Register names EAX, EBX, ESP, EBP, EIP RAX, RBX, RSP, RBP, RIP
Calling convention Arguments on stack First 6 in registers
Stack alignment 4-byte 16-byte (before CALL)
Syscall instruction int 0x80 syscall
Return address size 4 bytes 8 bytes
Packing function p32() in pwntools p64() in pwntools

On x86-64, addresses often contain null bytes (e.g., 0x0000000000401234), which complicates exploits that use string functions — strcpy stops at null.

References

Tools

Official Documentation