Class 31 of 37 • Advanced Linux System Programming

Linux Memory Management in C - Virtual Memory, Layout, mmap & Valgrind

Memory management is the cornerstone of operating system engineering and high-performance C programming in Linux. Every Linux process executes within an isolated, private sandbox called the Virtual Address Space. The Linux kernel, together with the CPU's Memory Management Unit (MMU), translates virtual memory addresses into physical RAM addresses using page tables.

In this master class, we explore the standard ELF process memory layout, compare the low-level dynamic memory allocators (brk vs mmap), understand Copy-on-Write (COW) during fork() process creation, and learn how to track down memory leaks using Valgrind and AddressSanitizer.

Linux Process Virtual Address Space Memory Layout Diagram

Figure 1: Architectural Virtual Address Space Layout of a Linux Process


1. Anatomical Breakdown of Process Memory Segments

When an ELF binary is loaded into RAM via the execve() system call, the kernel constructs five fundamental segments in user space:

Memory Segment Permissions Growth Direction Description & Contents
Text Segment (.text) Read & Execute (R-X) Fixed Size Compiled machine code CPU instructions; write-protected to prevent accidental modification or code injection attacks.
Initialized Data (.data) Read & Write (RW-) Fixed Size Global and static variables explicitly initialized with non-zero values (e.g. int max_users = 100;).
Uninitialized Data (.bss) Read & Write (RW-) Fixed Size Block Started by Symbol: global and static variables uninitialized or set to zero. The kernel zeros this entire block at execution time.
Heap Segment Read & Write (RW-) Grows Upward (↑) Dynamically allocated memory managed by malloc(), calloc(), realloc(), or the low-level brk()/sbrk() system calls.
Memory Mapping (mmap) Read / Write / Exec Varies Memory-mapped files, dynamically linked shared C libraries (like libc.so), and large dynamic allocations (typically > 128KB).
Stack Segment Read & Write (RW-) Grows Downward (↓) Stores active function stack frames, local auto variables, function parameters, return addresses, and environment arrays (argc, argv).

2. Hands-on C Program: Inspecting Memory Addresses

We can observe the relative layout of these segments by printing the pointer addresses of variables in different scopes:

/* mem_layout.c - Inspecting Linux Memory Segments in C */
#include <stdio.h>
#include <stdlib.h>

int global_init_var = 42;      /* Initialized Data Segment (.data) */
int global_uninit_var;         /* Uninitialized BSS Segment (.bss) */

void sample_function(void) {
    int local_stack_var = 10;  /* User Stack Segment */
    printf("Local Stack Variable Address   : %p\n", (void *)&local_stack_var);
}

int main(void) {
    char *heap_ptr = (char *)malloc(1024); /* Heap Segment */

    printf("=== Linux Process Memory Segment Addresses ===\n");
    printf("Text Segment (Code / main)     : %p\n", (void *)&main);
    printf("Initialized Data Segment       : %p\n", (void *)&global_init_var);
    printf("Uninitialized BSS Segment      : %p\n", (void *)&global_uninit_var);
    printf("Heap Segment (malloc pointer)  : %p\n", (void *)heap_ptr);
    sample_function();

    free(heap_ptr);
    return 0;
}

Compiling and running the program illustrates how addresses span from bottom (Text) to top (Stack):

$ gcc -Wall mem_layout.c -o mem_layout
$ ./mem_layout
=== Linux Process Memory Segment Addresses ===
Text Segment (Code / main)     : 0x55d1a8e1a189
Initialized Data Segment       : 0x55d1a8e1d014
Uninitialized BSS Segment      : 0x55d1a8e1d01c
Heap Segment (malloc pointer)  : 0x55d1aa4272a0
Local Stack Variable Address   : 0x7ffeec81b514
Address Space Layout Randomization (ASLR): Notice the randomized 64-bit addresses! Linux enables ASLR by default (see /proc/sys/kernel/randomize_va_space) to prevent buffer-overflow exploits by randomizing stack, heap, and library base positions on each execution.

3. High-Performance File I/O with mmap()

The mmap() system call maps a file or hardware device directly into the process's virtual memory space. Instead of executing costly read() and write() system calls (which require copying data between kernel buffers and user space), reading from a memory-mapped pointer reads directly from the Linux OS page cache!

/* mmap_demo.c - Ultra-fast file reading using mmap */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <filename>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) { perror("open failed"); exit(EXIT_FAILURE); }

    struct stat sb;
    if (fstat(fd, &sb) == -1) { perror("fstat failed"); exit(EXIT_FAILURE); }

    /* Map file into memory: PROT_READ (read-only), MAP_PRIVATE (copy on write) */
    char *file_data = (char *)mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (file_data == MAP_FAILED) { perror("mmap failed"); exit(EXIT_FAILURE); }

    /* The file is now a simple in-memory C array! */
    printf("--- File Content (Mapped %ld bytes) ---\n", (long)sb.st_size);
    write(STDOUT_FILENO, file_data, sb.st_size);

    /* Unmap memory and close file */
    munmap(file_data, sb.st_size);
    close(fd);
    return 0;
}

4. Detecting Memory Leaks with Valgrind

In C programming, failing to release heap memory using free() leads to memory leaks that degrade performance and crash production servers. Valgrind Memcheck is the industry standard for diagnosing heap corruption and leak defects:

/* leak_demo.c - Deliberate memory leak for debugging */
#include <stdlib.h>

void cause_leak(void) {
    int *array = (int *)malloc(100 * sizeof(int));
    array[0] = 42;
    /* Bug: Missing free(array); memory is leaked! */
}

int main(void) {
    cause_leak();
    return 0;
}

Compile with the -g flag to embed line numbers, then run through Valgrind:

$ gcc -g leak_demo.c -o leak_demo
$ valgrind --leak-check=full ./leak_demo
==38122== HEAP SUMMARY:
==38122==     in use at exit: 400 bytes in 1 blocks
==38122==   total heap usage: 1 allocs, 0 frees, 400 bytes allocated
==38122== 
==38122== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==38122==    at 0x4848899: malloc (vg_replace_malloc.c:380)
==38122==    by 0x10915B: cause_leak (leak_demo.c:5)
==38122==    by 0x109181: main (leak_demo.c:10)

5. Key Memory Management Concepts Explained

  • Demand Paging: When malloc() requests 100MB, the kernel doesn't immediately assign 100MB of RAM. It only reserves virtual page table entries. Physical RAM pages are allocated lazily upon the very first read/write operation via minor page faults.
  • Copy-on-Write (COW): When calling fork(), the child process shares the exact physical memory pages of the parent. The kernel only duplicates a page if one process modifies it.
  • Out of Memory (OOM) Killer: If system RAM and swap are exhausted, the Linux kernel invokes the OOM Killer heuristic to terminate high-memory rogue processes and prevent an OS crash.

Next Step in Your Linux Learning Path:

Now that you understand virtual memory and process spaces, learn how to build background system services in Class 32: Linux Daemons & Systemd Services.

Continue to Class 32: Daemons & Services → Previous Class: ← Socket Programming


6. University Viva & Job Interview Q&A

Q1: What is a segmentation fault (SIGSEGV) in Linux?

Answer: A Segmentation Fault occurs when a program attempts to access a virtual memory address that is either unmapped, violates memory access permissions (e.g., trying to write to the read-only Text segment), or dereferences a null pointer (0x0).

Q2: What is the difference between a minor and a major page fault?

Answer: A minor page fault occurs when the requested page is already in physical memory (e.g. shared library or COW page) and only requires a page table update. A major page fault requires the kernel to perform disk I/O (such as reading from swap space or loading an executable binary block).

Q3: How does mmap() differ from read() and write() for large files?

Answer: read() and write() require context switches and double buffering (copying data from kernel page cache to user space buffers). mmap() directly maps file pages to the user process's page table, avoiding memory copying and enabling random array-like access.


7. Frequently Asked Questions (FAQ)

Does free() immediately return memory back to the Linux OS?

Not always. The C runtime library (glibc ptmalloc) retains small freed memory blocks inside internal free lists to speed up subsequent malloc() requests. Large blocks allocated via mmap(), however, are released immediately back to the OS via munmap().

How can I view a live process's memory map in Linux?

You can inspect the virtual memory layout of any running Linux process by reading /proc/<PID>/maps or running the pmap <PID> command in the terminal.

Share Share on Facebook Share on Twitter Share on LinkedIn Pin on Pinterest Share on Stumbleupon Share on Tumblr Share on Reddit Share on Diggit

You may also like this!