64 位的 ASLR 和内存布局:是否仅限于规范部分 (128 TiB)?

ASLR and memory layout on 64 bits: Is it limited to the canonical part (128 TiB)?

在启用 ASLR 的情况下加载 PIE 可执行文件时,Linux 会限制程序段到规范部分(最多 0000_7fff_ffff_ffff)的映射,还是会使用完整的下部部分(开始位 0)?

显然 Linux 不会为您的进程提供不可用的地址,这会使它在尝试从 _start 执行代码时引发 #GP(0) 异常(并因此引发段错误)。 (或者如果接近截止点,当它尝试加载或存储 .data 或 .bss 时)

这实际上会发生在试图首先将 RIP 设置为非规范值的指令上,可能是 iretsysret1.


在具有 48 位虚拟地址的系统上,零到 0000_7fff_ffff_ffff 虚拟地址 space 的完整下半部分,当表示为符号时扩展的 64 位值。

在支持 PML5(并由内核使用)的系统上,虚拟地址为 57 位宽,因此
零到 00ff_ffff_ffff_ffff 是规范范围的下半部分。

参见 https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt - 第一行是用户 space 范围。 (它谈论“56 位”虚拟地址。这是不正确的或误导性的,PML5 是 57 位的,额外的全级别页表,每级别 9 位。所以低半部分是 56 位,第 57 位为 0,高半部分是 56 位,第 57 位是 1。)

========================================================================================================================
    Start addr    |   Offset   |     End addr     |  Size   | VM area description
========================================================================================================================
                  |            |                  |         |
 0000000000000000 |    0       | 00007fffffffffff |  128 TB | user-space virtual memory, different per mm
__________________|____________|__________________|_________|___________________________________________________________
                  |            |                  |         |
 0000800000000000 | +128    TB | ffff7fffffffffff | ~16M TB | ... huge, almost 64 bits wide hole of non-canonical
                  |            |                  |         |     virtual memory addresses up to the -128 TB
                  |            |                  |         |     starting offset of kernel mappings.
__________________|____________|__________________|_________|___________________________________________________________
                                                            |
                                                            | Kernel-space virtual memory, shared between all processes:
...

或对于 PML5:

 0000000000000000 |    0       | 00ffffffffffffff |   64 PB | user-space virtual memory, different per mm
__________________|____________|__________________|_________|___________________________________________________________
                  |            |                  |         |
 0000800000000000 |  +64    PB | ffff7fffffffffff | ~16K PB | ... huge, still almost 64 bits wide hole of non-canonical
                  |            |                  |         |     virtual memory addresses up to the -64 PB
                  |            |                  |         |     starting offset of kernel mappings.

脚注 1:
正如 prl 指出的那样,这种设计允许一个实现实际上只有 48 个实际位来存储 RIP 值在管道中的任何地方,除了跳转和检测有符号溢出以防执行超出末端进入非规范区域。 (也许在每个必须存储 uop 的地方保存晶体管,它需要知道它自己的地址。)不像你可以跳转 / iret 到任意 RIP,然后 #GP(0) 异常必须推送正确的64 位非规范地址,这意味着 CPU 必须暂时记住它。

对于调试查看跳转位置也更有用,因此以这种方式设计规则是有意义的,因为没有故意跳转到非规范地址的用例。 (与跳转到未映射的页面不同,#PF 异常处理程序可以修复这种情况,例如通过请求分页,因此您希望故障地址是新的 RIP。)

有趣的事实:在 Intel CPUs 上使用带有非规范 RIP 的 sysret 将在 ring 0 (CPL=0) 中使用#GP(0),因此 RSP 不会切换并且仍然 = 用户堆栈。如果存在任何其他线程,这会让它们弄乱内核用作堆栈的内存。这是 IA-32e 中的设计缺陷,英特尔 x86-64 的实现。这就是为什么 Linux 使用 iret 到 return 到用户 space 从系统调用入口点,如果 ptrace 在此过程中在此期间被使用。内核知道一个新的进程将有一个安全的 RIP,因此它实际上可能使用 sysret 更快地跳转到用户 space。