Loading learning content...
Every program that executes on a computer, from the simplest "Hello, World" to the most complex operating system kernel, organizes its memory into three fundamental categories: code, data, and stack. These aren't arbitrary divisions—they reflect the deep structure of computation itself.
Code holds the instructions—the immutable recipe that defines what the program does. Data holds the ingredients—the values the program manipulates. Stack holds the context—the call history, local variables, and return addresses that track where the program is in its execution.
Understanding these three segment types is essential for anyone who wants to truly understand how programs run. Each has distinct characteristics that dictate how the operating system must manage it, what protections apply, and how it interacts with the hardware.
By the end of this page, you will understand: the complete characteristics of code (text) segments, the varieties and purposes of data segments (initialized, uninitialized, read-only), stack segment mechanics including frame structure and growth, how these segments interact during program execution, protection requirements for each segment type, and how modern systems implement these segment concepts.
The code segment (historically called the text segment from early Unix terminology) contains the executable machine instructions that constitute the program. When a program runs, the CPU fetches instructions from this segment, decodes them, and executes them.
Fundamental Characteristics:
Why 'Text'?
The term "text segment" comes from early Unix and assembly language conventions. In assembly, the .text directive indicates that the following content is program code. The term persists in executable file formats, system tools, and programmer vocabulary, even though "code segment" is more descriptive.
Structure of the Code Segment:
A typical code segment contains:
_start or main)The linker determines the layout. Optimizing linkers may reorder functions to improve cache locality—frequently called functions placed near each other minimize instruction cache misses.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Example: How C code becomes part of the text segment // All of these functions become machine code in the text segment: // Simple function: ~10-20 bytes of machine codeint add(int a, int b) { return a + b;} // Function with loop: ~30-50 bytes of machine codeint factorial(int n) { int result = 1; for (int i = 2; i <= n; i++) { result *= i; } return result;} // Function with conditionals: ~40-80 bytes depending on optimizationint classify(int x) { if (x < 0) return -1; if (x == 0) return 0; return 1;} // The entry pointint main() { int sum = add(5, 3); int fact = factorial(5); int class = classify(-10); return 0;} // Compiled code segment layout (conceptual):// // 0x0000: _start: ; Runtime startup code// ...setup...// call main// // 0x0100: add: ; Function add// mov eax, edi// add eax, esi// ret// // 0x0110: factorial: ; Function factorial// ...loop code...// ret// // 0x0160: classify: ; Function classify// ...conditional code...// ret// // 0x01A0: main: ; Entry point// ...call sequence...// retModern operating systems enforce W^X (Write XOR Execute) policy: a memory region can be writable OR executable, but never both simultaneously. This prevents attackers from injecting malicious code into writable memory and executing it. The code segment is Execute+Read only; any attempt to write triggers a protection fault. This is a fundamental defense against code injection attacks.
One of the most significant benefits of having a distinct code segment is shareability. Since code is read-only and identical across all instances of a program, there's no need to keep multiple copies in physical memory.
The Sharing Mechanism:
When multiple processes run the same executable, the operating system recognizes that their code segments are identical. Instead of loading separate copies for each process:
Memory Savings from Sharing:
Consider a system with 100 users running bash shells, and bash's code segment is 1MB:
Now add shared libraries. If libc is 2MB and used by every program:
On a typical server with hundreds of processes, code sharing saves gigabytes of RAM.
Position-Independent Code (PIC):
For sharing to work when libraries load at different virtual addresses in different processes, code must be position-independent—it must work correctly regardless of where it's loaded.
PIC achieves this by:
This small overhead (extra indirection) pays for itself many times over in memory savings.
Position-independent code has slightly higher runtime overhead due to indirect addressing. However, this is offset by: massive memory savings enabling more processes to run, better instruction cache utilization (shared code stays cached), and reduced page faults (shared pages are more likely to be resident). For all but the most performance-critical inner loops, PIC is the right choice.
While code defines what a program does, data segments hold the state the program manipulates. Unlike the code segment, data segments are typically writable—the whole point of data is that it changes as the program runs.
Data segments are more complex than code segments because there are multiple types of data with different characteristics:
The Data Segment Taxonomy:
| Segment | Contents | Initialization | Permissions | In Binary File? |
|---|---|---|---|---|
| .data | Initialized global/static variables | Values from binary | Read + Write | Yes (stores values) |
| .rodata | Read-only data (constants, strings) | Values from binary | Read only | Yes (stores values) |
| .bss | Uninitialized global/static variables | Zero-filled at load | Read + Write | No (just size) |
The .data Segment (Initialized Data):
This segment holds global and static variables that have explicit initial values:
int global_counter = 100; // Goes in .data
static char buffer[10] = "Hello"; // Goes in .data
float pi = 3.14159f; // Goes in .data
Characteristics:
The .rodata Segment (Read-Only Data):
This segment holds data that should never change:
const int max_connections = 1024; // Goes in .rodata
const char* message = "Error!"; // String in .rodata
static const float coefficients[] = {1.0, 2.0, 3.0}; // In .rodata
Characteristics:
The .bss Segment (Uninitialized Data):
The name ".bss" comes from old assembly language: "Block Started by Symbol". This segment holds uninitialized global/static variables:
int uninitialized_array[1000]; // Goes in .bss
static void* pointers[500]; // Goes in .bss
Characteristics:
Consider: int buffer[1000000]; This is 4MB of zeros. In .data, the executable would include 4MB of zeros. In .bss, the executable just stores '4MB of bss needed.' At load time, the OS allocates 4MB and zero-fills it (or uses zero-page mapping). This is why the size of an executable can be much smaller than its memory footprint.
Understanding how data segments behave throughout program execution is crucial for systems programmers and anyone diagnosing memory-related issues.
Lifecycle Stages:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// Demonstrating data segment behavior #include <stdio.h>#include <string.h> // .rodata - constant string literalconst char* PROGRAM_NAME = "DataDemo"; // .data - initialized globalint request_count = 0; // .bss - uninitialized global (will be zero)char receive_buffer[4096]; // .data - initialized static (file scope)static int error_count = 0; // .bss - uninitialized staticstatic FILE* log_file; void process_request(const char* data) { // request_count is in .data, modified at runtime request_count++; // receive_buffer is in .bss, zero-initialized // First use sees zeros strncpy(receive_buffer, data, sizeof(receive_buffer) - 1); // PROGRAM_NAME is in .rodata // This would cause a protection fault if attempted: // PROGRAM_NAME[0] = 'X'; // CRASH! .rodata is read-only printf("[%s] Processed request #%d", PROGRAM_NAME, request_count);} int main() { // At this point: // - request_count is 0 (loaded from .data) // - receive_buffer is all zeros (zeroed .bss) // - PROGRAM_NAME points to "DataDemo" (in .rodata) process_request("First request"); // request_count becomes 1 process_request("Second request"); // request_count becomes 2 // The value 2 exists only in memory // When program exits, this state is lost return 0;}Access Patterns and Optimization:
Data segments exhibit characteristic access patterns that influence system performance:
Global Variables: Often accessed from many functions, leading to good cache utilization if the variable is small and frequently used. However, global state complicates concurrency—multiple threads accessing the same global require synchronization.
Static Variables: Limited scope means fewer functions access them, potentially better cache behavior in their "home" function. Static locals are particularly interesting—they're allocated in .data or .bss but have local scope.
Constants (.rodata): Highly shareable, often excellent cache behavior since they don't change. The compiler may inline small constants directly into code.
Large Arrays: Whether in .data or .bss, large arrays can cause cache pressure. Careful layout and access patterns (sequential vs. random) dramatically affect performance.
Copy-on-Write Optimization:
Modern systems often apply copy-on-write (COW) to .data segments when forking:
On Linux, use 'size ./program' to see text, data, and bss sizes. Use 'readelf -S ./program' for detailed segment information. Use 'objdump -h ./program' for section headers. Watch a variable with 'nm ./program | grep variable_name' to see which section contains it.
The stack segment is fundamentally different from code and data segments. While those have fixed sizes determined at compile/load time, the stack is dynamic—it grows and shrinks as functions are called and return. The stack is the mechanism that makes function calls, recursion, and local variables possible.
What Lives on the Stack:
Stack Growth Direction:
On most modern architectures (x86, x86-64, ARM), the stack grows downward—toward lower addresses. The stack starts at a high address and each push decreases the stack pointer.
High Address ┌─────────────────────────┐
│ Command-line args │
│ Environment variables │
├─────────────────────────┤
│ main()'s stack frame │
├─────────────────────────┤
│ func_a()'s frame │
├─────────────────────────┤
│ func_b()'s frame │ ← Current stack pointer
├─────────────────────────┤
│ │
│ (available stack) │
│ ↓ │ Stack grows down
│ │
├─────────────────────────┤
│ Guard page (no access)│
Low Address └─────────────────────────┘
This design places the stack and heap on opposite ends of the address space, with each growing toward the other. A guard page between them (or at the stack limit) generates a fault if the stack grows too far, preventing silent corruption.
The stack has a maximum size (often 8MB on Linux by default). Deep recursion or large local arrays can exhaust stack space, causing a stack overflow. Unlike heap exhaustion (which malloc signals by returning NULL), stack overflow typically causes an immediate crash—the stack pointer enters the guard page, triggering a segmentation fault.
Each function call creates a stack frame (also called an activation record)—a structured chunk of stack space containing everything that function needs. Understanding stack frames is essential for debugging, security analysis, and systems programming.
Canonical Stack Frame Layout (x86-64 System V ABI):
Higher addresses
┌─────────────────────────────┐
│ Arguments passed via stack │ (if any beyond registers)
│ (arg7, arg8, ...) │
├─────────────────────────────┤
│ Return address │ ← Pushed by CALL instruction
├─────────────────────────────┤ ← Old RSP before CALL
│ Saved RBP (optional) │ ← Frame pointer
├─────────────────────────────┤ ← New RBP (if used)
│ Local variables │
│ - variable1 │
│ - variable2 │
│ - ... │
├─────────────────────────────┤
│ Saved callee-save registers│
│ (RBX, R12-R15 if used) │
├─────────────────────────────┤
│ Spilled temporaries │
├─────────────────────────────┤
│ Outgoing arguments (stack) │ (for callee, if any)
├─────────────────────────────┤ ← RSP (stack pointer)
Lower addresses
12345678910111213141516171819202122232425262728293031323334353637383940
// Example: What happens on the stack during execution int multiply(int x, int y) { // x, y passed in registers (x86-64) int result = x * y; // 'result' is on the stack return result; // Return value in RAX register} int compute(int a, int b, int c) { int product; // Local variable on stack int sum; // Local variable on stack product = multiply(a, b); // Call creates new stack frame sum = product + c; return sum;} int main() { int answer; // Local variable in main's frame answer = compute(3, 4, 5); // answer = (3*4) + 5 = 17 return answer;} // Stack during multiply() call (conceptual)://// ┌──────────────────────────────────┐// │ main's stack frame │// │ answer (uninitialized at first)│// │ return address to CRT │// ├──────────────────────────────────┤// │ compute's stack frame │// │ product │// │ sum │// │ saved RBP │// │ return address to main │// ├──────────────────────────────────┤// │ multiply's stack frame │ ← CURRENT// │ result │// │ saved RBP │// │ return address to compute │// └──────────────────────────────────┘ ← Stack pointer (RSP)Frame Manipulation Operations:
Function Prologue (at function entry):
push rbp ; Save caller's frame pointer
mov rbp, rsp ; Set up new frame pointer
sub rsp, N ; Allocate space for locals
; Save callee-save registers if needed
Function Epilogue (at function exit):
; Restore callee-save registers if saved
mov rsp, rbp ; Deallocate locals
pop rbp ; Restore caller's frame pointer
ret ; Pop return address and jump
Stack Unwinding for Debugging:
When a debugger (or exception handler) needs to traverse the call stack, it follows the chain of saved frame pointers:
This enables stack traces, debugging, and structured exception handling.
Modern compilers often compile with -fomit-frame-pointer for optimization, freeing RBP as a general-purpose register. This works because compilers can track stack offsets statically, and DWARF debug information provides unwinding data. However, it makes manual debugging harder—you can't simply follow the RBP chain.
The stack's role in storing return addresses makes it a prime target for attackers. Classic buffer overflow attacks exploit the stack to redirect program execution. Understanding these attacks and their mitigations is essential for systems programmers.
The Classic Stack Buffer Overflow:
12345678910111213141516171819202122232425
// VULNERABLE CODE - DO NOT USEvoid vulnerable(char* user_input) { char buffer[64]; // Fixed-size buffer on stack strcpy(buffer, user_input); // DANGEROUS: no bounds checking! printf("You said: %s", buffer);} // Stack layout:// ┌──────────────────────┐// │ Return address │ ← Target for overflow// ├──────────────────────┤// │ Saved RBP │// ├──────────────────────┤// │ buffer[63] │// │ ... │ ← Overflow writes upward// │ buffer[0] │ ← Attacker's input starts here// └──────────────────────┘ // If user_input is longer than 64 bytes, strcpy overwrites:// 1. The rest of the buffer// 2. The saved RBP// 3. THE RETURN ADDRESS// // Attacker controls return address → Controls execution!Modern Stack Protection Mechanisms:
123456789101112131415161718192021222324252627282930
// How stack canaries work (conceptual) void protected_function(char* input) { // Compiler inserts canary after prologue unsigned long canary = __stack_chk_guard; // Random value char buffer[64]; strcpy(buffer, input); // Still vulnerable, but... // Compiler inserts check before epilogue if (canary != __stack_chk_guard) { // Canary was corrupted! Stack overflow detected __stack_chk_fail(); // Terminates program } // Only returns if canary is intact} // Stack with canary:// ┌──────────────────────┐// │ Return address │// ├──────────────────────┤// │ Saved RBP │// ├──────────────────────┤// │ 🐤 Stack Canary 🐤 │ ← Must be intact to return// ├──────────────────────┤// │ buffer[63] │// │ ... │ ← Overflow corrupts canary first// │ buffer[0] │// └──────────────────────┘Modern systems combine all these protections. An attacker must bypass stack canaries (random, checked before return), NX (can't execute injected code), ASLR (addresses randomized), and potentially shadow stacks (separate return address storage). Each layer makes exploitation harder; together, they make classic stack attacks impractical.
While we've focused on code, data, and stack as the canonical segments, a complete picture requires understanding the heap segment—the region for dynamic memory allocation. The heap isn't always modeled as a traditional segment, but it's essential to program memory.
Heap vs. Stack: Complementary Roles:
Heap Characteristics:
Dynamic Growth — The heap grows upward (toward higher addresses) as allocations are made. When more space is needed, the program requests additional pages from the OS.
Non-Contiguous Internally — Unlike the stack's strictly ordered growth, heap allocations can be scattered. Free blocks exist between allocated blocks, leading to fragmentation.
Manual Management — In languages like C, programmers must explicitly free heap allocations. Memory leaks occur when allocations are never freed; use-after-free bugs when freed memory is accessed.
Allocator Complexity — The heap allocator (malloc/free implementation) is a sophisticated piece of software managing free lists, coalescing freed blocks, and balancing speed against fragmentation.
Heap in the Memory Map:
┌─────────────────────────────────┐ High addresses
│ Stack (grows down) │
│ ↓ │
├─────────────────────────────────┤
│ │
│ (unmapped region) │
│ │
├─────────────────────────────────┤
│ ↑ │
│ Heap (grows up) │ ← Grows via brk/sbrk or mmap
├─────────────────────────────────┤
│ BSS │
├─────────────────────────────────┤
│ Data │
├─────────────────────────────────┤
│ Text (Code) │
└─────────────────────────────────┘ Low addresses
Modern allocators like jemalloc, tcmalloc, and mimalloc don't rely solely on expanding a single heap segment via brk(). They use mmap() to obtain memory from anywhere in the address space, manage separate arenas for different threads, and employ sophisticated strategies to minimize fragmentation and lock contention. The 'heap' is now a logical concept more than a single segment.
This page has provided a comprehensive exploration of the three fundamental segment types and the heap. Let's consolidate the key insights:
What's Next:
We've seen that segments have variable sizes—the code segment might be 1KB while the data segment is 100KB. The next page explores variable segment sizes in depth: why this flexibility is powerful, how it differs from fixed-size paging, and what challenges it creates for memory management.
You now understand the three fundamental segment types—code, data, and stack—along with the heap. You know what lives in each, how they're protected, how they interact, and why their distinct characteristics matter for systems design and security. This foundation prepares you for understanding variable segment sizes and the programmer's view of segmented memory.