为啥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 的实际内容通常(但不总是)是异常向量表,因此在非特权模式下无法访问。请查阅您的特定架构的文档。

关于入口点_startmain: 如果链接到 C 运行时(C 标准库),该库会包装名为 main 的函数,因此它可以在调用 main 之前初始化环境。在 Linux 上,这些是应用程序的 argcargv 参数、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 可执行文件的装载与进程

读书笔记|《程序员的自我修养》- 04 可执行文件的装载与进程

execve缓冲区溢出成功完成后的CPU执行流程? ..int 0x80成功完成之后?