What are Memory Mappings?
Memory mapping is a technique where the kernel maps a region of virtual address space to either a file on disk or anonymous (unnamed) memory. Instead of using read() and write() system calls, your program can access file contents directly through pointers as if they were in RAM. This is the core mechanism behind shared libraries, inter-process communication, and high-performance file I/O in Linux.
The mmap() system call is defined in <sys/mman.h>. Understanding its flags and protection arguments is essential for embedded and systems programming interviews at companies like Texas Instruments, Qualcomm, and STMicroelectronics.
Key Terms in This Module
The full prototype of mmap() is:
#include <sys/mman.h>
void *mmap(void *addr, /* Hint for where to place mapping (NULL = let kernel decide) */
size_t length, /* Size of the mapping in bytes */
int prot, /* Memory protection flags */
int flags, /* Mapping type: MAP_PRIVATE or MAP_SHARED + optional flags */
int fd, /* File descriptor (or -1 for anonymous mapping) */
off_t offset); /* Offset into the file (must be page-aligned) */
/* Returns: starting address of mapping on success, MAP_FAILED on error */
Return value: On success, mmap() returns a pointer to the start of the mapped region. On failure it returns MAP_FAILED (which is (void *) -1), and sets errno. Always check for MAP_FAILED, not NULL.
/* Correct error check */
char *addr = mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) { /* NOT: if (addr == NULL) */
perror("mmap");
exit(EXIT_FAILURE);
}
The flags argument controls the most important behaviour of a mapping. Exactly one of MAP_PRIVATE or MAP_SHARED must always be specified.
| Property | MAP_PRIVATE | MAP_SHARED |
|---|---|---|
| Writes visible to other processes? | โ No | โ Yes |
| Writes carried to underlying file? | โ No | โ Yes (not guaranteed immediate) |
| Copy-on-write used? | โ Yes (on write) | โ No |
| Typical use case | Loading executables, text segments, read-only config | IPC between processes, shared memory, file updates |
| Example | MAP_PRIVATE | MAP_ANONYMOUS for heap |
MAP_SHARED for shared file IPC |
How MAP_PRIVATE Copy-on-Write (CoW) works:
Code example โ MAP_PRIVATE (process-local changes):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int fd;
struct stat sb;
char *addr;
/* Open a file for reading */
fd = open("testfile.txt", O_RDONLY);
if (fd == -1) { perror("open"); exit(EXIT_FAILURE); }
if (fstat(fd, &sb) == -1) { perror("fstat"); exit(EXIT_FAILURE); }
/*
* MAP_PRIVATE: any writes we make are NOT reflected in the file.
* PROT_READ | PROT_WRITE: we can read and modify our private copy.
*/
addr = mmap(NULL, sb.st_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE,
fd, 0);
if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }
close(fd); /* Safe to close fd after mmap() succeeds */
/* This modifies our private copy only - file on disk is unchanged */
addr[0] = 'X';
printf("First char in our mapping: %c\n", addr[0]);
printf("File on disk is NOT changed.\n");
munmap(addr, sb.st_size);
return 0;
}
Code example โ MAP_SHARED (changes reflected in file):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int fd;
char *addr;
const size_t SIZE = 64;
/*
* For MAP_SHARED + PROT_WRITE, file must be opened O_RDWR.
* Opening O_RDONLY and using MAP_SHARED + PROT_WRITE will fail with EACCES.
*/
fd = open("shared_file.txt", O_RDWR | O_CREAT, 0644);
if (fd == -1) { perror("open"); exit(EXIT_FAILURE); }
/* File must be at least SIZE bytes - extend it if needed */
if (ftruncate(fd, SIZE) == -1) { perror("ftruncate"); exit(EXIT_FAILURE); }
addr = mmap(NULL, SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED, /* Changes ARE written to the file */
fd, 0);
if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }
close(fd);
/* This WILL be reflected in the file on disk */
snprintf(addr, SIZE, "Hello from mmap MAP_SHARED!\n");
printf("Written to file via mmap.\n");
/* msync() ensures changes are flushed to disk before munmap */
if (msync(addr, SIZE, MS_SYNC) == -1)
perror("msync");
munmap(addr, SIZE);
return 0;
}
The prot argument controls what operations a process can perform on the mapped memory. It can be PROT_NONE, or a bitwise OR of one or more flags:
| Flag | Meaning | Violation Signal |
|---|---|---|
PROT_NONE |
No access at all (not readable, writable, or executable) | SIGSEGV on any access |
PROT_READ |
Region can be read | SIGSEGV on write or execute |
PROT_WRITE |
Region can be written | SIGSEGV on execute |
PROT_EXEC |
Region contains executable code | SIGSEGV if write-only |
How protection violations are handled:
Code example โ demonstrating memory protection:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/mman.h>
#include <unistd.h>
void sigsegv_handler(int sig)
{
printf("Caught SIGSEGV (%d): memory protection violation!\n", sig);
/* In real code: log, cleanup, then exit */
exit(EXIT_FAILURE);
}
int main(void)
{
char *addr;
size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
/* Install SIGSEGV handler to demonstrate protection */
signal(SIGSEGV, sigsegv_handler);
/*
* Create anonymous private mapping with READ-ONLY protection.
* MAP_ANONYMOUS: not backed by a file.
* -1, 0 are fd and offset, ignored for anonymous mappings.
*/
addr = mmap(NULL, page_size,
PROT_READ, /* Read only! */
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }
printf("Page size: %zu bytes\n", page_size);
printf("Mapping at: %p\n", (void *)addr);
printf("Trying to READ (allowed): first byte = %d\n", addr[0]);
/* Attempting to WRITE will trigger SIGSEGV */
printf("Attempting WRITE to PROT_READ region...\n");
addr[0] = 'A'; /* This line triggers SIGSEGV */
printf("This line will never print.\n");
munmap(addr, page_size);
return 0;
}
Code example โ changing protection at runtime with mprotect():
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
int main(void)
{
size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
char *addr;
/* Start with read-only mapping */
addr = mmap(NULL, page_size,
PROT_READ,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
if (addr == MAP_FAILED) { perror("mmap"); exit(EXIT_FAILURE); }
printf("Initial protection: PROT_READ\n");
/* Upgrade protection to read+write using mprotect() */
if (mprotect(addr, page_size, PROT_READ | PROT_WRITE) == -1) {
perror("mprotect");
munmap(addr, page_size);
exit(EXIT_FAILURE);
}
printf("After mprotect: PROT_READ | PROT_WRITE\n");
/* Now write is allowed */
addr[0] = 'Z';
printf("Write succeeded: addr[0] = '%c'\n", addr[0]);
/* Downgrade back to read-only */
if (mprotect(addr, page_size, PROT_READ) == -1) {
perror("mprotect");
}
printf("Protection downgraded back to PROT_READ\n");
munmap(addr, page_size);
return 0;
}
A guard page is a page mapped with PROT_NONE placed intentionally at the start or end of a memory region. If the program accidentally reads or writes into the guard page, the kernel delivers SIGSEGV immediately, making bugs easy to detect. This is used by memory allocators, thread stacks, and custom arena allocators.
| GUARD PAGE PROT_NONE Any access = SIGSEGV |
USABLE MEMORY REGION PROT_READ | PROT_WRITE Normal read/write allowed |
GUARD PAGE PROT_NONE Any access = SIGSEGV |
| Low address | High address |
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
/*
* Allocate a memory region with guard pages on both sides.
* Total allocation = page + usable_size + page
*/
void *alloc_with_guards(size_t usable_size)
{
size_t page = (size_t)sysconf(_SC_PAGESIZE);
size_t total;
char *base;
/* Round up usable_size to page boundary */
usable_size = (usable_size + page - 1) & ~(page - 1);
total = page + usable_size + page;
/* Map the entire region as PROT_NONE first */
base = mmap(NULL, total,
PROT_NONE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
if (base == MAP_FAILED) return NULL;
/* Make the usable middle region read+write */
if (mprotect(base + page, usable_size, PROT_READ | PROT_WRITE) == -1) {
munmap(base, total);
return NULL;
}
/* Return pointer to usable region (after the first guard page) */
return base + page;
}
int main(void)
{
size_t page = (size_t)sysconf(_SC_PAGESIZE);
char *buf;
buf = alloc_with_guards(1024);
if (!buf) { fprintf(stderr, "alloc failed\n"); exit(EXIT_FAILURE); }
printf("Usable buffer at: %p\n", (void *)buf);
/* Safe: within usable region */
buf[0] = 'H';
buf[1023] = 'i';
printf("Write successful: buf[0]=%c, buf[1023]=%c\n", buf[0], buf[1023]);
/*
* Accessing buf[-1] (before) or buf[page] (after) would hit
* a PROT_NONE guard page and trigger SIGSEGV immediately.
*/
/* Cleanup: unmap including guard pages */
munmap(buf - page, page + 1024 + page);
return 0;
}
mmap() arguments have specific alignment requirements that differ between standards (SUSv3 vs SUSv4) and Linux. This is a common interview topic because misalignment causes EINVAL errors that are hard to debug.
| Argument | Requirement | Error if violated |
|---|---|---|
offset |
Must be a multiple of system page size | EINVAL |
addr (with MAP_FIXED) |
Must be page-aligned | EINVAL |
addr (without MAP_FIXED) |
Just a hint; kernel may ignore it | None (kernel ignores misaligned hint) |
addr + offset (MAP_FIXED, nonzero addr) |
Same remainder modulo page size (SUSv4) | Undefined behaviour |
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
int main(void)
{
long page_size = sysconf(_SC_PAGESIZE);
printf("System page size: %ld bytes\n", page_size);
/* CORRECT: offset = 0 (always page-aligned) */
int fd = open("testfile.txt", O_RDONLY);
if (fd != -1) {
void *addr = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr != MAP_FAILED) {
printf("mmap with offset=0: OK\n");
munmap(addr, 4096);
}
close(fd);
}
/* CORRECT: offset = 1 * page_size (multiple of page size) */
/* mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 1 * page_size); */
/* WRONG: offset = 100 (not a multiple of page size) */
/* mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 100);
* This returns MAP_FAILED with errno = EINVAL */
/* How to compute a page-aligned offset from an arbitrary byte offset */
off_t byte_offset = 5000;
off_t aligned_offset = (byte_offset / page_size) * page_size;
size_t extra = (size_t)(byte_offset - aligned_offset); /* bytes before target */
printf("Byte offset: %ld\n", (long)byte_offset);
printf("Aligned offset to pass to mmap: %ld\n", (long)aligned_offset);
printf("Add %zu bytes to returned pointer to reach target\n", extra);
return 0;
}
sysconf(_SC_PAGESIZE) or getpagesize() to get the system page size at runtime. Do NOT hardcode 4096 โ some architectures (ARM64 with 64KB pages, for example) use different page sizes.Q1: What is the difference between MAP_PRIVATE and MAP_SHARED?
MAP_PRIVATE creates a copy-on-write mapping. Writes are local to the process and not visible to other processes or reflected in the underlying file. MAP_SHARED creates a shared mapping. Writes are visible to all processes that have mapped the same region, and for file mappings, changes are written back to the file (though not necessarily immediately).
Q2: What signal is delivered when a process accesses memory in violation of its protection?
The kernel delivers SIGSEGV (Segmentation Fault) to the process. On some non-Linux UNIX implementations, SIGBUS may be delivered instead, but Linux uses SIGSEGV for protection violations.
Q3: What is PROT_NONE used for?
PROT_NONE marks pages as completely inaccessible. Its primary use is creating guard pages at the boundaries of memory regions. If code accidentally overruns a buffer into a guard page, SIGSEGV is triggered immediately, making buffer overflow bugs detectable. Thread stacks typically have a guard page below the stack to detect stack overflow.
Q4: Can different processes map the same memory region with different protections?
Yes. Memory protections are stored in process-private virtual memory tables (page tables). Each process has its own set of page tables. So Process A can map a file with PROT_READ while Process B maps the same file with PROT_READ | PROT_WRITE | MAP_SHARED.
Q5: Why must the offset argument of mmap() be page-aligned?
The hardware MMU (Memory Management Unit) maps memory at page granularity. It cannot map a file starting at an arbitrary byte offset that falls in the middle of a page. The mapping must start at a page boundary. If you need to access data at an arbitrary offset, align down to the page boundary, map from there, and add the difference to the returned pointer.
Q6: On older x86-32, PROT_READ implies PROT_EXEC. Why is this a security concern?
If read implies execute, any readable data region (like stack or heap) is also executable. An attacker who writes shellcode to the stack can execute it directly. This is why the NX (No-eXecute) bit was introduced in hardware. Since Linux kernel 2.6.8, Linux uses the NX bit on x86-32 to properly separate PROT_READ and PROT_EXEC, enabling W^X (Write XOR Execute) memory protection policies used by modern security hardening.
Q7: What does mmap() return on failure and why must you check for MAP_FAILED, not NULL?
On failure, mmap() returns MAP_FAILED, which is defined as (void *) -1. You must check for MAP_FAILED, not NULL, because the kernel is allowed to map memory at address 0 (though this is rarely done in practice). Checking if (addr == NULL) would miss the error condition.
Q8: What does MAP_FIXED do and why is it dangerous?
MAP_FIXED tells the kernel to place the mapping exactly at the address specified in addr, not just use it as a hint. This is dangerous because if the specified address already contains an existing mapping, the kernel silently replaces it, potentially destroying data or code that was already mapped there (like part of a shared library). It should only be used when you know exactly what is at that address.
Continue Learning Memory Mappings
Next: munmap() โ Removing Memory Mappings
