main()
过程,而是链接器创建的名为_start
的过程。目前我的谷歌研究只导致了一些(模糊的)历史推测,比如这个:
“有一种传说是0x08048000曾经是STACK_TOP(也就是说,栈从接近0x08048000向0增长)在* NIX到i386的一个端口上,由来自加利福尼亚州圣克鲁斯的一个团队宣传。那是128MB的RAM昂贵的时候,4GB的RAM是不可想象的。”
有人能够确认/否认这个吗?
main()
过程,而是链接器创建的名为_start
的过程。正如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的硬件,这被认为不是一个大问题。
为什么不能从地址0x0开始?这至少有两个原因:
至于入口点_start
与main
的区别:
main
的函数,以便在调用main
之前初始化环境。在Linux上,这些是应用程序的argc和argv参数、env变量,以及可能一些同步原语和锁。它还确保从主函数返回传递状态码,并调用_exit
函数终止进程。
_GLOBAL_OFFSET_TABLE_
在 Binutils 2.24 中也指向 0x200XXX
范围。 - Ciro Santilli OurBigBook.comNULL
表示为除0
以外的任何值都是毫无意义的性能损失。即使在内存非常有限的嵌入式环境中,0x00
通常也会被保留用于表示NULL
。标准允许并不意味着这是个好主意。 - yynyNULL
表示为非零值。C已经超过30年了,人们已经基本达成共识,将NULL
指针常量转换为整数会得到一个值为0
的结果。 - yyny0x00
地址可以映射到内核想要的任何页表上。事实上,在某些主板和某些引导加载程序中,可能会留下第一个RAM插槽为空,这意味着从某种意义上讲甚至没有物理地址'0'。然而,整个观点仍然没有意义。事实上,每个可想象的运行C语言的处理器都可以表示一个0x00
的地址,每个可想象的内核和应用程序都希望如此。将C标准作为终极真理来引用是适得其反的,并且对新程序员是有害的。 - yyny
0x08048000
曾经是STACK_TOP
,那已经是很久以前的事了。后者一直到 2.0.40 都是TASK_SIZE
。 - ivan_pozdeev