为什么我不能在64位内核上mmap(MAP_FIXED)32位Linux进程中的最高虚拟页面?

时间:2017-12-08 10:34:03

标签: linux assembly linux-kernel x86 mmap

在Linux上尝试在用户空间中测试Is it allowed to access memory that spans the zero boundary in x86?时,我写了一个32位测试程序,试图映射32位虚拟地址空间的低页和高页。

echo 0 | sudo tee /proc/sys/vm/mmap_min_addr之后,我可以映射零页面,但我不知道为什么我无法映射-4096,即(void*)0xfffff000,即最高页面。 为什么mmap2((void*)-4096)会返回-ENOMEM

strace ./a.out 
execve("./a.out", ["./a.out"], 0x7ffe08827c10 /* 65 vars */) = 0
strace: [ Process PID=1407 runs in 32 bit mode. ]
....
mmap2(0xfffff000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0

另外,在linux/mm/mmap.c中拒绝检查的是什么,为什么这样设计呢?这是确保创建指向one-past-an-object的指针不是wrap around and break pointer comparisons的一部分,因为ISO C和C ++允许创建一个指向一个接一个的结尾的指针,但是不能在对象。

我在64位内核(Arch Linux上的4.12.8-2-ARCH)下运行,因此32位用户空间可以提供整个4GiB。 (与64位内核上的64位代码或32位内核不同,其中2:2或3:1用户/内核拆分会使高页成为内核地址。)

我没有尝试过最小的静态可执行文件(没有CRT启动或libc,只是asm),因为我认为这不会有所作为。没有CRT启动系统调用看起来很可疑。

在断点处停止时,我检查了/proc/PID/maps。首页尚未使用。堆栈包含第二高的页面,但首页未映射。

00000000-00001000 rw-p 00000000 00:00 0             ### the mmap(0) result
08048000-08049000 r-xp 00000000 00:15 3120510                 /home/peter/src/SO/a.out
08049000-0804a000 r--p 00000000 00:15 3120510                 /home/peter/src/SO/a.out
0804a000-0804b000 rw-p 00001000 00:15 3120510                 /home/peter/src/SO/a.out
f7d81000-f7f3a000 r-xp 00000000 00:15 1511498                 /usr/lib32/libc-2.25.so
f7f3a000-f7f3c000 r--p 001b8000 00:15 1511498                 /usr/lib32/libc-2.25.so
f7f3c000-f7f3d000 rw-p 001ba000 00:15 1511498                 /usr/lib32/libc-2.25.so
f7f3d000-f7f40000 rw-p 00000000 00:00 0 
f7f7c000-f7f7e000 rw-p 00000000 00:00 0 
f7f7e000-f7f81000 r--p 00000000 00:00 0                       [vvar]
f7f81000-f7f83000 r-xp 00000000 00:00 0                       [vdso]
f7f83000-f7fa6000 r-xp 00000000 00:15 1511499                 /usr/lib32/ld-2.25.so
f7fa6000-f7fa7000 r--p 00022000 00:15 1511499                 /usr/lib32/ld-2.25.so
f7fa7000-f7fa8000 rw-p 00023000 00:15 1511499                 /usr/lib32/ld-2.25.so
fffdd000-ffffe000 rw-p 00000000 00:00 0                       [stack]

是否有{/ 1}}中没有出现的VMA区域仍然说服内核拒绝该地址?我查看mapsENOMEM的出现次数,但要阅读的代码很多,也许我错过了一些东西。什么东西可以保留一些高地址范围,或者因为它位于堆栈旁边?

以其他顺序进行系统调用没有帮助(但是小心写入了PAGE_ALIGN和类似的宏以避免在屏蔽之前环绕,因此无论如何都不可能。)

完整源代码,使用linux/mm/mmapc.编译:

gcc -O3 -fno-pie -no-pie -m32 address-wrap.c

(我省略了试图解析#include <sys/mman.h> //void *mmap(void *addr, size_t len, int prot, int flags, // int fildes, off_t off); int main(void) { volatile unsigned *high = mmap((void*)-4096L, 4096, PROT_READ | PROT_WRITE, MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); volatile unsigned *zeropage = mmap((void*)0, 4096, PROT_READ | PROT_WRITE, MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); return (high == MAP_FAILED) ? 2 : *high; } 的部分,因为它只是在mmap失败时出现段错误。)

1 个答案:

答案 0 :(得分:4)

mmap函数最终调用do_mmapdo_brk_flags来完成满足内存分配请求的实际工作。这些函数依次调用get_unmapped_area。在该函数中,进行检查以确保不能分配超出由TASK_SIZE定义的用户地址空间限制的内存。我引用代码:

 * There are a few constraints that determine this:
 *
 * On Intel CPUs, if a SYSCALL instruction is at the highest canonical
 * address, then that syscall will enter the kernel with a
 * non-canonical return address, and SYSRET will explode dangerously.
 * We avoid this particular problem by preventing anything executable
 * from being mapped at the maximum canonical address.
 *
 * On AMD CPUs in the Ryzen family, there's a nasty bug in which the
 * CPUs malfunction if they execute code from the highest canonical page.
 * They'll speculate right off the end of the canonical space, and
 * bad things happen.  This is worked around in the same way as the
 * Intel problem.

#define TASK_SIZE_MAX   ((1UL << __VIRTUAL_MASK_SHIFT) - PAGE_SIZE)

#define IA32_PAGE_OFFSET    ((current->personality & ADDR_LIMIT_3GB) ? \
                    0xc0000000 : 0xFFFFe000)

#define TASK_SIZE       (test_thread_flag(TIF_ADDR32) ? \
IA32_PAGE_OFFSET : TASK_SIZE_MAX)

在具有48位虚拟地址空间的处理器上,__VIRTUAL_MASK_SHIFT为47。

请注意,TASK_SIZE的指定取决于当前进程是32位32位,64位32位,64位64位。对于32位进程,保留两个页面;一个用于vsyscall page,另一个用作保护页面。实质上,vsyscall页面无法取消映射,因此用户地址空间的最高地址实际上是0xFFFFe000。对于64位进程,保留一个保护页面。这些页面仅保留在64位Intel和AMD处理器上,因为仅在这些处理器上使用SYSCALL机制。

以下是get_unmapped_area中执行的检查:

if (addr > TASK_SIZE - len)
     return -ENOMEM;