Überlappende Seiten mit mmap (MAP_FIXED)

Lesezeit: 16 Minuten

Aus einigen obskuren Gründen, die für diese Frage nicht relevant sind, muss ich auf MAP_FIXED zurückgreifen, um eine Seite zu erhalten, in deren Nähe sich der Textabschnitt von libc im Speicher befindet.

Vor dem Lesen von mmap(2) (was ich eigentlich hätte tun sollen), erwartete ich eine Fehlermeldung, wenn ich mmap mit MAP_FIXED aufrief und eine Basisadresse einen bereits zugeordneten Bereich überlappte.

Dies ist jedoch nicht der Fall. Hier ist zum Beispiel ein Teil von /proc/maps für bestimmte Prozesse

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

Was nach dem folgenden mmap-Aufruf …

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

… verwandelt sich in:

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

Das bedeutet, dass ich einen Teil des virtuellen Adressraums, der libc gewidmet ist, mit meiner eigenen Seite überschrieben habe. Offensichtlich nicht das, was ich will …

Im MAP_FIXED-Teil des mmap(2)-Handbuchs heißt es eindeutig:

Wenn der durch addr und len angegebene Speicherbereich Seiten irgendeiner existierenden Abbildung(en) überlappt, dann wird der überlappte Teil der existierenden Abbildung(en) sein verworfen.

Das erklärt, was ich sehe, aber ich habe ein paar Fragen:

  1. Gibt es eine Möglichkeit zu erkennen, ob etwas bereits einer bestimmten Adresse zugeordnet wurde? ohne Zugriff auf /proc/maps?
  2. Gibt es eine Möglichkeit, mmap zum Scheitern zu zwingen, wenn überlappende Seiten gefunden werden?

  • (+1) Das kann sein etwas Hilfe: stackoverflow.com/questions/8362747/…

    – NPE

    18. Februar 2013 um 19:49 Uhr


  • Ich denke, die Verwendung von “MAP_FIXED” ist eines dieser Dinge, bei denen “hier, haben Sie diese Waffe, aber seien Sie bitte vorsichtig, dass Sie sich nicht in den Fuß schießen”. Mit anderen Worten: “Es ist Ihre Aufgabe, sicherzustellen, dass Sie es richtig verwenden”. Ich habe mir in der Vergangenheit einen Teil des mmap-Codes angesehen, und soweit ich weiß, tut er das, wonach Sie fragen, was auch immer Sie tatsächlich verlangen – solange es nicht gegen die Sicherheit verstößt [and your application destroying it’s own copy of the C library is not a security breach, since all that will happen is that YOUR application dies…]

    – Mats Petersson

    18. Februar 2013 um 19:56 Uhr

  • @MatsPetersson – fair genug, aber ist es sogar möglich um es richtig zu verwenden? Dh wenn dir das bewusst ist MAP_FIXED überschreibt vorhandene Zuordnungen, und Sie sind dazu bereit überprüfen Gibt es für vorhandene Zuordnungen eine vernünftige Möglichkeit, dies zu tun?

    – BeeOnRope

    30. Dezember 2016 um 21:58 Uhr

  • @BeeOnRope Wenn Sie die richtigen Adressen kennen und wissen, was im System abgebildet ist, ist es nicht unmöglich, es richtig zu verwenden. Nur schwer.

    – Mats Petersson

    31. Dezember 2016 um 8:23 Uhr


  • @MatsPetersson – das Problem ist, woher wissen Sie, was im System abgebildet ist? Besonders Dinge wie die C-Laufzeit werden Dinge an willkürlichen Orten abbilden, die sich Ihrer Kontrolle entziehen.

    – BeeOnRope

    31. Dezember 2016 um 14:10 Uhr

Benutzer-Avatar
Nominelles Tier

  1. Verwenden page = sysconf(SC_PAGE_SIZE) Um die Seitengröße herauszufinden, scannen Sie dann jeden seitengroßen Block, den Sie überprüfen möchten msync(addr, page, 0) (mit (unsigned long)addr % page == 0dh addr an Seiten ausgerichtet). Wenn es zurückkehrt -1 mit errno == ENOMEMdiese Seite ist nicht zugeordnet.

    Bearbeitet: Wie unten kommentiert, mincore(addr,page,&dummy) überlegen ist msync(). (Die Implementierung des Systemaufrufs ist in mm/mincore.c in den Linux-Kernel-Quellen, wobei C-Bibliotheken normalerweise einen Wrapper bereitstellen, der aktualisiert errno. Da der Systemaufruf die Zuordnungsprüfung sofort nach der Sicherstellung durchführt addr seitenausgerichtet ist, ist es im nicht abgebildeten Fall optimal (ENOMEM). Es funktioniert etwas, wenn die Seite bereits zugeordnet ist. Wenn also die Leistung von größter Bedeutung ist, versuchen Sie es zu vermeiden, Seiten zu überprüfen, von denen Sie wissen, dass sie zugeordnet sind.

    Sie müssen dies einzeln für jede Seite tun, da für Bereiche, die größer als eine einzelne Seite sind, ENOMEM bedeutet, dass die Region nicht vollständig kartiert wurde; es könnte immer noch teilweise zugeordnet sein. Die Zuordnung erfolgt immer granular zu seitengroßen Einheiten.

  2. Soweit ich das beurteilen kann, gibt es keine Möglichkeit zu sagen mmap() fehlschlagen, wenn die Region bereits zugeordnet ist oder bereits zugeordnete Seiten enthält. (Gleiches gilt für mremap()Sie können also kein Mapping erstellen und es dann in die gewünschte Region verschieben.)

    Dies bedeutet, dass Sie das Risiko einer Race Condition eingehen. Es wäre am besten, die eigentlichen Systemaufrufe selbst auszuführen, anstatt die C-Bibliotheks-Wrapper, nur für den Fall, dass sie Speicherzuweisungen vornehmen oder Speicherzuordnungen intern ändern:

    #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);
    }
    

Ich vermute jedoch, dass Sie, was auch immer Sie zu tun versuchen, letztendlich analysieren müssen /proc/self/maps ohnehin.

  • Ich empfehle, Standard-I/O zu vermeiden stdio.h insgesamt (da die verschiedenen Operationen Speicher dynamisch zuweisen und somit die Zuordnungen ändern) und stattdessen die niedrigere Ebene verwenden unistd.h Schnittstellen, die die Zuordnungen mit viel geringerer Wahrscheinlichkeit beeinflussen. Hier ist eine Reihe einfacher, grober Funktionen, die Sie verwenden können, um jede kartierte Region und die in dieser Region aktivierten Schutzmaßnahmen herauszufinden (und die anderen Informationen zu verwerfen). In der Praxis verwendet es etwa ein Kilobyte Code und weniger als das im Stack, sodass es selbst auf begrenzten Architekturen (z. B. eingebettete Geräte) sehr nützlich ist.

    #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;
    }
    

    Das read_maps() Funktion liest bis zu n Regionen, Startadressen wie void * in die ptr Array, Längen in die len Array und Berechtigungen in die mode Array, das die Gesamtzahl der Maps zurückgibt (kann größer sein als n) oder Null mit errno gesetzt, wenn ein Fehler auftritt.

    Es ist durchaus möglich, Syscalls für die Low-Level-E/A oben zu verwenden, sodass Sie keine C-Bibliotheksfunktionen verwenden, aber ich denke nicht, dass dies überhaupt notwendig ist. (Die C-Bibliotheken verwenden, soweit ich das beurteilen kann, sehr einfache Wrapper um die eigentlichen Systemaufrufe für diese.)

Ich hoffe, Sie finden das nützlich.

  • Wäre es nicht leistungsmäßig besser, mincore() anstelle von msync() zu verwenden, da es keine direkten IO-Auswirkungen hat?

    – fons

    19. Februar 2013 um 8:38 Uhr

  • @fons: read_maps() wird am Ende des zweiten Quelltextausschnitts implementiert. Ich habe es von Grund auf neu geschrieben (war gelangweilt), gegen die Kernel-Dokumentation. Dies sind stabile Schnittstellen, wenn also procfs unter gemountet ist /proc, es sollte funktionieren. Bezüglich mincore(), unbedingt – aber auch hier nur, wenn Sie jede einzelne Seite überprüfen. (Ich habe gerade die neuesten Kernelquellen für die mincore() Systemaufruf ein mm/mincore.cund es ist so ziemlich optimal: es tut die access_ok() unmittelbar nach Überprüfung der Seitenausrichtung.) Also ja; mincore() wäre die bessere Wahl.

    – Nominelles Tier

    19. Februar 2013 um 17:33 Uhr

  • Es gibt einen einfachen Weg, es zu sagen mmap fehlschlagen, wenn die Adresse bereits vergeben ist: auslassen MAP_FIXED. In diesem Fall wird es “scheinbar” gelingen, eine andere Adresse als die von Ihnen angeforderte auszuwählen. Wenn die anders gewählte Adresse akzeptabel ist, verwenden Sie sie einfach. Wenn nicht, rufen Sie an munmap und versuchen Sie es erneut unter einer anderen Adresse.

    – R.. GitHub HÖR AUF, EIS ZU HELFEN

    13. September 2014 um 20:31 Uhr

  • Wie dup2 Funktion für Dateideskriptoren, MAP_FIXED sollte niemals verwendet werden, es sei denn, Sie wissen, dass das Ziel bereits zugewiesen ist, vor/während Ihres Anrufs nicht freigegeben wird und Sie es atomar durch etwas Neues ersetzen möchten.

    – R.. GitHub HÖR AUF, EIS ZU HELFEN

    13. September 2014 um 20:32 Uhr

  • @KelvinHu: Ah, jetzt verstehe ich. Sie sehen, auch in Ihrem Fall mincore() Ausgang ist richtig; Es ist nur so, dass die C-Bibliothek zusätzliche Schutzseiten zuweist, die nicht freigegeben werden, wenn Sie die ursprüngliche Zuordnung aufheben. Sie können dies überprüfen, indem Sie ausführen strace ./yourbinaryMit Blick auf die mmap() Anrufe. Sie werden feststellen, dass die /proc/self/maps Die Ausgabe enthält diese Schutzseiten, und ihre Ausgabe stimmt mit was überein mincore() Berichte. Mit anderen Worten: Nein, mincore() Ausgang ist zuverlässig; Es ist nur so, dass Sie möglicherweise unerwartete “Schutzseiten” für Sie zugeordnet haben.

    – Nominelles Tier

    23. November 2017 um 15:21 Uhr

Benutzer-Avatar
Eliot Miranda

„Das erklärt, was ich sehe, aber ich habe ein paar Fragen:“

“Gibt es eine Möglichkeit zu erkennen, ob etwas bereits einer bestimmten Adresse zugeordnet wurde? ohne auf /proc/maps zuzugreifen?”

Ja, verwenden Sie mmap ohne MAP_FIXED.

“Gibt es eine Möglichkeit, mmap zum Fehlschlagen zu zwingen, wenn überlappende Seiten gefunden werden?”

Anscheinend nicht, aber verwenden Sie einfach munmap nach mmap, wenn mmap eine Zuordnung an einer anderen als der angeforderten Adresse zurückgibt.

Wenn benutzt ohne MAP_FIXED, mmap sowohl unter Linux als auch unter Mac OS X (und ich vermute auch anderswo) gehorcht dem Adressparameter, wenn keine vorhandene Zuordnung im Bereich vorhanden ist [address, address + length) exists. So if mmap answers a mapping at a different address to the one you supply you can infer there already exists a mapping in that range and you need to use a different range. Since mmap will typically answer a mapping at a very high address when it ignores the address parameter, simply unmap the region using munmap, and try again at a different address.

Using mincore to check for use of an address range is not only a waste of time (one has to probe a page at a time), it may not work. Older linux kernels will only fail mincore appropriately for file mappings. They won’t answer anything at all for MAP_ANON mappings. But as I’ve pointed out, all you need is mmap and munmap.

I’ve just been through this exercise in implementing a memory manager for a Smalltalk VM. I use sbrk(0) to find out the first address at which I can map the first segment, and then use mmap and an increment of 1Mb to search for room for subsequent segments:

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;
}

This appears to work well, allocating memory at ascending addresses while skipping over any existing mappings.

HTH

user avatar
fons

It seems that posix_mem_offset() is what I was looking for.

Not only it tells you if an address is mapped but also, in case it happens to be mapped, it implicitly gives you the boundaries of the mapped area to which it belongs (by providing SIZE_MAX in the len argument).

So, before enforcing MAP_FIXED, I can use posix_mem_offset() to verify that the address I am using is not mapped yet.

I could use msync() or mincore() too (checking for an ENOMEM error tells you that an address is already mapped), but then I would be blinder (no information about the area where the address is mapped). Also, msync() has side effects which may have a performance impact and mincore() is BSD-only (not POSIX).

  • Current Linux kernels do not provide such a syscall, and at least libc6-2.15-0ubuntu10.3 does not provide a posix_mem_offset() function, so posix_mem_offset() may not be as portable as you think.

    – Nominal Animal

    Feb 19, 2013 at 17:26

  • @NominalAnimal, True. I wrote this before getting to test the function, I will use mincore then.

    – fons

    Feb 20, 2013 at 8:35

1158960cookie-checkÜberlappende Seiten mit mmap (MAP_FIXED)

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy