为啥ELF执行入口点虚拟地址的形式是0x80xxxxxx而不是0x0?
Posted
技术标签:
【中文标题】为啥ELF执行入口点虚拟地址的形式是0x80xxxxxx而不是0x0?【英文标题】:Why is the ELF execution entry point virtual address of the form 0x80xxxxx and not zero 0x0?为什么ELF执行入口点虚拟地址的形式是0x80xxxxxx而不是0x0? 【发布时间】:2011-01-12 08:22:56 【问题描述】:执行时,程序将从虚拟地址 0x80482c0 开始运行。这个地址并不指向我们的main()
过程,而是指向一个由链接器创建的名为_start
的过程。
到目前为止,我的 Google 研究只是让我得出一些(模糊的)历史推测,如下所示:
民间传说 0x08048000 曾经是 STACK_TOP(即堆栈从 0x08048000 附近向下增长到 0)在 *NIX 到 i386 的端口上,这是由加利福尼亚州圣克鲁斯的一个团体颁布的。当时 128MB 的 RAM 很昂贵,而 4GB 的 RAM 是不可想象的。
任何人都可以确认/否认这一点吗?
【问题讨论】:
如果0x08048000
曾经是STACK_TOP
,那是非常很久以前了。后者是TASK_SIZE
一直到2.0.40。
x86-64 Linux 确实选择了低地址 (Why is address 0x400000 chosen as a start of text segment in x86_64 ABI?):避免 wiki.debian.org/mmap_min_addr,并选择靠近低 1GiB 开头的 2MiB 页面组的开头。 Why is address 0x400000 chosen as a start of text segment in x86_64 ABI? 还解释了 i386 选择 0x080xxxxx 的一些动机。
【参考方案1】:
正如 Mads 所指出的,为了捕获大多数通过空指针进行的访问,类 Unix 系统倾向于使地址为零的页面“未映射”。因此,访问会立即触发 CPU 异常,即段错误。这比让应用程序流氓要好得多。然而,异常向量表可以位于任何地址,至少在 x86 处理器上是这样(为此有一个特殊的寄存器,加载了lidt
操作码)。
起始地址是一组描述内存布局方式的约定的一部分。链接器在生成可执行二进制文件时,必须知道这些约定,因此它们不太可能改变。基本上,对于 Linux,内存布局约定是从 90 年代初的 Linux 的第一个版本继承而来的。一个进程必须可以访问多个区域:
代码必须在包含起点的范围内。 必须有一个堆栈。 必须有一个堆,其限制随着brk()
和sbrk()
系统调用而增加。
必须有空间供mmap()
系统调用使用,包括共享库加载。
如今,malloc()
所在的堆由 mmap()
调用支持,这些调用在内核认为合适的任何地址获取内存块。但在过去,Linux 就像以前的类 Unix 系统一样,它的堆在一个不间断的块中需要一个很大的区域,这可能会随着地址的增加而增长。所以无论约定是什么,它都必须将代码和堆栈填充到低地址,并将给定点之后的地址空间的每一块都分配给堆。
但也有堆栈,它通常很小,但在某些情况下可能会显着增长。堆栈向下增长,当堆栈已满时,我们真的希望进程可以预见地崩溃,而不是覆盖某些数据。所以堆栈必须有一个广阔的区域,在该区域的低端,一个未映射的页面。瞧!在地址 0 处有一个未映射的页面,用于捕获空指针取消引用。因此,定义堆栈将获得前 128 MB 的地址空间,但第一页除外。这意味着代码必须在这 128 MB 之后,位于类似于 0x080xxxxx 的地址。
正如 Michael 指出的那样,“丢失” 128 MB 的地址空间没什么大不了的,因为地址空间对于实际使用的内容来说非常大。当时,Linux内核将单个进程的地址空间限制为1 GB,硬件允许的最大4 GB,这并没有被认为是一个大问题。
【讨论】:
【参考方案2】:为什么不从地址 0x0 开始?这至少有两个原因:
因为地址 0 被称为 NULL 指针,编程语言使用它来完善检查指针。如果要在那里执行代码,则不能为此使用地址值。 地址 0 的实际内容通常(但不总是)是异常向量表,因此在非特权模式下无法访问。请查阅您的特定架构的文档。关于入口点_start
与main
:
如果链接到 C 运行时(C 标准库),该库会包装名为 main
的函数,因此它可以在调用 main
之前初始化环境。在 Linux 上,这些是应用程序的 argc 和 argv 参数、env 变量,可能还有一些同步原语和锁。它还确保从 main 返回传递状态码,并调用 _exit
函数,该函数终止进程。
【讨论】:
在 C 中,空指针的值可能与最低级别的 0 完全不同。在 C(源代码)的范围内,机器的无效指针值必须映射到 0。从技术上讲,C 空指针实际上不需要映射到地址 0。_GLOBAL_OFFSET_TABLE_
也指向 Binutils 2.24 中的 0x200XXX
范围。
@datenwolf 这是一个有争议的问题,所有现代处理器都将地址表示为二进制补码整数,将NULL
表示为0
以外的任何其他值,在这种情况下将是毫无意义的性能损失。仅仅因为标准允许它并不意味着它是一个好主意。即使在内存非常有限的嵌入式环境中,0x00
通常也会保留给NULL
。
@yyny:有些东西是 8086 实模式……另外,我指的不是 2s 补全与其他类型的数字表示,而是 trap 值。
@datenwolf 我知道没有针对 8086 的 C 编译器。这就是我的全部观点。 ANSI C 标准在编写时考虑到了前向兼容性,当时仍可以想象分段内存将变得司空见惯。现在几乎每个处理器都使用基于二进制补码的整数寻址,这意味着没有实际理由将NULL
表示为非零值。 C 在这一点上已经超过 30 年了,并且已经很好地同意将 NULL
指针常量转换为整数会产生 0
的值。以上是关于为啥ELF执行入口点虚拟地址的形式是0x80xxxxxx而不是0x0?的主要内容,如果未能解决你的问题,请参考以下文章
为啥使用“ld -e”选项不能更改 ELF 入口点 0x8048000?
为啥我的可执行文件中的入口点地址是 0x8048330? (0x330 是 .text 部分的偏移量)
为啥 Linux 二进制文件的虚拟内存地址从 0x8048000 开始?
读书笔记|《程序员的自我修养》- 04 可执行文件的装载与进程