在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的变化。很难找到 对此事的任何解释。有人有线索吗?
答案 0 :(得分:30)
结论: amd64
在使用大地址时遇到的一些技术限制建议将较低2GiB
的地址空间专用于代码和数据以提高效率。因此,堆栈已经重新定位在此范围之外。
在i386
ABI 1
0x8048000
下方向下增长。这提供“超过128 MB
对于堆栈和大约2 GB的文本和数据“(第3-22页)。0x80000000
(2GiB)开始,1GiB
,至少从0xC0000000
开始(第3-21页)({{3} })。128MiB
(288KiB
)之上的某些堆栈空间将被保留用于那个目的。 amd64
(which is what it typically does被公式化为对i386
的修正(第9页))具有更大的(48位)地址空间但大多数指令只接受32位立即操作数(包括跳转指令中的直接地址和偏移),需要更多的工作和效率更低的代码(特别是在考虑指令相互依赖性时)来处理更大的值。作者总结了一些解决这些局限性的措施,他们建议使用一些“代码模型”来“允许编译器生成更好的代码”。(p.33)
0x00000000
到0x7effffff
“,它允许一些非常有效的相对引用和数组迭代。这是1.98GiB
,对于许多程序来说已经足够了。movabs
指令,就像介质一样
代码模型,甚至用于处理文本部分内的地址。此外,分支到其地址时需要间接分支
当前指令指针的偏移量是未知的。“他们继续建议将代码库拆分为多个共享库,因为这些度量不适用于具有已知在边界内的偏移的相对引用(如”小位置独立代码模型“)。因此堆栈被移动到共享库空间(0x80000000000
,128GiB
)下,因为它的地址永远不是立即操作数,总是间接引用或lea
/ {{1来自另一个引用,因此只适用相对偏移限制。
以上解释了为什么加载地址被移动到较低的地址。现在,为什么它完全移至mov
(0x400000
)?在这里,我是空的,总结了我在ABI规范中所读到的内容,我只能猜测它感觉“恰到好处”:
4MiB
操作的更大数据单元,但又足够小,不会浪费大量有价值的起始amd64
地址空间。 1 请注意,随着时间的推移,实际的x32 Linux已偏离此布局whose ABI和more。但是我们在这里谈论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 大页面区域,或者无论如何扩展到第二个区域。