为什么我不能在 64 位内核上 mmap(MAP_FIXED) 32 位 Linux 进程中的最高虚拟页面?
Why can't I mmap(MAP_FIXED) the highest virtual page in a 32-bit Linux process on a 64-bit kernel?
在 Linux 上尝试在 user-space 中测试 时,我编写了一个 32 位测试程序,试图映射 32 位的低页和高页虚拟地址 space.
echo 0 | sudo tee /proc/sys/vm/mmap_min_addr
之后可以映射到零页,但是不知道为什么不能映射-4096
,即(void*)0xfffff000
,最高页。 为什么mmap2((void*)-4096)
return-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
, and why is it designed that way? Is this part of making sure that creating a pointer to one-past-an-object doesn't 中拒绝它的检查是什么,因为 ISO C 和 C++ 允许创建一个指向尾后一位的指针,但除此之外不在对象之外。
我 运行 在 64 位内核下(Arch Linux 上的 4.12.8-2-ARCH),所以 32 位用户 space 拥有整个 4GiB可用的。 (不同于 64 位内核上的 64 位代码,或 32 位内核中 2:2 或 3:1 user/kernel 拆分将使高页成为内核地址。)
我还没有尝试使用最小的静态可执行文件(没有 CRT 启动或 libc,只有 asm),因为我认为这不会有什么不同。 None 个 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]
是否有未出现在 maps
中的 VMA 区域仍然说服内核拒绝该地址?我查看了 linux/mm/mmapc.
中出现的 ENOMEM
,但要阅读的代码很多,所以我可能漏掉了什么。保留一些高地址范围的东西,或者因为它在堆栈旁边?
以其他顺序进行系统调用没有帮助(但是 PAGE_ALIGN 和类似的宏被仔细编写以避免在屏蔽之前环绕,所以无论如何这不太可能。)
完整源代码,使用 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;
}
(我省略了试图取消引用 (int*)-2
的部分,因为它只是在 mmap 失败时出现段错误。)
mmap 函数最终调用 do_mmap or do_brk_flags which do the actual work of satisfying the memory allocation request. These functions in turn call get_unmapped_area. It is in that function that the checks are made to ensure that memory cannot be allocated beyond the user address space limit, which is defined by 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 位虚拟地址 spaces 的处理器上,__VIRTUAL_MASK_SHIFT
为 47。
注意TASK_SIZE
是根据当前进程是32位上32位,64位上32位,64位上64位来指定的。对于 32 位进程,保留两个页面;一个用于 vsyscall page,另一个用作保护页。本质上,vsyscall 页面无法取消映射,因此用户地址 space 的最高地址实际上是 0xFFFFe000。对于 64 位进程,保留一个保护页。这些页面仅在 64 位 Intel 和 AMD 处理器上保留,因为仅在这些处理器上使用 SYSCALL
机制。
这是在 get_unmapped_area
中执行的检查:
if (addr > TASK_SIZE - len)
return -ENOMEM;
在 Linux 上尝试在 user-space 中测试
echo 0 | sudo tee /proc/sys/vm/mmap_min_addr
之后可以映射到零页,但是不知道为什么不能映射-4096
,即(void*)0xfffff000
,最高页。 为什么mmap2((void*)-4096)
return-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
, and why is it designed that way? Is this part of making sure that creating a pointer to one-past-an-object doesn't
我 运行 在 64 位内核下(Arch Linux 上的 4.12.8-2-ARCH),所以 32 位用户 space 拥有整个 4GiB可用的。 (不同于 64 位内核上的 64 位代码,或 32 位内核中 2:2 或 3:1 user/kernel 拆分将使高页成为内核地址。)
我还没有尝试使用最小的静态可执行文件(没有 CRT 启动或 libc,只有 asm),因为我认为这不会有什么不同。 None 个 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]
是否有未出现在 maps
中的 VMA 区域仍然说服内核拒绝该地址?我查看了 linux/mm/mmapc.
中出现的 ENOMEM
,但要阅读的代码很多,所以我可能漏掉了什么。保留一些高地址范围的东西,或者因为它在堆栈旁边?
以其他顺序进行系统调用没有帮助(但是 PAGE_ALIGN 和类似的宏被仔细编写以避免在屏蔽之前环绕,所以无论如何这不太可能。)
完整源代码,使用 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;
}
(我省略了试图取消引用 (int*)-2
的部分,因为它只是在 mmap 失败时出现段错误。)
mmap 函数最终调用 do_mmap or do_brk_flags which do the actual work of satisfying the memory allocation request. These functions in turn call get_unmapped_area. It is in that function that the checks are made to ensure that memory cannot be allocated beyond the user address space limit, which is defined by 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 位虚拟地址 spaces 的处理器上,__VIRTUAL_MASK_SHIFT
为 47。
注意TASK_SIZE
是根据当前进程是32位上32位,64位上32位,64位上64位来指定的。对于 32 位进程,保留两个页面;一个用于 vsyscall page,另一个用作保护页。本质上,vsyscall 页面无法取消映射,因此用户地址 space 的最高地址实际上是 0xFFFFe000。对于 64 位进程,保留一个保护页。这些页面仅在 64 位 Intel 和 AMD 处理器上保留,因为仅在这些处理器上使用 SYSCALL
机制。
这是在 get_unmapped_area
中执行的检查:
if (addr > TASK_SIZE - len)
return -ENOMEM;