5

在兼容 AMD64 的架构上,地址在被取消引用之前需要采用规范形式。

来自英特尔手册,第 3.3.7.1 节

在 64 位模式下,如果地址位 63 到微架构实现的最高有效位被设置为全 1 或全零,则地址被认为是规范形式的。

现在,当前操作系统和体系结构上最重要的实现位是第 47 位。这给我们留下了一个 48 位的地址空间。

特别是当启用ASLR时,用户程序可以期望接收到第 47 位设置的地址。

如果使用诸如指针标记之类的优化并且使用高位来存储信息,则程序必须确保在取消引用地址之前将第 48 位到第 63 位设置回第 47 位。

但是考虑一下这段代码:

int main()
{
    int* intArray = new int[100];

    int* it = intArray;

    // Fill the array with any value.
    for (int i = 0; i < 100; i++)
    {
        *it = 20;
        it++;   
    }

    delete [] intArray;
    return 0;
}

现在考虑一下intArray,说:

0000 0000 0000 0000 0 111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1100

设置itintArray并增加it一次后,考虑sizeof(int) == 4,将变为:

0000 0000 0000 0000 1 000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

第 47 位以粗体显示。这里发生的是指针算术检索到的第二个指针无效,因为不是规范形式。正确的地址应该是:

1111 1111 1111 1111 1 000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

程序如何处理这个问题?操作系统是否保证永远不会为您分配地址范围不随第 47 位变化的内存?

4

1 回答 1

8

规范地址规则意味着 64 位虚拟地址空间中存在一个巨大的漏洞。2^47-1与其上方的下一个有效地址连续,因此单个地址不会包含任何不可用的 64 位地址范围。mmap

+----------+
| 2^64-1   |   0xffffffffffffffff
| ...      |
| 2^64-2^47|   0xffff800000000000
+----------+
|          |
| unusable |      not to scale: this part is 2^16 times as large
|          |
+----------+
| 2^47-1   |   0x00007fffffffffff
| ...      |
| 0        |   0x0000000000000000
+----------+

此外,大多数内核保留规范范围的高半部分供自己使用。例如x86-64 Linux 的内存映射。无论如何,用户空间只能在连续的低范围内分配,因此间隙的存在是无关紧要的。

操作系统是否保证永远不会为您分配地址范围不随第 47 位变化的内存?

不完全是。当前硬件支持的 48 位地址空间是一个实现细节。规范地址规则确保未来的系统可以支持更多的虚拟地址位,而不会在很大程度上破坏向后兼容性。

最多,您只需要一个兼容标志就可以让操作系统不为进程提供任何具有不同高位的内存区域。(就像 Linux 的MAP_32BITmmap 的当前标志,或进程范围的设置)。这可以支持将高位用于标记并手动重新进行符号扩展的程序。

未来的硬件将不需要支持任何类型的标志来忽略高地址位,因为高位中的垃圾当前是一个错误。 Intel 5 级分页增加了另外 9 个虚拟地址位,扩大了规范的高和低半部分。 白皮书

另请参阅为什么在 64 位中虚拟地址比物理地址(52 位长)短 4 位(48 位长)?


有趣的事实:Linux 默认将堆栈映射到有效地址较低范围的顶部。(相关: 为什么 Linux 偏爱 0x7f 映射?

$ gdb /bin/ls
...
(gdb) b _start
Function "_start" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (_start) pending.
(gdb) r
Starting program: /bin/ls

Breakpoint 1, 0x00007ffff7dd9cd0 in _start () from /lib64/ld-linux-x86-64.so.2
(gdb) p $rsp
$1 = (void *) 0x7fffffffd850
(gdb) exit

$ calc
2^47-1
              0x7fffffffffff

(现代 GDB 可以starti在第一条用户空间指令执行之前中断,而不是乱用断点命令。)

于 2016-08-16T19:31:58.853 回答