Understanding where your code, data, and stack live in memory is critical for debugging. When you see a crash at address 0x7ffd..., you know it's on the stack. When you see 0x55555..., it's in your code segment. A core dump shows you each of these regions — knowing the layout tells you what went wrong.
A 64-bit Linux process looks like this in memory (addresses grow upward):
High address (0x7fff...) ┌─────────────────────┐ │ Stack │ ← argc, argv, envp, local variables │ ↓ │ (grows downward) │ │ │ ↑ │ │ Heap │ ← malloc() allocations (grows upward) ├─────────────────────┤ │ mmap region │ ← shared libraries, mmap() allocations │ ↓ │ ├─────────────────────┤ │ .bss │ ← zero-initialized globals │ .data │ ← initialized globals │ .rodata │ ← read-only data (string literals) │ .text │ ← your compiled code (executable) └─────────────────────┘ Low address (0x5555...)
The stack and heap grow toward each other but rarely collide (the stack is usually limited to 8MB by default).
You can inspect the actual layout of any running process:
# See your own process's memory map cat /proc/self/maps # Output: # 555555554000-555555555000 r--p 00000000 08:01 123456 /usr/bin/cat # 555555555000-555555556000 r-xp 00001000 08:01 123456 /usr/bin/cat # 555555556000-555555557000 r--p 00002000 08:01 123456 /usr/bin/cat # 555555557000-555555558000 rw-p 00003000 08:01 123456 /usr/bin/cat # ... # 7ffd12345000-7ffd12367000 rw-p 00000000 00:00 0 [stack] # Read the ELF section-to-memory mapping readelf -l /bin/ls | grep -E "LOAD|GNU_RELRO"
Key regions from the ELF perspective:
| Region | Permission | Source | Purpose |
|---|---|---|---|
| .text | r-x (read-execute) | PT_LOAD (code segment) | Machine code |
| .rodata | r-- (read-only) | PT_LOAD (code segment) | String literals, constants |
| .data | rw- (read-write) | PT_LOAD (data segment) | Initialized globals |
| .bss | rw- (read-write) | PT_LOAD (data segment, memsz larger than filesz) | Zero-initialized globals |
| heap | rw- (read-write) | mmap by brk() | malloc() allocations |
| stack | rw- (read-write) | mmap by kernel | Local vars, function frames |
The .bss region is special: p_filesz = 0 but p_memsz is nonzero. The kernel allocates memory for it but doesn't read from the file — it's always zero-initialized. This saves file space.
# mem_layout.c -- compile with: gcc -o mem_layout mem_layout.c # global_init = 42 -- stored in .data # global_uninit -- stored in .bss (zero-initialized) # ro = "hello" -- pointer in .data, string in .rodata # # In main(): # local_var -- on the stack # heap_var = malloc() -- on the heap (just above .bss) # data_ptr = &global_init -- .data segment # bss_ptr = &global_uninit -- .bss segment # stack_ptr = &local_var -- stack $ ./mem_layout text: 0x555555555149 -- code (low address) rodata: 0x555555556004 -- read-only data data: 0x555555558010 -- initialized globals bss: 0x555555558014 -- zero-initialized globals heap: 0x5555555592a0 -- malloc (just above .bss) stack: 0x7ffd...f3c -- stack (high address, grows down)
# See your program's memory layout cat /proc/self/maps | head -20 # Find the text segment (look for r-xp) cat /proc/$$/maps | grep r-xp # Find the stack cat /proc/$$/maps | grep stack # Check stack size limit ulimit -s # Default: 8192 KB (8 MB)
Why does the .bss segment save file space compared to .data?
You see a crash at address 0x7ffd4a3b2100. Based on the memory layout, where is this likely?
You know the memory layout. The final concept covers the complete startup sequence — from when ld.so finishes to when your main() function actually runs.