为什么在x86_64 ABI中选择0x400000作为文本段的起始地址?

36
这份文档的第27页中指出,文本段从0x400000开始。为什么选择了这个特定的地址?有什么原因吗?在Linux上的GNU ld也选择了同样的地址。
$ ld -verbose | grep -i text-segment
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;

这让人感到惊讶,因为在32位x86可执行文件中,这个地址是更大的:

$ ld -verbose | grep -i text-segment
  PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x08048000)); . = SEGMENT_START("text-segment", 0x08048000) + SIZEOF_HEADERS;

我阅读了这个问题,讨论了为什么在i386架构中选择0x080xxxxx地址作为入口点,但没有解释为什么在x86_64中发生了变化。很难找到任何关于此事的解释。有人知道吗?


根据OSDev Wiki(http://wiki.osdev.org/System_V_ABI#Documents),https://www.uclibc.org/docs/psABI-x86_64.pdf是最新版本(0.99.7)。 - ivan_pozdeev
0x400000 是4MiB,所以这可能与大页支持有关。然而,第3.3.3节只允许页面大小最多为64KiB。 - ivan_pozdeev
@ivan_pozdeev: https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI 上有从git HEAD修订的LaTeX源代码构建的PDF链接。x86-64页面表可以使用2MB巨大页面(甚至1GiB),Linux会透明/机会主义地为匿名内存这样做,但不适用于文件支持映射。我猜4MB离0足够远,使得索引的NULL指针解引用通常不会索引到有效页面。哈哈,我看到你在回答中也说了同样的话。 - Peter Cordes
为什么不呢?为什么要选择另一个地址? - wildplasser
2个回答

37

底线: amd64 在使用大地址时存在一些技术限制,建议将低 2GiB 的地址空间专门用于代码和数据以提高效率。因此,堆栈已经被搬出这个范围。


i386 ABI1 中:

  • 堆栈位于代码之前,从 0x8048000 下降增长。这提供了略多于 128MB 的堆栈空间和约 2GB 的文本和数据空间 (第3-22页)。
  • 动态段从 0x80000000 (2GiB) 开始,
  • 内核占据了顶部的“保留区域”,规范允许最多达到 1GiB ,起始位置至少为 0xC0000000 (第3-21页) (通常情况如此)。
  • 主程序不需要具有位置独立性。
  • 实现不需要捕获空指针访问 (第3-21页),但合理地预计会有一些堆栈空间位于 128MiB 以上(即 288KiB),用于此目的。

amd64其ABI是对i386的修改(第9页))具有更大的(48位)地址空间,但大多数指令只接受32位立即数操作数(包括跳转指令中的直接地址和偏移量),需要更多的工作和效率较低的代码(特别是考虑指令相互依赖性时)来处理较大的值。作者总结了一些解决这些限制的措施,通过引入一些他们建议使用的“代码模型”来使编译器生成更好的代码(第33页)。

  • 具体而言,第一个“小型代码模型”建议使用地址“从0到2 31 -2 24 -1或从0x000000000x7effffff范围内”,这允许非常高效的相对引用和数组迭代。这是 1.98GiB ,对于许多程序来说已经足够。
  • “中型代码模型”基于前一个模型,将数据分为在上述边界下的“快速”部分和需要特殊指令才能访问的“较慢”的剩余部分。而代码保持在边界以下。
  • 只有“大型”模型对大小不做任何假设,要求编译器使用movabs指令处理文本节内的地址,就像中型代码模型一样。另外,当跳转到当前指令指针的偏移量未知的地址时需要使用间接分支。他们继续建议将代码库拆分为多个共享库,因为这些措施不适用于已知处于边界内的相对引用(如“小型位置无关代码模型”中所述)。
因此,堆栈被移动到共享库空间下面(0x80000000000, 128GiB),因为它的地址从不是立即操作数,始终是通过另一个引用间接地或使用 lea/mov 引用,因此只适用相对偏移限制。
以上解释了为什么加载地址被移动到较低的地址。现在,为什么要将其移动到恰好的 0x4000004MiB)呢?在这里,我没有答案,简单总结一下我在 ABI 规范中读到的内容,我只能猜测它感觉“刚刚好”:
  • 它足够大,可以捕获任何可能的错误结构偏移量,允许更大的数据单元被 amd64 操作,但足够小,不会浪费宝贵的起始 2GiB 地址空间。
  • 它等于到目前为止最大的实际页面大小,并且是所有其他虚拟内存单元大小的倍数。

1请注意,随着时间的推移,实际的 x32 Linux 已经偏离了这种布局,详情请参见此处此处。但是这里我们谈论的是 ABI 规范,因为 amd64 规范形式上基于它而不是任何派生布局(请参见它的段落以获取引用)。


0

低地址处是静态代码/数据,高地址处是栈,即传统模型。x86-64遵循这种模式;i386则是不寻常的。(有“堆”位于中间,虽然在汇编中并不存在这样的东西;.data/.bss在.text上方,brk会在.bss刚过之后添加更多的空间,并且mmap会在它们之间选取随机地址)。

i386布局为代码下方留出了放置栈的空间,但是现代Linux也没有利用它。在32位代码中,你仍然会获得像0xffffe000这样的栈地址(例如在64位内核下)。我不确定现代32位内核构建会将用户空间栈放在哪里。当然,这只针对主线程的栈;新线程的栈必须手动分配,通常使用mmap。


为什么默认的ld基地址是0x400000(4 MiB)?

足够高以避免{{link1:mmap_min_addr}}(默认为64k),并留下一个间隔,使得NULL解引用仍可能产生嘈杂错误,而不是静默地读取代码。即使像ptr[i]这样带有一些大的i。但否则虚拟地址空间底部附近是一个好地方。

另外,为了优化页面表:它们是一种稀疏的基数树(在this answer中有图示)。理想情况下,使用中的页面应尽可能共享更多的树高级别,因此树的高级别大多数是“不存在”的条目。这样内核需要分配和管理的就会更少,硬件页表行走器可以在4k页面的TLB缺失时内部缓存更高级别的条目(PDE缓存),以加快访问同一2M、1G或512G区域中的页面。而且页面行走器通过缓存访问内存,因此较小的页面表也意味着这些访问的缓存占用更少的空间。

0x400000 = 4MiB。它是靠近虚拟地址空间低1GiB开始的2MiB页面组的起点。因此,具有需要多个页面的较大代码和/或静态数据的可执行文件将它们全部放在页面表的同一子树中,尽量接触最少的1G和2M区域。

好吧,几乎是最少的1G区域:从0x40000000(1 GiB)开始会将它放在1GiB区域的第一个两个2MiB大页之前。但是,这只有在您的静态数据大小略小于1GiB时才有关系,否则您仍适合于第一个1GiB大页面区域,或者在任何情况下都扩展到第二个页面。


基本上是 为什么Linux/gnu链接器选择地址0x400000? 的一个副本 - 当我回答那个问题时,我忘记了我已经回答过这个问题。


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