Перекрытие страниц с помощью mmap (MAP_FIXED)

Из-за некоторых неясных причин, которые не имеют отношения к этому вопросу, мне нужно прибегнуть к использованию MAP_FIXED, чтобы получить страницу, близкую к тому, где в памяти находится текстовый раздел libc.

Прежде чем читать mmap (2) (что я должен был сделать в первую очередь), я ожидал получить ошибку, если бы я вызвал mmap с MAP_FIXED и базовый адрес, перекрывающий уже отображенную область.

Однако это не так. Например, вот часть /proc/maps для определенного процесса

7ffff7299000-7ffff744c000 r-xp 00000000 08:05 654098                     /lib/x86_64-linux-gnu/libc-2.15.so

Что после выполнения следующего вызова mmap...

  mmap(0x7ffff731b000,
       getpagesize(),
       PROT_READ | PROT_WRITE | PROT_EXEC,
       MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED,
       0,
       0);

... превращается в:

7ffff7299000-7ffff731b000 r-xp 00000000 08:05 654098                     /lib/x86_64-linux-gnu/libc-2.15.so
7ffff731b000-7ffff731c000 rwxp 00000000 00:00 0 
7ffff731c000-7ffff744c000 r-xp 00083000 08:05 654098                     /lib/x86_64-linux-gnu/libc-2.15.so

Это означает, что я переписал часть виртуального адресного пространства, выделенного для libc, с моей собственной страницей. Ясно, что я не хочу...

В части MAP_FIXED руководства mmap (2) четко указано:

Если область памяти, указанная addr и len, перекрывает страницы любого               существующее сопоставление (-ы), то перекрываемая часть существующих сопоставлений будет отброшена.

Это объясняет, что я вижу, но у меня есть несколько вопросов:

  • Есть ли способ обнаружить, что что-то уже сопоставлено с определенным адресом? без доступа /proc/maps?
  • Есть ли способ заставить mmap потерпеть неудачу в случае нахождения перекрывающихся страниц?

Ответ 1

  • Используйте page = sysconf(SC_PAGE_SIZE), чтобы узнать размер страницы, затем сканируйте каждый размер размера страницы, который вы хотите проверить, используя msync(addr, page, 0)(unsigned long)addr % page == 0, т.е. addr, совпадающим со страницами). Если он возвращает -1 с помощью errno == ENOMEM, эта страница не отображается.

    Отредактировано: Как отмечалось ниже, mincore(addr,page,&dummy) превосходит msync(). (Реализация syscall находится в mm/mincore.c в источниках ядра Linux, а библиотеки C обычно предоставляют обертку, которая обновляет errno. Поскольку syscall проверяет соответствие сразу после того, как addr выравнивается по странице, это (ENOMEM). Он выполняет некоторую работу, если страница уже сопоставлена, поэтому, если производительность имеет первостепенное значение, старайтесь избегать проверки страниц, которые, как вы знаете, сопоставлены.

    Вы должны делать это отдельно, отдельно для каждой страницы, потому что для регионов, больших одной страницы, ENOMEM означает, что регион не был полностью отображен; он может быть частично отображен. Отображение всегда зависит от размера страницы.

  • Насколько я могу судить, невозможно сообщить mmap() об ошибке, если регион уже отображен или содержит уже отображенные страницы. (То же самое относится к mremap(), поэтому вы не можете создать сопоставление, а затем переместите его в нужную область.)

    Это означает, что вы рискуете расы. Лучше всего было бы выполнять фактические syscalls самостоятельно, вместо обложек библиотеки C, на всякий случай, если они распределяют память или меняют отображение памяти внутри:

    #define _GNU_SOURCE
    #include <unistd.h>
    #include <sys/syscall.h>
    
    static size_t page = 0;
    static inline size_t page_size(void)
    {
        if (!page)
            page = (size_t)sysconf(_SC_PAGESIZE);
        return page;
    }
    
    
    static inline int raw_msync(void *addr, size_t length, int flags)
    {
        return syscall(SYS_msync, addr, length, flags);
    }
    
    static inline void *raw_mmap(void *addr, size_t length, int prot, int flags)
    {
        return (void *)syscall(SYS_mmap, addr, length, prot, flags, -1, (off_t)0);
    }
    

Тем не менее, я подозреваю, что все, что вы пытаетесь сделать, в конечном итоге вам нужно разобрать /proc/self/maps.

  • Я рекомендую вообще избегать стандартного ввода-вывода stdio.h (поскольку различные операции будут распределять память динамически и, таким образом, изменять сопоставления), а вместо этого использовать интерфейсы нижнего уровня unistd.h, которые намного меньше вероятно, повлияет на сопоставления. Вот набор простых, грубых функций, которые вы можете использовать для поиска каждой отображаемой области и защищенных объектов в этом регионе (и отбрасывания другой информации). На практике он использует около килобайта кода и меньше, чем в стеке, поэтому он очень полезен даже на ограниченных архитектурах (скажем, встраиваемых устройствах).

    #include <unistd.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <string.h>
    
    #ifndef   INPUT_BUFFER
    #define   INPUT_BUFFER   512
    #endif /* INPUT_BUFFER */
    
    #ifndef   INPUT_EOF
    #define   INPUT_EOF     -256
    #endif /* INPUT_EOF */
    
    #define   PERM_PRIVATE  16
    #define   PERM_SHARED    8
    #define   PERM_READ      4
    #define   PERM_WRITE     2
    #define   PERM_EXEC      1
    
    typedef struct {
        int            descriptor;
        int            status;
        unsigned char *next;
        unsigned char *ends;
        unsigned char  buffer[INPUT_BUFFER + 16];
    } input_buffer;
    
    /* Refill input buffer. Returns the number of new bytes.
     * Sets status to ENODATA at EOF.
    */
    static size_t input_refill(input_buffer *const input)
    {
        ssize_t n;
    
        if (input->status)
            return (size_t)0;
    
        if (input->next > input->buffer) {
            if (input->ends > input->next) {
                memmove(input->buffer, input->next,
                        (size_t)(input->ends - input->next));
                input->ends = input->buffer + (size_t)(input->ends - input->next);
                input->next = input->buffer;
            } else {
                input->ends = input->buffer;
                input->next = input->buffer;
            }
        }
    
        do {
            n = read(input->descriptor, input->ends,
                     INPUT_BUFFER - (size_t)(input->ends - input->buffer));
        } while (n == (ssize_t)-1 && errno == EINTR);
        if (n > (ssize_t)0) {
            input->ends += n;
            return (size_t)n;
    
        } else
        if (n == (ssize_t)0) {
            input->status = ENODATA;
            return (size_t)0;
        }
    
        if (n == (ssize_t)-1)
            input->status = errno;
        else
            input->status = EIO;
    
        return (size_t)0;
    }
    
    /* Low-lever getchar() equivalent.
    */
    static inline int input_next(input_buffer *const input)
    {
        if (input->next < input->ends)
            return *(input->next++);
        else
        if (input_refill(input) > 0)
            return *(input->next++);
        else
            return INPUT_EOF;
    }
    
    /* Low-level ungetc() equivalent.
    */
    static inline int input_back(input_buffer *const input, const int c)
    {
        if (c < 0 || c > 255)
            return INPUT_EOF;
        else
        if (input->next > input->buffer)
            return *(--input->next) = c;
        else
        if (input->ends >= input->buffer + sizeof input->buffer)
            return INPUT_EOF;
    
        memmove(input->next + 1, input->next, (size_t)(input->ends - input->next));
        input->ends++;
        return *(input->next) = c;
    }
    
    /* Low-level fopen() equivalent.
    */
    static int input_open(input_buffer *const input, const char *const filename)
    {
        if (!input)
            return errno = EINVAL;
    
        input->descriptor = -1;
        input->status = 0;
        input->next = input->buffer;
        input->ends = input->buffer;
    
        if (!filename || !*filename)
            return errno = input->status = EINVAL;
    
        do {
            input->descriptor = open(filename, O_RDONLY | O_NOCTTY);
        } while (input->descriptor == -1 && errno == EINTR);
        if (input->descriptor == -1)
            return input->status = errno;
    
        return 0;
    }
    
    /* Low-level fclose() equivalent.
    */
    static int input_close(input_buffer *const input)
    {
        int result;
    
        if (!input)
            return errno = EINVAL;
    
        /* EOF is not an error; we use ENODATA for that. */
        if (input->status == ENODATA)
            input->status = 0;
    
        if (input->descriptor != -1) {
            do {
                result = close(input->descriptor);
            } while (result == -1 && errno == EINTR);
            if (result == -1 && !input->status)
                input->status = errno;
        }
    
        input->descriptor = -1;
        input->next = input->buffer;
        input->ends = input->buffer;
    
        return errno = input->status;
    }
    
    /* Read /proc/self/maps, and fill in the arrays corresponding to the fields.
     * The function will return the number of mappings, even if not all are saved.
    */
    size_t read_maps(size_t const n,
                     void **const ptr, size_t *const len,
                     unsigned char *const mode)
    {
        input_buffer    input;
        size_t          i = 0;
        unsigned long   curr_start, curr_end;
        unsigned char   curr_mode;
        int             c;
    
        errno = 0;
    
        if (input_open(&input, "/proc/self/maps"))
            return (size_t)0; /* errno already set. */
    
        c = input_next(&input);
        while (c >= 0) {
    
            /* Skip leading controls and whitespace */
            while (c >= 0 && c <= 32)
                c = input_next(&input);
    
            /* EOF? */
            if (c < 0)
                break;
    
            curr_start = 0UL;
            curr_end = 0UL;
            curr_mode = 0U;
    
            /* Start of address range. */
            while (1)
                if (c >= '0' && c <= '9') {
                    curr_start = (16UL * curr_start) + c - '0';
                    c = input_next(&input);
                } else
                if (c >= 'A' && c <= 'F') {
                    curr_start = (16UL * curr_start) + c - 'A' + 10;
                    c = input_next(&input);
                } else
                if (c >= 'a' && c <= 'f') {
                    curr_start = (16UL * curr_start) + c - 'a' + 10;
                    c = input_next(&input);
                } else
                    break;
            if (c == '-')
                c = input_next(&input);
            else {
                errno = EIO;
                return (size_t)0;
            }
    
            /* End of address range. */
            while (1)
                if (c >= '0' && c <= '9') {
                    curr_end = (16UL * curr_end) + c - '0';
                    c = input_next(&input);
                } else
                if (c >= 'A' && c <= 'F') {
                    curr_end = (16UL * curr_end) + c - 'A' + 10;
                    c = input_next(&input);
                } else
                if (c >= 'a' && c <= 'f') {
                    curr_end = (16UL * curr_end) + c - 'a' + 10;
                    c = input_next(&input);
                } else
                    break;
            if (c == ' ')
                c = input_next(&input);
            else {
                errno = EIO;
                return (size_t)0;
            }
    
            /* Permissions. */
            while (1)
                if (c == 'r') {
                    curr_mode |= PERM_READ;
                    c = input_next(&input);
                } else
                if (c == 'w') {
                    curr_mode |= PERM_WRITE;
                    c = input_next(&input);
                } else
                if (c == 'x') {
                    curr_mode |= PERM_EXEC;
                    c = input_next(&input);
                } else
                if (c == 's') {
                    curr_mode |= PERM_SHARED;
                    c = input_next(&input);
                } else
                if (c == 'p') {
                    curr_mode |= PERM_PRIVATE;
                    c = input_next(&input);
                } else
                if (c == '-') {
                    c = input_next(&input);
                } else
                    break;
            if (c == ' ')
                c = input_next(&input);
            else {
                errno = EIO;
                return (size_t)0;
            }
    
            /* Skip the rest of the line. */
            while (c >= 0 && c != '\n')
                c = input_next(&input);
    
            /* Add to arrays, if possible. */
            if (i < n) {
                if (ptr) ptr[i] = (void *)curr_start;
                if (len) len[i] = (size_t)(curr_end - curr_start);
                if (mode) mode[i] = curr_mode;
            }
            i++;
        }
    
        if (input_close(&input))
            return (size_t)0; /* errno already set. */
    
        errno = 0;
        return i;
    }
    

    Функция read_maps() считывает области n, запускает адреса как void * в массив ptr, длина в массив len и разрешения в массив mode, возвращая общее число (может быть больше, чем n), или ноль с errno, если возникает ошибка.

    Вполне возможно использовать системные вызовы для низкоуровневого ввода-вывода, чтобы вы не использовали никаких функций библиотеки C, но я не думаю, что это вообще необходимо. (Библиотеки C, насколько я могу судить, используют очень простые обертки вокруг фактических системных вызовов для них.)

Надеюсь, вы сочтете это полезным.

Ответ 2

"Это объясняет, что я вижу, но у меня есть несколько вопросов:"

"Есть ли способ обнаружить, что что-то уже было сопоставлено с определенным адресом? без доступа к /proc/maps?"

Да, используйте mmap без MAP_FIXED.

"Есть ли способ заставить mmap потерпеть неудачу в случае нахождения перекрывающихся страниц?"

По-видимому, нет, но просто используйте munmap после mmap, если mmap возвращает сопоставление не за запрошенным адресом.

При использовании без MAP_FIXED mmap как в Linux, так и в Mac OS X (и я подозреваю, что в другом месте также) подчиняется параметру адреса, если нет существующего сопоставления в диапазоне [адрес, адрес + длина). Поэтому, если mmap отвечает на сопоставление с другим адресом на тот, который вы поставляете, вы можете сделать вывод, что в этом диапазоне уже существует сопоставление, и вам нужно использовать другой диапазон. Поскольку mmap обычно отвечает на сопоставление с очень высоким адресом, когда он игнорирует параметр адреса, просто отмените область с помощью munmap и повторите попытку с другим адресом.

Использование mincore для проверки использования диапазона адресов - это не только трата времени (нужно пробовать страницу за раз), она может не работать. Старые ядра linux будут неадекватны только для корректного отображения файлов. Они ничего не ответят за сопоставления MAP_ANON. Но, как я уже сказал, все, что вам нужно, это mmap и munmap.

Я только что прошел через это упражнение в реализации менеджера памяти для Smalltalk VM. Я использую sbrk (0), чтобы узнать первый адрес, на котором я могу отобразить первый сегмент, а затем использовать mmap и приращение 1Mb для поиска места для последующих сегментов:

static long          pageSize = 0;
static unsigned long pageMask = 0;

#define roundDownToPage(v) ((v)&pageMask)
#define roundUpToPage(v) (((v)+pageSize-1)&pageMask)

void *
sqAllocateMemory(usqInt minHeapSize, usqInt desiredHeapSize)
{
    char *hint, *address, *alloc;
    unsigned long alignment, allocBytes;

    if (pageSize) {
        fprintf(stderr, "sqAllocateMemory: already called\n");
        exit(1);
    }
    pageSize = getpagesize();
    pageMask = ~(pageSize - 1);

    hint = sbrk(0); /* the first unmapped address above existing data */

    alignment = max(pageSize,1024*1024);
    address = (char *)(((usqInt)hint + alignment - 1) & ~(alignment - 1));

    alloc = sqAllocateMemorySegmentOfSizeAboveAllocatedSizeInto
                (roundUpToPage(desiredHeapSize), address, &allocBytes);
    if (!alloc) {
        fprintf(stderr, "sqAllocateMemory: initial alloc failed!\n");
        exit(errno);
    }
    return (usqInt)alloc;
}

/* Allocate a region of memory of at least size bytes, at or above minAddress.
 *  If the attempt fails, answer null.  If the attempt succeeds, answer the
 * start of the region and assign its size through allocatedSizePointer.
 */
void *
sqAllocateMemorySegmentOfSizeAboveAllocatedSizeInto(sqInt size, void *minAddress, sqInt *allocatedSizePointer)
{
    char *address, *alloc;
    long bytes, delta;

    address = (char *)roundUpToPage((unsigned long)minAddress);
    bytes = roundUpToPage(size);
    delta = max(pageSize,1024*1024);

    while ((unsigned long)(address + bytes) > (unsigned long)address) {
        alloc = mmap(address, bytes, PROT_READ | PROT_WRITE,
                     MAP_ANON | MAP_PRIVATE, -1, 0);
        if (alloc == MAP_FAILED) {
            perror("sqAllocateMemorySegmentOfSizeAboveAllocatedSizeInto mmap");
            return 0;
        }
        /* is the mapping both at or above address and not too far above address? */
        if (alloc >= address && alloc <= address + delta) {
            *allocatedSizePointer = bytes;
            return alloc;
        }
        /* mmap answered a mapping well away from where Spur prefers.  Discard
         * the mapping and try again delta higher.
         */
        if (munmap(alloc, bytes) != 0)
            perror("sqAllocateMemorySegment... munmap");
        address += delta;
    }
    return 0;
}

Это, как представляется, хорошо работает, выделяя память по возрастающим адресам, пропуская любые существующие сопоставления.

НТН

Ответ 3

Кажется, что posix_mem_offset() - это то, что я искал.

Не только он указывает вам, отображается ли адрес, но также, в случае его отображения, он неявно дает вам границы отображаемой области, к которой он принадлежит (предоставляя SIZE_MAX в аргументе len).

Итак, перед тем, как принудительно выполнить MAP_FIXED, я могу использовать posix_mem_offset(), чтобы убедиться, что адрес, который я использую, еще не отображается.

Я мог бы использовать msync() или mincore() (проверка на ошибку ENOMEM говорит о том, что адрес уже сопоставлен), но тогда я бы стал blinder (нет информации о области, в которой отображается адрес). Кроме того, msync() имеет побочные эффекты, которые могут иметь влияние на производительность, а mincore() - только BSD (не POSIX).