1. Page Zero的作用
Making a big __PAGEZERO
in a 64-bit architecture makes a whole lot of sense. The address range of a 64-bit system, even when the upper 16 bits are "cropped off" like that of x86_64, allows for a huge amount of memory (the 48-bit address space of x86_64 is 256TB of memory address space). It is highly likely that this will be thought of as "small" at some point in the future, but right now, the biggest servers have 1-4TB, so there's plenty of room to grow, and more ordinary machines have 16-32GB.
Note also that no memory is actually OCCUPIED. It's just "reserved virtual space" (that is, "it will never be used"). It takes up absolutely zero resources, because it's not mapped in the page-table, it's not there physically. It's just an entry in the file, which tells the loader to reserve this space to it can never be used, and thus "safeguarded". The actual "data" of this section is zero in size, since, again, there's actually nothing there, just a "make sure this is not used". So your actual file size won't be any larger or smaller if this section is changed in size. It would be a few bytes smaller (the size of the section description) if it didn't exist at all. But that's really the only what it would make any difference at all.
The purpose of a __PAGEZERO
is to catch NULL pointer dereferences. By reserving a large section of memory at the beginning of memory, any access through a NULL pointer will be caught and the application aborted. In a 32-bit architecture, something like:
int *p = NULL;
int x = p[0x100000];
is likely to succeed, because at 0x400000 (4MB) the code-space starts (trying to write to such a location is likely to crash, but reading will work - assuming of course the code-space actually starts there and not someplace else in the address range.
Edit:
This presentation shows that ARM, the latest entrant into the 64-bit processor sapce, is also using 48-bit virtual address space, and enforces canonical addresses (top 16 bits need to all be the same value) so it can be expanded in the future. In other words, the virtual space available on a 64-bit ARM processor is also 256TB.
In addition to catching NULL dereferences, using a size of 0x100000000 in 64-bit means that no 32-bit pointer is valid. This helps catch buggy software that has been compiled for 64-bit but is not 64-bit safe. For example, it copies a pointer to an int and then back to a pointer variable, truncating it.
2. 翻译
让 64 位架构中的 __PAGEZERO
尽可能大有许多意义:
首先,64 系统下,地址够用。虽然很多 64 位架构和 x86_64 架构一样,将 64 中的高 16 位剪掉了,但是仍然有 48 位可以用来表示地址。48 位意味着 256TB 的大小,而如今服务器最大的内存大概是 1-4TB,一般机器的内存时 16-32GB,所以 256TB 在很长一段时间内具备增长的空间,即:够用!
另外,__PAGEZERO
所指代的内存并没有真正被分配(或占用)。首先,在文件在硬盘上时,__PAGEZERO
的 filesize 为 0,即不分配硬盘空间。再者,mach-O 是被加载进入虚拟内存而不是物理内存,物理内存的使用需要通过 MMU 进行印射。因此,__PAGEZERO
占用的虚拟内存在物理内存中并没有被分配。如果程序访问该段内存,肯定会直接被系统拦截,报出 BAD_ACCESS 的错误,即:
- 存储在硬盘上时,不占用硬盘空间;
- 被加载进入虚拟内存时,不会分配真实的物理内存;
- 只是其到一个作用:告诉加载者分配固定内存,告诉使用者这段虚拟内存不能使用;
至此,__PAGEZERO
的使用方式了解了,但是 __PAGEZERO
这段的具体作用是什么?首先是为了判断空指针的异常访问。
如下图,在 macOS 中测试:
结果就是 p 虽然是空指针,但是依然访问成功了。
这里 read 是可以的 write 是不行的。write 的限制很多,如类型判断,起始位置判断等等,read 可能只需要判断该虚拟空间是否有印射到真实的物理内存中;
因为 0x100000000
超出了 __PAGEZERO
的范围,再来看一个空指针异常的正常流程:
如上图,0x80000000
在 __PAGEZERO
段内,访问时直接报错;
其实这个测试也可以在 ARM64 的 iphone 上验证,只不过 iOS 中有 Slide,所以需要用 expression
来实时赋值:
直接访问
0x100000000
会报错,因为 Slide 的原因,该内存未被分配物理内存,也会报 Bad Access;所以这里感觉可以验证指针访问的内存如果没有印射到物理内存的话,实际是会报错的,即:指针访问的有效内存都是已经 Page In 过的,否则就会 Bad Access;
所以,__PAGEZERO
的目的就是为了判断空指针的访问,因此这个地址越大,其有效性就越高。
最后,作用补充了一点,__PAGEZERO
设置成 4GB 的另外一个原因是为了隔绝 32 位系统,比如指针小于 4GB 时,那这大概率是一个 32 位系统的指针,那么指向了 __PAGEZERO
就会直接报错。这就意味着 arm64 架构不支持 32 位的软件了;
总结:
- PAGEZERO 在 iOS 中大小为 4GB;
- arm64 使用 48 位做内存地址,虚拟内存最大可以是 256TB;
- iOS 中源码体现的虚拟内存最大为 64GB (0x10|0000|0000);
- PAGEZERO 的目的之一是为了捕获空指针的访问;
- PAGEZERO 的目的之二是为了隔绝 32 程序的运行;
3. 为什么64位系统的高16 位会被剪掉
32 位系统最大 CPU 最大寻址能力是 4GB,这已经有点不够用了,64位架构应运而生。
首先,64位系统的寻址能力是这么大:
但是当前的电脑内存一般是 16~32GB,手机一般 2~16GB,所以使用 64位来寻址会造成:
- 硬件设计更复杂;
- 性价比不高;
虽然有些架构确实采用了 64 位寻址地址来进行设计和实现,如 SPARC 的 64 位版就允许完整的 64 位寻址空间,AArch64 允许用高 8 位来做tag,那么还有56 位寻址空间~~
但是主流的 64 位架构都是使用 48 位地址进行寻址,高 16 位做保留,这样做有几个好处:
- 性价比高;
- 以后逐步放开高 16 位即可兼容;
- 寻址设计相对简单(如 Page Table 的设计);
因此,48 位寻址的架构,其地址的范围就是 0~256TB,这在一段时间内已经很够用了。
另外需要注意一点:
- 高16位必须是全1或全0,而且必须与低48位的最高位(第47位)一致;
设计为带符号扩展的原因也很简单:很多环境中,寻址空间的高一半(higher-half)有特殊用途,而低一半(lower-half)给用户做一般用途。这“高/低”可以通过最高位是1还是0来判断;如果把地址看成带符号整数,那么“负数”部分就是高一半,“正数”部分就是低一半。
维基百科: