为什么 ELF 执行入口点的虚拟地址形式是 0x80xxxxx 而不是零 0x0?

26
执行程序时,程序将从虚拟地址0x80482c0开始运行。此地址不指向我们的main()过程,而是链接器创建的名为_start的过程。
目前我的谷歌研究只导致了一些(模糊的)历史推测,比如这个:
“有一种传说是0x08048000曾经是STACK_TOP(也就是说,栈从接近0x08048000向0增长)在* NIX到i386的一个端口上,由来自加利福尼亚州圣克鲁斯的一个团队宣传。那是128MB的RAM昂贵的时候,4GB的RAM是不可想象的。”
有人能够确认/否认这个吗?

1
如果 0x08048000 曾经是 STACK_TOP,那已经是很久以前的事了。后者一直到 2.0.40 都是 TASK_SIZE - ivan_pozdeev
x86-64 Linux选择低地址(为什么在x86_64 ABI中将地址0x400000选择为文本段的起始位置?):避免使用https://wiki.debian.org/mmap_min_addr,并选择靠近低1GiB开头的2MiB页面组的起始位置。 为什么在x86_64 ABI中将地址0x400000选择为文本段的起始位置?还解释了i386选择0x080xxxxx的一些动机。 - Peter Cordes
2个回答

39

正如Mads所指出的那样,为了捕获大多数对空指针的访问,类Unix系统倾向于使地址为零的页面“未映射”。因此,访问会立即触发CPU异常,换句话说就是分段错误。这比让应用程序失控要好得多。然而,异常向量表可以位于任何地址,至少在x86处理器上是这样(有一个特殊的寄存器,加载lidt操作码)。

起始点地址是一组约定中的一部分,用于描述内存的布局。链接器在生成可执行二进制文件时必须知道这些约定,因此它们不太可能改变。基本上,对于Linux,内存布局约定继承自早期90年代的Linux第一个版本。一个进程必须能够访问几个区域:

  • 代码必须在包括起始点在内的范围内。
  • 必须有一个栈。
  • 必须有一个堆,其限制通过brk()sbrk() 系统调用增加。
  • 必须有一些空间用于mmap() 系统调用,包括共享库加载。

现在,堆(malloc()所在的地方)由mmap() 调用支持,该调用在内核看到合适的任意地址上获得内存块。但在早期,Linux就像以前的类Unix系统一样,其堆需要一个不间断的大区域,该区域可以向增加的地址方向增长。因此,无论约定是什么,它都必须把代码和栈塞向低地址,并将给定点之后的每个地址空间块赋予堆。

但是还有一个堆栈(stack),通常很小,但在某些情况下可能会大幅增长。堆栈向下增长,当堆栈已满时,我们真的希望进程可以可预测地崩溃,而不是覆盖一些数据。因此,必须为堆栈留出广泛的区域,在该区域的低端有一个未映射的页面。幸运的是,在地址0处有一个未映射的页面,可以捕获空指针引用错误。因此,定义堆栈将获得前128 MB的地址空间,除了第一页之外。这意味着代码必须跟随这128 MB之后,位于类似于0x080xxxxx的地址。

正如Michael所指出的那样,“丢失” 128 MB的地址空间并不是什么大问题,因为地址空间非常大,与实际使用的空间相比非常宽裕。当时,Linux内核将单个进程的地址空间限制为1 GB,最大允许4 GB的硬件,这被认为不是一个大问题。


7

为什么不能从地址0x0开始?这至少有两个原因:

  • 因为地址零被广泛地称为NULL指针,并被编程语言用于检查指针的有效性。如果你在那里执行代码,就不能使用一个地址值来进行检查。
  • 地址0处的实际内容通常(但不总是)是异常向量表,并且在非特权模式下无法访问。请参阅您特定架构的文档。

至于入口点_startmain的区别:

如果链接到C运行时库(C标准库),则该库会包装名为main的函数,以便在调用main之前初始化环境。在Linux上,这些是应用程序的argcargv参数、env变量,以及可能一些同步原语和锁。它还确保从主函数返回传递状态码,并调用_exit函数终止进程。


6
在C语言中,空指针的值在最底层可能与0完全不同。在C语言范围内(源代码),机器的无效指针值必须映射到0。技术上讲,C语言中的空指针实际上并没有要求映射到地址零。 - datenwolf
_GLOBAL_OFFSET_TABLE_ 在 Binutils 2.24 中也指向 0x200XXX 范围。 - Ciro Santilli OurBigBook.com
1
@datenwolf 这是一个无意义的争论,所有现代处理器都将地址表示为二进制补码整数,在这种情况下,将NULL表示为除0以外的任何值都是毫无意义的性能损失。即使在内存非常有限的嵌入式环境中,0x00通常也会被保留用于表示NULL。标准允许并不意味着这是个好主意。 - yyny
@yyny:关于8086实模式的一些内容...我也不是在提到二进制补码和其他数字表示方式,而是陷阱值。 - datenwolf
@datenwolf 我不知道有任何针对8086的C编译器。这就是我的全部观点。ANSI C标准是以向前兼容为目标编写的,在那个时候,分段内存仍然可能变得普遍。现在,几乎每个处理器都使用二进制补码整数寻址,这意味着没有实际理由将NULL表示为非零值。C已经超过30年了,人们已经基本达成共识,将NULL指针常量转换为整数会得到一个值为0的结果。 - yyny
换个角度看,使用分页内存,0x00地址可以映射到内核想要的任何页表上。事实上,在某些主板和某些引导加载程序中,可能会留下第一个RAM插槽为空,这意味着从某种意义上讲甚至没有物理地址'0'。然而,整个观点仍然没有意义。事实上,每个可想象的运行C语言的处理器都可以表示一个0x00的地址,每个可想象的内核和应用程序都希望如此。将C标准作为终极真理来引用是适得其反的,并且对新程序员是有害的。 - yyny

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接