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

时间:2016-09-25 17:04:11

标签: linux memory x86-64 elf abi

this文件中。 27它说文本段开始于 为0x400000。为什么选择这个特定的地址?有没有 原因是什么?在GNU ld上的Linux中选择了相同的地址:

$ 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;

我阅读了this question,其中讨论了为什么选择了0x080xxxxx地址 对于i386,但它没有解释x86_64的变化。很难找到 对此事的任何解释。有人有线索吗?

2 个答案:

答案 0 :(得分:30)

结论: amd64在使用大地址时遇到的一些技术限制建议将较低2GiB的地址空间专用于代码和数据以提高效率。因此,堆栈已经重新定位在此范围之外。

i386 ABI 1

  • 堆栈位于代码之前,从0x8048000下方向下增长。这提供“超过128 MB 对于堆栈和大约2 GB的文本和数据“(第3-22页)。
  • 动态细分从0x80000000(2GiB)开始,
  • 并且内核占据顶部的“保留区域”,规范允许最多1GiB,至少从0xC0000000开始(第3-21页)({{3} })。
  • 主程序不需要与位置无关。
  • 捕获空指针访问不需要实现(第3-21页),但可以预期128MiB288KiB)之上的某些堆栈空间将被保留用于那个目的。

amd64which is what it typically does被公式化为对i386的修正(第9页))具有更大的(48位)地址空间但大多数指令只接受32位立即操作数(包括跳转指令中的直接地址和偏移),需要更多的工作和效率更低的代码(特别是在考虑指令相互依赖性时)来处理更大的值。作者总结了一些解决这些局限性的措施,他们建议使用一些“代码模型”来“允许编译器生成更好的代码”。(p.33)

  • 具体来说,他们中的第一个,“小代码模型”,建议使用地址“,范围从0到2 31 -2 24 -1或者从0x000000000x7effffff,它允许一些非常有效的相对引用和数组迭代。这是1.98GiB,对于许多程序来说已经足够了。
  • “中间代码模型”基于前一个,将数据分割成上述边界下的“快速”部分和“较慢”的剩余部分,需要特殊的访问指令。虽然代码仍在边界之下。
  • 只有“大”模型不做任何关于大小的假设,要求编译器“使用movabs指令,就像介质一样 代码模型,甚至用于处理文本部分内的地址。此外,分支到其地址时需要间接分支 当前指令指针的偏移量是未知的。“他们继续建议将代码库拆分为多个共享库,因为这些度量不适用于具有已知在边界内的偏移的相对引用(如”小位置独立代码模型“)。

因此堆栈被移动到共享库空间(0x80000000000128GiB)下,因为它的地址永远不是立即操作数,总是间接引用或lea / {{1来自另一个引用,因此只适用相对偏移限制。

以上解释了为什么加载地址被移动到较低的地址。现在,为什么它完全移至mov0x400000)?在这里,我是空的,总结了我在ABI规范中所读到的内容,我只能猜测它感觉“恰到好处”:

  • 它足够大,可以捕获任何可能不正确的结构偏移,允许4MiB操作的更大数据单元,但又足够小,不会浪费大量有价值的起始amd64地址空间。
  • 它等于迄今为止最大的实际页面大小,是人们可以想到的所有其他虚拟内存单元大小的倍数。

1 请注意,随着时间的推移,实际的x32 Linux已偏离此布局whose ABImore。但是我们在这里谈论ABI规范,因为2GiB正式基于它而不是任何派生布局(参见其引用的段落)。

答案 1 :(得分:0)

低地址的静态代码/数据,高地址的堆栈,是the traditional model。 x86-64 遵循这一点; i386 是不寻常的。 (中间有“堆”,尽管这在 asm 中不是真实的;.text 上方有 .data/.bss,brk 在 .bss 之后添加更多空间,并且 mmap 在两者之间选择随机地址.)

i386 布局为将堆栈放在代码下方留出了空间,但现代 Linux 无论如何都没有这样做。您仍然会在 32 位代码 (e.g. under a 64-bit kernel) 中获得类似 0xffffe000 的堆栈地址。我不确定 32 位内核的现代版本会将用户空间堆栈放在哪里。当然,这只是针对主线程的堆栈;新线程的堆栈必须手动分配,通常使用 mmap。


为什么 0x400000 (4 MiB) 专门用于 ld 默认基地址?

足够高以避免 mmap_min_addr (默认 64k) 并留下间隙,因此 NULL deref 仍然可能会嘈杂地出错,而不是静默读取代码。即使它就像带有一些大 ptr[i]i。但否则靠近虚拟地址空间的底部是个好地方,

也是为了优化页表:它们是一个稀疏的基数树(this answer 中的图表)。理想情况下,使用中的页面尽可能多地共享树的更高级别,因此树的更高级别大多具有“不存在”条目。内核分配和管理更少,硬件页表遍历器可以在内部缓存更高级别的条目(PDE 缓存),以加快 4k 页面中处于相同 2M、1G 或 512G 区域时的 TLB 未命中。以及 page-walker(s) accesses memory through cache,因此更小的页表也意味着这些访问的缓存占用更少。

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

好吧,几乎尽可能少的 1G 区域:从 0x40000000 (1 GiB) 开始会将其放在 1GiB 区域的最开始,而不是跳过前两个 2MiB它的大页面。但这仅在您的静态数据大小略低于 1GiB 时才重要,否则您仍然适合第一个 1GiB 大页面区域,或者无论如何扩展到第二个区域。