【问题标题】:Data Structure Alignment : Linker or Compiler数据结构对齐:链接器或编译器
【发布时间】:2017-07-15 02:18:41
【问题描述】:

数据结构对齐对应于谁的任务?是编译器、链接器、加载器还是硬件本身,就像 x86 一样?编译器是否进行相对对齐的寻址,这样当链接器在编译的可执行文件中正确“放置”时,数据结构总是与各自的本机大小边界对齐?装载机以后还要做什么工作?

【问题讨论】:

    标签: c linker x86 operating-system executable


    【解决方案1】:

    我认为正确的最短答案:这是编译器的工作。

    这就是为什么有各种 #pragmas 和其他编译器级别的魔术旋钮,您可以在必要时扭动来控制对齐。

    我不认为 C 语言指定了那些其他组件(链接器和加载器)的存在。

    【讨论】:

    • 嗯,在 C11 标准中暗示了链接,甚至在脚注中提到了链接器这个词;以及其他提及,例如“翻译单元可以单独翻译,然后再链接以生成可执行程序。” - 当然,标准并不要求必须存在单独的链接器。
    • 这是可以理解的,编译器设计者不能说链接器,更不用说加载器了,但是实际的机制,我可以说取决于实现,它包括链接器,更本机的是加载器?
    • 它们不需要被提及为对齐感知:标准说明事情必须如何工作,然后工具链以一种有意义的方式实现它。在大多数情况下,这意味着整个工具链,从编译器到链接器,再到加载器/运行时链接器,都需要了解对齐方式。实际上,该标准甚至不需要链接器或编译器(例如,您可以使用解释型 C),但实际上这正是它在大多数平台上的实现方式。
    【解决方案2】:

    数据对齐与代码生成紧密结合。
    考虑在某个边界上对齐局部变量的函数的序言和尾声生成的所有负担[live example]

    下面的两个代码是由同一个函数生成的,但对齐方式不同(左边一个32B,右边一个4B)

    foo(double):                         foo(double):
       push    ebp                          lea     ecx, [esp+4]
       mov     ebp, esp                     and     esp, -8
       sub     esp, 40                      push    DWORD PTR [ecx-4]
       mov     eax, DWORD PTR [ebp+8]       push    ebp
       mov     DWORD PTR [ebp-40], eax      mov     ebp, esp
       mov     eax, DWORD PTR [ebp+12]      push    ecx
       mov     DWORD PTR [ebp-36], eax      sub     esp, 20
       fld1                                 mov     eax, ecx
       fstp    QWORD PTR [ebp-8]            fld1
       fld     QWORD PTR [ebp-40]           fstp    QWORD PTR [ebp-16]
       fstp    QWORD PTR [ebp-16]           fld     QWORD PTR [eax]
       fld     QWORD PTR [ebp-8]            fstp    QWORD PTR [ebp-24]
       fmul    QWORD PTR [ebp-16]           fld     QWORD PTR [ebp-16]
       leave                                fmul    QWORD PTR [ebp-24]
       ret                                  add     esp, 20
                                            pop     ecx
                                            pop     ebp
                                            lea     esp, [ecx-4]
                                            ret
    

    虽然此示例涉及堆栈的对齐方式,但其目的是显示出现的复杂情况。
    结构对齐的工作原理相同。

    为了将此责任推给链接器,编译器必须生成临时代码和大量元数据,以便链接器可以修补必要的指令。
    适应有限的链接器接口会导致生成次优代码。
    丰富链接器功能会将编译器-链接器边界向左移动,从而有效地使后者“排序”为小型编译器。

    加载程序无法处理程序数据 - 无论程序如何访问数据,它都必须加载任何程序,并尽可能将代码和数据视为不透明。
    特别是,加载程序通常填充或重写可执行元数据,而不是代码或数据。
    让代码每次通过元数据读取结构字段将是一个巨大的性能损失,完全没有理由。

    硬件没有结构的概念,也没有程序的意图。
    当被指示从 X 读取时,它会尽可能快且正确地从 X 读取,但它不会赋予该 X任何意义>.
    硬件按要求执行。
    如果不能,则发出条件信号。 x86 架构的对齐要求非常宽松,但代价是操作延迟可能会加倍(或最差)。


    编译器全权负责对齐数据。
    这样做时会派上用场的两个引理是1

    • 如果和对象 aX-aligned 相对于 Y-aligned 对象 b 和 X | YYX 的倍数)然后 aX 对齐关于 b 的相同参考。

      例如,PE/ELF 文件(甚至mallocd 缓冲区)中的部分可以在特定边界(8 字节、16 字节、4KiB 等)对齐加载。
      如果一个部分以 4KiB 对齐加载,那么所有直到 212 的二次方对齐都会在内存中自动得到尊重,即使它们是相对于该部分的开头进行的,无论该部分在何处加载。

    • 在长度为2X-1的缓冲区B中,至少有一个X-aligned地址A em> 并且 2X-1 - (A-B) >= X (它有足够的空间容纳大小为 X 的对象)。

      如果您需要在 8 字节边界对齐对象并且该对象的长度为 8 字节(通常是这样),那么分配 16-1 = 15 字节的缓冲区将保证为 每个缓冲区的可能起始地址。

    由于这两个引理以及与加载程序的既定约定,编译器无需借助其他工具即可完成其职责。


    1没有过多解释。

    【讨论】:

    • 我能说什么,这是对该主题的最佳解释...我想我需要更多的调试经验才能理解它的全部内容...谢谢您的精彩解释。
    • 这并不完全正确 - 链接器和加载器(也称为运行时链接器)也必须处理对齐:否则如何尊重全局数据的对齐?
    • @BeeOnRope ELF/PE 文件格式指定了部分的对齐方式,加载器必须遵守它。在合并两个目标文件时,链接器也有类似的职责。然而,这不是字段或变量的对齐,因为存在高级概念。
    • 它与字段和变量的对齐方式直接相关:尝试使变量具有不同的对齐方式,并且节对齐方式发生变化。所以链接器和加载器需要成为对齐难题的一部分。当然,他们可能不知道为什么某个特定部分与某个值对齐,但他们仍然愿意提供帮助。
    • @BeeOnRope 我不确定 malloc(正如我所写),但实际上你的简单推理是正确的!关于 ELF,在再次阅读 ELF 文件格式后,我相信您又是对的 :) 这些部分必须具有虚拟地址和物理偏移量全等模 4KiB,但未在 4KiB 上对齐。链接器必须绝对能够处理对齐(如简单的readelf -s 所示),但它仍然是编译器为链接器提供正确的对齐方式以供使用,所以我喜欢将其视为一个不可知的特性(很像and 指令)。感谢您的反馈!
    【解决方案3】:

    答案是编译器和链接器0都需要了解和处理对齐要求。编译器是一对中的智能,因为它只了解实际的结构、堆栈和变量对齐规则——但它会将一些关于所需对齐的信息传播给链接器,链接器在生成最终可执行文件。

    编译器会处理大量运行时对齐处理,相反,它也经常依赖于满足某些最小对齐的事实1。此处的现有答案涵盖了编译器在某些细节上所做的事情。

    缺少的是链接器和加载器框架也处理对齐。一般来说,每个部分都有一个最小对齐属性,链接器写入该属性并且加载器尊重它,确保该部分加载到至少与该属性一样对齐的边界上。

    不同的部分会有不同的要求,对代码的更改会直接影响这些要求。一个简单的例子是全局数据,无论是在.bss.rodata.data 还是其他一些部分。这些部分的对齐至少与其中存储的任何对象的最大对齐要求一样大。

    因此,如果您有一个具有 64 字节对齐的只读 (const) 全局对象,则您的 .rodata 部分将具有 64 字节的最小对齐,并且链接器将确保满足此要求。

    您可以使用objdump -hAlgn 列中查看任何对象文件的实际对齐要求。这是一个随机的例子:

    Sections:
    Idx Name          Size      VMA               LMA               File off  Algn  Flags
      0 .interp       0000001c  0000000000400238  0000000000400238  00000238  2**0  CONTENTS, ALLOC, LOAD, READONLY, DATA
      1 .note.ABI-tag 00000020  0000000000400254  0000000000400254  00000254  2**2  CONTENTS, ALLOC, LOAD, READONLY, DATA
      2 .note.gnu.build-id 00000024  0000000000400274  0000000000400274  00000274  2**2  CONTENTS, ALLOC, LOAD, READONLY, DATA
      3 .gnu.hash     00000030  0000000000400298  0000000000400298  00000298  2**3  CONTENTS, ALLOC, LOAD, READONLY, DATA
      4 .dynsym       00000288  00000000004002c8  00000000004002c8  000002c8  2**3  CONTENTS, ALLOC, LOAD, READONLY, DATA
      5 .dynstr       00000128  0000000000400550  0000000000400550  00000550  2**0  CONTENTS, ALLOC, LOAD, READONLY, DATA
      6 .gnu.version  00000036  0000000000400678  0000000000400678  00000678  2**1  CONTENTS, ALLOC, LOAD, READONLY, DATA
      7 .gnu.version_r 00000050  00000000004006b0  00000000004006b0  000006b0  2**3  CONTENTS, ALLOC, LOAD, READONLY, DATA
      8 .rela.dyn     00000060  0000000000400700  0000000000400700  00000700  2**3  CONTENTS, ALLOC, LOAD, READONLY, DATA
      9 .rela.plt     00000210  0000000000400760  0000000000400760  00000760  2**3  CONTENTS, ALLOC, LOAD, READONLY, DATA
     10 .init         0000001a  0000000000400970  0000000000400970  00000970  2**2  CONTENTS, ALLOC, LOAD, READONLY, CODE
     11 .plt          00000170  0000000000400990  0000000000400990  00000990  2**4  CONTENTS, ALLOC, LOAD, READONLY, CODE
     12 .plt.got      00000008  0000000000400b00  0000000000400b00  00000b00  2**3  CONTENTS, ALLOC, LOAD, READONLY, CODE
     13 .text         000021e2  0000000000400b10  0000000000400b10  00000b10  2**4  CONTENTS, ALLOC, LOAD, READONLY, CODE
     14 .fini         00000009  0000000000402cf4  0000000000402cf4  00002cf4  2**2  CONTENTS, ALLOC, LOAD, READONLY, CODE
     15 .rodata       00000700  0000000000402d00  0000000000402d00  00002d00  2**5  CONTENTS, ALLOC, LOAD, READONLY, DATA
     16 .eh_frame_hdr 000000b4  0000000000403400  0000000000403400  00003400  2**2  CONTENTS, ALLOC, LOAD, READONLY, DATA
     17 .eh_frame     000003d4  00000000004034b8  00000000004034b8  000034b8  2**3  CONTENTS, ALLOC, LOAD, READONLY, DATA
     18 .init_array   00000008  0000000000603e10  0000000000603e10  00003e10  2**3  CONTENTS, ALLOC, LOAD, DATA
     19 .fini_array   00000008  0000000000603e18  0000000000603e18  00003e18  2**3  CONTENTS, ALLOC, LOAD, DATA
     20 .jcr          00000008  0000000000603e20  0000000000603e20  00003e20  2**3  CONTENTS, ALLOC, LOAD, DATA
     21 .dynamic      000001d0  0000000000603e28  0000000000603e28  00003e28  2**3  CONTENTS, ALLOC, LOAD, DATA
     22 .got          00000008  0000000000603ff8  0000000000603ff8  00003ff8  2**3  CONTENTS, ALLOC, LOAD, DATA
     23 .got.plt      000000c8  0000000000604000  0000000000604000  00004000  2**3  CONTENTS, ALLOC, LOAD, DATA
     24 .data         00000020  00000000006040d0  00000000006040d0  000040d0  2**4  CONTENTS, ALLOC, LOAD, DATA
     25 .bss          000001c8  0000000000604100  0000000000604100  000040f0  2**5  ALLOC
     26 .comment      00000034  0000000000000000  0000000000000000  000040f0  2**0  CONTENTS, READONLY
    

    这里的对齐要求从2**0(不需要对齐)到2**5(在32字节边界上对齐)不等。

    除了您提到的候选者之外,运行时 还需要具有对齐意识。这个话题有点复杂,但基本上你可以确定malloc 和相关函数返回适合任何基本类型对齐的内存(这通常意味着在 64 位系统上对齐 8 字节),尽管things get more complicated 当你是谈论过度对齐的类型,或 C++ alignas


    0 我最初只是将(编译时)链接器和(运行时)加载器组合在一起,因为它们实际上是同一枚硬币的两个方面(实际上大部分链接实际上是运行时链接)。然而,在更仔细地研究加载过程之后,加载器似乎可能只是在它们现有的文件偏移处加载段(节),自动尊重链接器设置的对齐方式。

    1 在 x86 这样通常允许未对齐访问的平台上,情况就不那么好了,但在对齐限制更严格的平台上,如果遇到不正确的对齐,代码实际上可能会失败。

    【讨论】:

      猜你喜欢
      • 2012-05-05
      • 1970-01-01
      • 1970-01-01
      • 2014-02-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多