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



在第27页的这个文档中,它说文本段开始于0 x400000。为什么选择这个特定的地址?有吗?为什么呢?在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;

我读了这个讨论为什么选择0x080xxxxx地址的问题对于i386,但它不能解释x86_64的变化。很难找到对那件事有什么解释吗?有人知道吗?

底线: amd64在使用大地址时的一些技术限制建议将地址空间的较低2GiB专用于代码和数据以提高效率。


i386 ABI1

  • 堆栈位于代码之前,从0x8048000下面向下增长。哪一个提供的>>略大于128 MB (p. 3-22)。
  • 动态段从0x80000000 (2GiB)开始,
  • 和内核占用顶部的"保留区域",规范允许最多1GiB,至少从0xC0000000开始(p. 3-21)(这是它通常做的)。
  • 不要求主程序与位置无关。
  • 实现不需要捕获空指针访问(第3-21页),但是期望128MiB(即288KiB)之上的一些堆栈空间将为此目的保留是合理的。

amd64(它的ABI是作为i386的修正而制定的)具有更大的(48位)地址空间,但是大多数指令只接受32位的直接操作数(包括跳转指令中的直接地址和偏移量),需要更多的工作和更低效率的代码(特别是在考虑指令相互依赖性时)来处理更大的值。作者通过介绍一些"代码模型"来总结解决这些限制的方法,他们建议使用这些模型来"允许编译器生成更好的代码"。 (p. 33)

  • 具体来说,其中第一个,"小代码模型",建议使用地址"范围从0到231-224-1或从0x000000000x7effffff",这允许一些非常有效的相对引用和数组迭代。这是1.98GiB,对于许多程序来说已经足够了。
  • "中码模型"是在前一种模型的基础上,数据划分为上述边界下的"快"部分和需要特殊指令访问的"慢"部分。而代码仍然在边界下。
  • 并且只有"large"模型没有对大小做任何假设,要求编译器"使用movabs指令,就像在medium中一样代码模型,甚至用于处理文本部分中的地址。另外,当分支到地址时,需要使用间接分支当前指令指针的偏移量未知。他们继续建议将代码库拆分为多个共享库,因为这些措施不适用于已知偏移量在界限内的相对引用(如"小位置独立代码模型"中所述)。

因此堆栈被移动到共享库空间(0x80000000000, 128GiB)下,因为它的地址永远不是直接操作数,总是间接引用或与其他引用的lea/mov引用,因此只适用相对偏移限制。


上面解释了为什么加载地址被移动到一个较低的地址。现在,为什么它被精确地移动到0x400000 (4MiB)?在这里,我一无所获,所以,总结一下我在ABI规范中读到的内容,我只能猜测它感觉"刚刚好":

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

1 请注意,随着时间的推移,实际的x32 linux已经越来越偏离这种布局。但是我们在这里讨论的是ABI规范,因为amd64规范是正式基于它而不是任何派生的布局(参见其段落以获取引用)。

静态代码/数据在低地址,堆栈在高地址,是传统的模式。X86-64紧随其后;I386是不寻常的。(有"堆"字)在中间,尽管这在asm中并不存在;. data/。.text上面的bss, brk在.bss后面增加更多的空间,mmap在两者之间随机选择地址

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


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

足够高,以避免mmap_min_addr(默认64k),并留下一个空白,因此NULL deref仍然可能出现噪音故障,而不是默默地读取代码。即使它像ptr[i]和一些大的i。除此之外,在虚拟地址空间的底部附近是一个好地方,

也是为了优化页表:它们是一个稀疏的基数树(图在这个答案)。理想情况下,正在使用的页面共享尽可能多的更高层次的树,因此更高层次的树大多"不存在"。条目。内核分配&HW页表漫步器可以在内部缓存更高级别的条目(PDE缓存),以加速4k页在相同的2M、1G或512G区域中的TLB丢失。而且页游动器通过缓存访问内存,所以更小的页表也意味着这些访问的缓存占用更少。

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

嗯,几乎尽可能少的1G区域:从0x40000000 (1GiB)开始,将使它处于1GiB区域的最开始,而不是跳过它的前两个2mb大的页面。但这只有在静态数据大小低于1GiB时才有意义,否则您仍然适合第一个1GiB的大页面区域,或者扩展到第二个区域。


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

最新更新