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



数据结构对齐的任务与谁相对应?像X86一样,是编译器,链接器,加载程序或硬件本身吗?编译器是否进行相对对齐的地址,以便当链接器正确地将链接器正确地放置在汇编的可执行文件中时,数据结构始终与各自的天然大小边界保持一致?加载程序还必须执行什么其他任务?

答案是 compiler and linker 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和相关功能返回适合任何基本类型的存储器(通常仅表示8个字节在64位系统上),尽管当您说话时情况会变得更加复杂关于过度对准类型,或C alignas


0 我最初只是将(编译时)链接器和(运行时)加载程序分组在一起,因为它们实际上是同一枚硬币的两个方面(实际上,许多链接实际上是运行时链接)。但是,在更仔细地查看加载过程之后,似乎加载程序可以在其现有文件偏移量处加载片段(部分),从而自动尊重链接器设置的对齐。

1 在X86之类的平台上通常允许访问不一致,但是在对齐限制的平台上更严格,如果遇到不正确的对齐,代码实际上可能会失败。

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

这就是为什么必须有各种#pragma s和其他编译器级魔术旋钮,您可以扭曲以控制对齐。

我认为C语言不指定其他组件的存在(Linker和Loader)。

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

以下两个代码是从相同函数生成的,但具有不同的对齐方式(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"一个小型编译器。

加载程序在程序数据上无能为力 - 它必须加载任何程序,无论他们如何访问数据,试图将代码和数据视为不透明。
特别是,加载程序通常填充或重写可执行的元数据,而不是代码或数据。
每次读取一个结构字段的代码都会通过Meta-Data 进行,这根本没有理由是巨大的性能。

硬件没有结构的概念,也没有程序的意图。
当指示从 x 读取时,它将尽最大迅速,正确地从 x 读取它,但它不会为 x
硬件可以做到要做的事情。
如果不能,条件会发出信号。X86架构的对齐要求非常轻松,费用可能使操作的潜伏期加倍(或最坏)。


编译器负有整合数据的责任。
这样做时方便的两个引理是 1

  • if and object a 相对于A y-y-Aligned object b >和 x | y y x 的倍数)然后关于 b 的相同参考。

    例如,可以在特定边界(8个字节,16个字节,4Kib等)对齐PE/ELF文件中的部分(甚至有点malloc D缓冲区)。
    如果将部分加载在4KIB处对齐,则最多2 12 的所有两个对齐能力被自动尊重,即使在记忆中,它们也相对于本节的开始,不管该部分的加载地点。

  • 在缓冲区中 b 的长度 2x-1 至少有一个地址 a x-对齐,因此 2x-1 - ( a - b )> = x (it有足够的空间容纳大小 x 的对象)。

    如果您需要在8 Bytes边界处对象对象,并且该对象的长度为8 by(通常为),则分配一个16-1 = 15字节的缓冲区将保证存在适当的地址。em>每个可能的开始地址。

多亏了这两个引理和与装载机的既定惯例,编译器可以履行其职责而无需接触其他工具。


1 给出而没有太多解释。

最新更新