数据结构对齐:链接器或编译器

Data Structure Alignment : Linker or Compiler

数据结构对齐对应的是谁的任务?是编译器、链接器、加载器还是硬件本身,比如 x86?编译器是否进行相对对齐寻址,这样当编译后的可执行文件中的链接器正确 'placed' 时,数据结构始终与各自的本机大小边界对齐?加载器以后还有什么任务要做?

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

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

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

数据对齐与代码生成紧密结合。
考虑生成具有在某些边界上对齐的局部变量的函数的序言和结尾的所有负担[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

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

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

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

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


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

  • 如果对象 a 相对于 Y 对齐 X 对齐 对象 bX | YYX的倍数)然后a 相对于 b 的相同参考 X 对齐

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

  • 在长度为2X-1的缓冲区B中至少有一个地址 AX 对齐 并且 2X-1 - (A -B) >= X (它有足够的 space 来容纳大小为 X[=85 的对象=]).

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

多亏了这两个引理和与加载程序的既定约定,编译器可以在不接触其他工具的情况下完成其职责。


1 没有过多解释。

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

编译器负责大量运行时对齐处理,相反,也经常依赖满足某些最小对齐的事实1。此处的现有答案详细介绍了编译器的功能。

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

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

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

您可以使用 objdump -h 查看 Algn 列中任何目标文件的实际对齐要求。这是一个随机示例:

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 和相关函数 return 内存适合任何基本类型对齐(这通常只意味着 64 位系统上的 8 字节对齐),虽然things get more complicated 当你谈论过度对齐类型或 C++ alignas.


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

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