数据结构对齐:链接器或编译器
Data Structure Alignment : Linker or Compiler
数据结构对齐对应的是谁的任务?是编译器、链接器、加载器还是硬件本身,比如 x86?编译器是否进行相对对齐寻址,这样当编译后的可执行文件中的链接器正确 'placed' 时,数据结构始终与各自的本机大小边界对齐?加载器以后还有什么任务要做?
我认为正确的最短答案:这是编译器的工作。
这就是为什么有各种 #pragma
s 和其他编译器级别的魔术旋钮,您可以在必要时转动它们来控制对齐。
我不认为 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 对齐 对象 b 和 X | Y(Y是X的倍数)然后a 相对于 b 的相同参考 X 对齐。
例如,PE/ELF 文件(甚至 malloc
d 缓冲区)中的部分可以按特定边界(8 字节、16 字节、4KiB 等)对齐加载.
如果一个部分以 4KiB 对齐加载,那么一旦在内存中,所有 212 的二次方对齐都会自动得到遵守,即使它们是相对于开始的部分,无论该部分在哪里加载。
在长度为2X-1的缓冲区B中至少有一个地址 A 即 X 对齐 并且 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 这样通常允许未对齐访问的平台上不太如此,但在对齐限制更严格的平台上,如果遇到不正确的对齐,代码实际上可能会失败。
数据结构对齐对应的是谁的任务?是编译器、链接器、加载器还是硬件本身,比如 x86?编译器是否进行相对对齐寻址,这样当编译后的可执行文件中的链接器正确 'placed' 时,数据结构始终与各自的本机大小边界对齐?加载器以后还有什么任务要做?
我认为正确的最短答案:这是编译器的工作。
这就是为什么有各种 #pragma
s 和其他编译器级别的魔术旋钮,您可以在必要时转动它们来控制对齐。
我不认为 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 对齐 对象 b 和 X | Y(Y是X的倍数)然后a 相对于 b 的相同参考 X 对齐。
例如,PE/ELF 文件(甚至
malloc
d 缓冲区)中的部分可以按特定边界(8 字节、16 字节、4KiB 等)对齐加载.
如果一个部分以 4KiB 对齐加载,那么一旦在内存中,所有 212 的二次方对齐都会自动得到遵守,即使它们是相对于开始的部分,无论该部分在哪里加载。在长度为2X-1的缓冲区B中至少有一个地址 A 即 X 对齐 并且 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 这样通常允许未对齐访问的平台上不太如此,但在对齐限制更严格的平台上,如果遇到不正确的对齐,代码实际上可能会失败。