数据结构对齐的任务与谁相对应?像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 给出而没有太多解释。