正如 Mads 指出的那样,为了通过空指针捕获大多数访问,类 Unix 系统倾向于使地址为零的页面“未映射”。因此,访问会立即触发 CPU 异常,即段错误。这比让应用程序流氓要好得多。但是,异常向量表可以位于任何地址,至少在 x86 处理器上是这样(有一个特殊的寄存器用于加载lidt
操作码)。
起始地址是一组描述内存布局方式的约定的一部分。链接器在生成可执行二进制文件时,必须知道这些约定,因此它们不太可能改变。基本上,对于 Linux,内存布局约定是从 90 年代初的 Linux 的第一个版本继承而来的。一个进程必须能够访问多个区域:
- 代码必须在包含起点的范围内。
- 一定有栈。
- 必须有一个堆,其限制随着系统调用
brk()
和sbrk()
系统调用而增加。
- 系统调用必须有一些空间
mmap()
,包括共享库加载。
如今,堆所在的位置malloc()
由mmap()
调用支持,这些调用在内核认为合适的任何地址获取内存块。但在过去,Linux 就像以前的类 Unix 系统一样,它的堆在一个不间断的块中需要一个很大的区域,这可能会随着地址的增加而增长。因此,无论约定是什么,它都必须将代码和堆栈填充到低地址,并将给定点之后的地址空间的每一块都分配给堆。
但也有堆栈,它通常很小,但在某些情况下可能会显着增长。堆栈向下增长,当堆栈已满时,我们真的希望进程可以预见地崩溃,而不是覆盖某些数据。所以堆栈必须有一个广阔的区域,在该区域的低端,一个未映射的页面。瞧!在地址 0 处有一个未映射的页面,用于捕获空指针取消引用。因此,定义堆栈将获得前 128 MB 的地址空间,但第一页除外。这意味着代码必须在这 128 MB 之后,位于类似于 0x080xxxxx 的地址处。
正如迈克尔指出的那样,“丢失” 128 MB 的地址空间没什么大不了的,因为地址空间对于实际使用的内容来说非常大。当时,Linux内核将单个进程的地址空间限制为1 GB,超过硬件允许的最大4 GB,这被认为不是什么大问题。