mremap() โ€” Remapping Memory Regions Linux Memory Mappings

 

mremap() โ€” Remapping Memory Regions
Chapter 49 | Linux Memory Mappings | EmbeddedPathashala
๐Ÿ“Œ Linux-Specific
๐Ÿ” Resize & Relocate Maps
โšก Zero-Copy Realloc

What is mremap()?

On most UNIX systems, once you create a memory mapping using mmap(), you cannot change its location or size. Linux provides a non-portable system call mremap() that breaks this limitation. It lets you expand, shrink, or even relocate an existing mapping within the process virtual address space.

A key use case is as a backend for realloc() โ€” instead of copying bytes to a new location, mremap() can just extend the mapping in-place (or move it), making reallocation very efficient.

Key Concepts

mremap() MREMAP_MAYMOVE MREMAP_FIXED old_address old_size new_size MAP_FAILED Virtual Address Space Page Alignment

Function Signature

To use mremap(), define _GNU_SOURCE before including the header:

#define _GNU_SOURCE
#include <sys/mman.h>

void *mremap(void *old_address, size_t old_size,
             size_t new_size, int flags, ...);

/* Returns: new starting address on success, MAP_FAILED on error */

Parameters:

  • old_address โ€” Page-aligned start address of existing mapping (from mmap)
  • old_size โ€” Current size of the mapping
  • new_size โ€” Desired new size
  • flags โ€” MREMAP_MAYMOVE and/or MREMAP_FIXED (or 0)

Both old_size and new_size are rounded up to the next page boundary automatically by the kernel.

Flags Explained

Flag Meaning Extra Arg? Failure Behavior
0 Expand/shrink in place only No ENOMEM if no space
MREMAP_MAYMOVE Allow kernel to relocate mapping No Falls back to ENOMEM only if no VA space
MREMAP_FIXED Move to specific address Yes: void *new_address Only with MREMAP_MAYMOVE

MREMAP_MAYMOVE โ€” Without this flag, if there is not enough contiguous virtual address space to expand the mapping at its current location, mremap() returns MAP_FAILED with errno set to ENOMEM. With this flag, the kernel can move the mapping to a new VA location.

MREMAP_FIXED โ€” Must be used together with MREMAP_MAYMOVE. Takes an extra argument void *new_address. The mapping is moved to exactly that address. If any existing mapping occupies that range, it is unmapped first.

How mremap() Works โ€” Virtual Address Space View

Before and after expanding a mapping (with MREMAP_MAYMOVE):

BEFORE mremap()
Other mappings
Mapping (4KB)
… free space …

โ†’

AFTER (in-place expand)
Other mappings
Mapping (8KB โ€” expanded)
… free space …

AFTER (moved)
Other mappings
[old loc โ€” freed]
Mapping (8KB โ€” new loc)

When a mapping is moved, the old virtual address range is freed. Any pointers into the old mapping become invalid. Always use offsets, not absolute pointers, inside a mapping used with mremap().

โš ๏ธ Critical: Pointer Invalidation After mremap()

If MREMAP_MAYMOVE is set and the kernel moves the mapping, the old address is no longer valid. Any saved pointers into the old region will cause undefined behavior (segfault).

Rule: Always use offsets from the returned base address, not stored absolute pointers.

/* BAD: storing absolute pointer */
int *ptr = (int *)mmap(...);
int *save = ptr + 10;       /* absolute pointer */
ptr = mremap(ptr, ...);     /* mapping may move! */
*save = 42;                 /* UNDEFINED BEHAVIOR */

/* GOOD: use offset */
size_t offset = 10 * sizeof(int);
char *base = (char *)mmap(...);
base = mremap(base, ...);   /* safe to update base */
*((int *)(base + offset)) = 42;  /* correct */

Example 1: Expand a Mapping In-Place
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

int main(void)
{
    size_t old_size = getpagesize();    /* 4096 bytes typically */
    size_t new_size = 2 * getpagesize(); /* expand to 8192 bytes */

    /* Step 1: Create initial anonymous mapping */
    void *addr = mmap(NULL, old_size,
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS,
                      -1, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    /* Write something in the mapping */
    strcpy((char *)addr, "Hello from original mapping!");
    printf("Before mremap: addr=%p, data=%s\n", addr, (char *)addr);

    /* Step 2: Try to expand WITHOUT allowing move */
    void *new_addr = mremap(addr, old_size, new_size, 0);
    if (new_addr == MAP_FAILED) {
        perror("mremap (no move) failed, trying with MAYMOVE");

        /* Step 3: Retry with MREMAP_MAYMOVE */
        new_addr = mremap(addr, old_size, new_size, MREMAP_MAYMOVE);
        if (new_addr == MAP_FAILED) {
            perror("mremap with MAYMOVE");
            munmap(addr, old_size);
            return 1;
        }
    }

    printf("After mremap: new_addr=%p\n", new_addr);
    printf("Data still accessible: %s\n", (char *)new_addr);

    /* Write into the extended area */
    strcpy((char *)new_addr + old_size, "Extra space after expansion");
    printf("Extended area: %s\n", (char *)new_addr + old_size);

    munmap(new_addr, new_size);
    return 0;
}
Example 2: MREMAP_FIXED โ€” Move to a Specific Address
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    size_t pgsz = getpagesize();

    /* Create first mapping */
    void *old_map = mmap(NULL, pgsz,
                         PROT_READ | PROT_WRITE,
                         MAP_PRIVATE | MAP_ANONYMOUS,
                         -1, 0);
    if (old_map == MAP_FAILED) { perror("mmap old"); return 1; }

    strcpy((char *)old_map, "Data in old mapping");

    /* Reserve a destination region (anonymous, large enough) */
    void *dest = mmap(NULL, 2 * pgsz,
                      PROT_NONE,
                      MAP_PRIVATE | MAP_ANONYMOUS,
                      -1, 0);
    if (dest == MAP_FAILED) { perror("mmap dest"); return 1; }

    /* Move old_map to dest using MREMAP_FIXED */
    void *result = mremap(old_map, pgsz, pgsz,
                          MREMAP_MAYMOVE | MREMAP_FIXED,
                          dest);   /* extra arg: target address */
    if (result == MAP_FAILED) {
        perror("mremap fixed");
        return 1;
    }

    printf("Mapping moved to: %p\n", result);
    printf("Data: %s\n", (char *)result);

    munmap(result, pgsz);
    return 0;
}
Example 3: mremap() as a Zero-Copy realloc() Backend

This is how glibc’s malloc()/realloc() can use mremap() internally for large allocations:

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>

/* Simple mmap-based allocator using mremap for resize */

void *my_alloc(size_t size)
{
    /* Round up to page size */
    size_t pgsz = getpagesize();
    size = (size + pgsz - 1) & ~(pgsz - 1);

    void *ptr = mmap(NULL, size,
                     PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS,
                     -1, 0);
    return (ptr == MAP_FAILED) ? NULL : ptr;
}

void *my_realloc(void *ptr, size_t old_size, size_t new_size)
{
    size_t pgsz = getpagesize();
    old_size = (old_size + pgsz - 1) & ~(pgsz - 1);
    new_size = (new_size + pgsz - 1) & ~(pgsz - 1);

    /* mremap: no copy, just remap pages */
    void *new_ptr = mremap(ptr, old_size, new_size, MREMAP_MAYMOVE);
    return (new_ptr == MAP_FAILED) ? NULL : new_ptr;
}

void my_free(void *ptr, size_t size)
{
    size_t pgsz = getpagesize();
    size = (size + pgsz - 1) & ~(pgsz - 1);
    munmap(ptr, size);
}

int main(void)
{
    int *arr = my_alloc(100 * sizeof(int));
    if (!arr) { perror("alloc"); return 1; }

    for (int i = 0; i < 100; i++)
        arr[i] = i * i;

    printf("arr[50] before realloc = %d\n", arr[50]);

    /* Resize to 200 ints โ€” zero copy! */
    arr = my_realloc(arr, 100 * sizeof(int), 200 * sizeof(int));
    if (!arr) { perror("realloc"); return 1; }

    printf("arr[50] after realloc  = %d (preserved)\n", arr[50]);
    for (int i = 100; i < 200; i++)
        arr[i] = i * 10;
    printf("arr[150] = %d\n", arr[150]);

    my_free(arr, 200 * sizeof(int));
    return 0;
}

Example 4: Shrinking a Mapping

mremap() also works to shrink. Pages removed from the mapping are automatically freed:

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    size_t pgsz = getpagesize();
    size_t large = 8 * pgsz;
    size_t small = 2 * pgsz;

    char *map = mmap(NULL, large,
                     PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS,
                     -1, 0);
    if (map == MAP_FAILED) { perror("mmap"); return 1; }

    /* Fill first page */
    memset(map, 'A', pgsz);
    printf("Before shrink: map[0]=%c, size=%zu\n", map[0], large);

    /* Shrink: only flags=0 needed, kernel doesn't need to move */
    char *shrunk = mremap(map, large, small, 0);
    if (shrunk == MAP_FAILED) { perror("mremap shrink"); return 1; }

    printf("After shrink: shrunk[0]=%c, size=%zu\n", shrunk[0], small);
    /* Accessing shrunk[4*pgsz] would be SIGSEGV now โ€” mapping is smaller */

    munmap(shrunk, small);
    return 0;
}

Interview Questions & Answers

Q1. What is mremap() and how is it different from mmap()?

A: mmap() creates a new memory mapping. mremap() changes the size or location of an existing mapping. It is Linux-specific and not available on most other UNIX systems. mmap() always creates a new range; mremap() modifies an existing one.

Q2. What happens if MREMAP_MAYMOVE is not specified and there is no contiguous free space?

A: mremap() fails with ENOMEM. Without MREMAP_MAYMOVE, the kernel can only expand the mapping at its current virtual address. If adjacent virtual pages are already used, expansion fails.

Q3. Why do pointers into a mapping become invalid after mremap()?

A: If MREMAP_MAYMOVE is set and the kernel relocates the mapping, the old virtual address range is unmapped. Any pointer holding the old address now points to an unmapped region, causing SIGSEGV when dereferenced. Always use offsets from the returned base.

Q4. Can MREMAP_FIXED be used without MREMAP_MAYMOVE?

A: No. MREMAP_FIXED must always be combined with MREMAP_MAYMOVE. Using MREMAP_FIXED alone results in an error (EINVAL).

Q5. How does mremap() benefit glibc’s realloc()?

A: For large allocations backed by mmap, realloc() can call mremap() instead of allocating new space and copying data. mremap() just updates the page table entries โ€” no byte-by-byte copy occurs. This is O(1) regardless of mapping size.

Q6. Are old_size and new_size required to be page-aligned?

A: They do not need to be page-aligned by the caller. The kernel automatically rounds them up to the next multiple of the system page size.

Q7. What does MREMAP_FIXED do to any existing mapping at the target address?

A: Any existing mapping in the range [new_address, new_address + new_size) is automatically unmapped before the remapped region is placed there. This is similar to how MAP_FIXED works in mmap().

Q8. How would you implement a growable buffer using mremap()?

A: Create an initial anonymous mmap of a base size. When the buffer is full, call mremap() with MREMAP_MAYMOVE and double the size. Update the base pointer to the returned address. Store data as offsets to handle potential relocation.

Chapter 49 โ€” Memory Mappings Series

Next: MAP_NORESERVE & Swap Overcommit โ†’ OOM Killer MAP_FIXED

Leave a Reply

Your email address will not be published. Required fields are marked *