【发布时间】:2017-07-15 02:18:41
【问题描述】:
数据结构对齐对应于谁的任务?是编译器、链接器、加载器还是硬件本身,就像 x86 一样?编译器是否进行相对对齐的寻址,这样当链接器在编译的可执行文件中正确“放置”时,数据结构总是与各自的本机大小边界对齐?装载机以后还要做什么工作?
【问题讨论】:
标签: c linker x86 operating-system executable
数据结构对齐对应于谁的任务?是编译器、链接器、加载器还是硬件本身,就像 x86 一样?编译器是否进行相对对齐的寻址,这样当链接器在编译的可执行文件中正确“放置”时,数据结构总是与各自的本机大小边界对齐?装载机以后还要做什么工作?
【问题讨论】:
标签: c linker x86 operating-system executable
我认为正确的最短答案:这是编译器的工作。
这就是为什么有各种 #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
虽然此示例涉及堆栈的对齐方式,但其目的是显示出现的复杂情况。
结构对齐的工作原理相同。
为了将此责任推给链接器,编译器必须生成临时代码和大量元数据,以便链接器可以修补必要的指令。
适应有限的链接器接口会导致生成次优代码。
丰富链接器功能会将编译器-链接器边界向左移动,从而有效地使后者“排序”为小型编译器。
加载程序无法处理程序数据 - 无论程序如何访问数据,它都必须加载任何程序,并尽可能将代码和数据视为不透明。
特别是,加载程序通常填充或重写可执行元数据,而不是代码或数据。
让代码每次通过元数据读取结构字段将是一个巨大的性能损失,完全没有理由。
硬件没有结构的概念,也没有程序的意图。
当被指示从 X 读取时,它会尽可能快且正确地从 X 读取,但它不会赋予该 X任何意义>.
硬件按要求执行。
如果不能,则发出条件信号。 x86 架构的对齐要求非常宽松,但代价是操作延迟可能会加倍(或最差)。
编译器全权负责对齐数据。
这样做时会派上用场的两个引理是1:
如果和对象 a 是 X-aligned 相对于 Y-aligned 对象 b 和 X | Y(Y 是 X 的倍数)然后 a 是 X 对齐关于 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没有过多解释。
【讨论】:
readelf -s 所示),但它仍然是编译器为链接器提供正确的对齐方式以供使用,所以我喜欢将其视为一个不可知的特性(很像and 指令)。感谢您的反馈!
答案是编译器和链接器0都需要了解和处理对齐要求。编译器是一对中的智能,因为它只了解实际的结构、堆栈和变量对齐规则——但它会将一些关于所需对齐的信息传播给链接器,链接器在生成最终可执行文件。
编译器会处理大量运行时对齐处理,相反,它也经常依赖于满足某些最小对齐的事实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 和相关函数返回适合任何基本类型对齐的内存(这通常意味着在 64 位系统上对齐 8 字节),尽管things get more complicated 当你是谈论过度对齐的类型,或 C++ alignas。
0 我最初只是将(编译时)链接器和(运行时)加载器组合在一起,因为它们实际上是同一枚硬币的两个方面(实际上大部分链接实际上是运行时链接)。然而,在更仔细地研究加载过程之后,加载器似乎可能只是在它们现有的文件偏移处加载段(节),自动尊重链接器设置的对齐方式。
1 在 x86 这样通常允许未对齐访问的平台上,情况就不那么好了,但在对齐限制更严格的平台上,如果遇到不正确的对齐,代码实际上可能会失败。
【讨论】: