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

执行时,程序将从虚拟地址0x80482c0开始运行。 这个地址不是指向main()过程,而是指向由链接器创build的名为_start的过程。

到目前为止,我的Google研究只是引发了一些(含糊)的历史猜测:

有一个民间传说,0x08048000曾经是STACK_TOP(也就是栈从0x08048000附近向0向下生长)在* NIX的端口到i386,是由加利福尼亚州圣克鲁兹的一个小组发布的。 这是128MB内存昂贵,而4GB的RAM是不可想象的。

任何人都可以确认/否认这一点?

正如Mads指出的那样,为了通过空指针捕捉大多数访问,类Unix系统倾向于使地址为零的页面“未映射”。 因此,访问立即触发CPUexception,换句话说就是段错误。 这比让应用程序stream氓更好。 但是,exception向量表可以位于任何地址,至less在x86处理器(有一个特殊的寄存器,加载了lidt操作码)。

起点地址是描述内存如何布置的一组约定的一部分。 链接器在生成可执行二进制文件时必须知道这些约定,所以它们不可能改变。 基本上,对于Linux而言,内存布局约定在90年代初从Linux的第一个版本inheritance而来。 一个过程必须能够访问几个领域:

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

现在, malloc()所在的堆是由mmap()调用支持的,该调用在内核认为合适的地址处获得大块内存。 但是在过去的时代,Linux就像以前的类Unix系统一样,它的堆在一个不间断的块中需要大面积,这可能会增加地址。 所以无论是什么约定,它必须填充代码并堆栈到低地址,并且在给定点到堆之后给每个地址空间块。

但也有一些堆栈,通常很小,但在某些场合可能会显着增长。 堆栈增长下来,当堆栈满了,我们真的希望这个过程可预测地崩溃,而不是覆盖一些数据。 所以这个堆栈必须有一个大的区域,在这个区域的低端,一个未映射的页面。 而且! 在地址为零的地方有一个未映射的页面来捕获空指针的解引用。 因此,它被定义为堆栈将获得第一个128 MB的地址空间,除了第一页。 这意味着代码必须在128 MB之后,类似于0x080xxxxx的地址。

正如Michael所指出的那样,“释放”128 MB的地址空间并不是什么大问题,因为地址空间对于实际使用的内容来说是非常大的。 当时,Linux内核将单个进程的地址空间限制为1 GB,超过了硬件允许的最大4 GB,这并不是一个大问题。

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

  • 因为地址零被称为NULL指针,并被编程语言用来理解检查指针。 如果你要在那里执行代码,你不能使用地址值。
  • 地址0处的实际内容通常(但不总是)是exception向量表,因此不能在非特权模式下访问。 请参阅您特定体系结构的文档。

至于入口点_start vs main :如果你链接到C运行库(C标准库),库封装了名为main的函数,所以它可以在调用main之前初始化环境。 在Linux上,这些是应用程序的argcargv参数, envvariables以及可能的某些同步原语和锁。 它还确保从主通道返回状态码,并调用_exit函数,终止进程。