用mmap重叠页面(MAP_FIXED)

由于与这个问题无关的一些模糊的原因,我需要求助于使用MAP_FIXED来获得一个页面,这个页面接近libc的文本部分在内存中的位置。

在阅读mmap(2)(本来应该做的)之前,如果我使用MAP_FIXED调用mmap,并且基地址与已经映射的区域重叠,那么我期望得到一个错误。

但事实并非如此。 例如,这里是某个进程的/ 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的虚拟地址空间的一部分。 显然不是我想要的…

在mmap(2)手册的MAP_FIXED部分中,它明确指出:

如果由addr和len指定的内存区域与任何现有映射的页面重叠,则现有映射的重叠部分将被丢弃

这解释了我所看到的,但我有几个问题:

  1. 有没有办法来检测是否已经映射到某个地址? 没有访问/ proc / maps?
  2. 有没有办法强迫mmap在发现重叠页面的情况下失败?

  1. 使用page = sysconf(SC_PAGE_SIZE)来查找页面大小,然后使用msync(addr, page, 0) (使用(unsigned long)addr % page == 0来扫描每个页面大小的块,即addr align到页面)。 如果errno == ENOMEM返回-1 ,则该页面不会被映射。

    编辑:作为下面评论, mincore(addr,page,&dummy)优于msync() 。 (syscall的实现是在Linux内核源代码中的mm/mincore.c中,C库通常提供了一个更新errno的包装器,因为系统调用确保addr是页面对mm/mincore.c即进行映射检查,未映射的大小写( ENOMEM )。如果页面已经被映射,它会做一些工作,所以如果性能是最重要的,尽量避免检查你知道被映射的页面。

    您必须单独对每个页面执行此操作,因为对于大于单个页面的区域, ENOMEM表示该区域未完全映射; 它可能仍然是部分映射的。 映射始终是页面大小的单位。

  2. 据我所知,如果区域已被映射,或者包含已经映射的页面,则无法告诉mmap()失败。 (这同样适用于mremap() ,所以你不能创建一个映射,然后将其移动到所需的区域。)

    这意味着你有一个竞争条件的风险。 最好是自己执行实际的系统调用,而不是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

  • 我建议完全避免标准I / O stdio.h (因为各种操作会动态地分配内存,从而改变映射),而是使用较低级别的unistd.h接口,这些接口不太可能影响映射。 这里有一组简单粗糙的函数,您可以使用它来找出每个映射区域以及在该区域中启用的保护(并放弃其他信息)。 实际上,它使用大约1千字节的代码,并且比堆栈少,所以即使在有限的体系结构(比如嵌入式设备)上也是非常有用的。

     #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设置错误号。

    上面的低级I / O使用系统调用是很有可能的,所以你不要使用任何C库特性,但是我不认为这是必要的。 (就我所知,C库在实际的系统调用中使用了非常简单的包装。)

希望这个对你有帮助。

“这解释了我所看到的,但我有几个问题:”

“有没有办法检测是否已经映射到某个地址,而不访问/ proc / maps?

是的,使用没有MAP_FIXED的mmap。

“在找到重叠页面的情况下,是否有办法强制mmap失败?

显然不是,但只要在mmap之后使用munmap,如果mmap在请求的地址之外返回一个映射。

没有使用MAP_FIXED时,在Linux和Mac OS X(以及我也怀疑在其他地方)的mmap都存在地址参数,如果在地址[地址,地址+长度)范围内没有现有的映射。 因此,如果mmap应答与您提供的不同地址的映射,则可以推断在该范围内已经存在映射,并且您需要使用不同的范围。 由于mmap通常会在忽略地址参数时在非常高的地址上回答映射,只需使用munmap取消映射该区域,然后在不同的地址再次尝试。

使用mincore来检查地址范围的使用不仅浪费时间(一次只能探测一个页面),它可能无法工作。 较早的Linux内核只会针对文件映射进行适当的mincore失败。 对于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; } 

这似乎工作正常,在跳过任何现有的映射跳过地址分配内存。

HTH

看来, posix_mem_offset()是我正在寻找。

它不仅告诉你一个地址是否被映射,而且如果它恰好被映射,它会隐式地给你它所属映射区域的边界(通过在len参数中提供SIZE_MAX)。

所以,在执行MAP_FIXED之前,我可以使用posix_mem_offset()来验证我所使用的地址还没有被映射。

我也可以使用msync()mincore() (检查一个ENOMEM错误,告诉你一个地址已经映射),但是我会盲目的(没有关于地址映射区域的信息)。 此外, msync()有副作用,可能会有性能影响, mincore()是仅BSD(不是POSIX)。