在研究内核构建系统时,我注意到在 v4.19 之前,内核使用增量链接 ( ld -r
),然后它转移到精简存档 ( ar T
),如下所示:以下内核 Makefile 术语之间有什么区别:vmLinux、vmlinuz、vmlinux.bin、zimage 和 bzimage?我注意到
然后,我尝试制作一个综合增量链接基准测试,以查看链接加速是否相当大:https://stackoverflow.com/questions/29391965/what-is-partial-linking-in-gnu-linker/53959624#53959624但这不是我的基准。
因此,我的问题是:为什么内核使用增量链接或精简档案?
是为了加快构建速度还是出于其他原因?
哪个提交引入了增量链接?这样我就能够找出其中的基本原理了git log
。我发现使用git log --grep 'thin archive'
(a5967db9af51a84f5e181600954714a9e4c69f1f) 移动到精简档案,但无法轻松 grep 增量链接档案。
如果它的存在是为了加速构建,是否有一种方法可以快速测试与 vs 的链接而不需要增量链接来查看加速?
答案1
档案薄弱的原因
我通过电子邮件联系了尼古拉斯·皮金(Nicholas Piggin),他是补丁的作者之一,他解释说,精简档案不仅可以减少磁盘使用,还可以防止链接故障。
问题是增量链接的对象文件可能变得如此之大,以至于链接器甚至无法插入蹦床重定位,它必须指向对象之间生成的代码。
我还没有得到关于增量构建的理由的答复。
这是他精彩的回复:
这是一个相当长的答案,具体取决于您知道多少。有几个原因。 Stephen 制作该补丁的主要动机是允许非常大的内核成功链接。
其他一些好处是:
这是存储中间构建工件的“更好”方式,您将输出代码保存在一个位置并使用引用(精简档案)跟踪它们,直到它们全部链接在一起。因此,所需的 IO 和磁盘空间更少,特别是对于大型构建和调试信息。
对于只构建少量输出目录的普通现代工作站来说,Linux 并不是一个巨大的项目,这一切都将在缓存中,并且增量链接文件的时间非常快。因此对于 Linux 来说,构建速度的优势通常很小。
它允许链接器生成稍微更好的代码。通过重新排列文件并更优化地定位链接器存根。
尽管对上游 LTO 构建的支持还不够多,但它往往可以更好地与 LTO 构建配合使用。
但我们还是回到最初的动机。
当您构建尚未最终链接的可重定位目标文件时,您将获得一堆代码,其中包含对在其他地方定义的函数和变量的符号的引用。
--- a.S --- bl myfunc ---
组装成
a.o: file format elf64-powerpcle
.text 节的反汇编:
0000000000000000 <.text>: 0: 01 00 00 48 bl 0x0
所以代码有一个到NIA+0(即它本身)的分支,这不是我们所要求的。转储重定位显示丢失的位:
.text 节的反汇编:
0000000000000000 <.text>: 0: 01 00 00 48 bl 0x0 0: R_PPC64_REL24 myfunc
重定位不在 .text 部分,它不是代码,而是一些 ELF 元数据,它表示该位置的指令与名为 myfunc 的符号有 24 位相对偏移量。
当您对对象进行“最终链接”时,文件基本上连接在一起,并且通过调整代码和数据以指向正确的位置来解决这些重定位问题。
将 aS 与包含 myfunc 符号的 bS 链接起来会得到以下结果:
c: file format elf64-powerpcle Disassembly of section .text: 00000000100000d8 <_start>: 100000d8: 05 00 00 48 bl 100000dc <myfunc> 00000000100000dc <myfunc>: 100000dc: 01 00 63 38 addi r3,r3,1 100000e0: 20 00 80 4e blr
重定位元数据被剥离,分支指向正确的偏移量。
因此链接器实际上在链接时调整指令。它更进一步,它生成指令。如果您有一个大型构建,并且该分支无法以 24 位偏移量到达 myfunc,则链接器会将蹦床(又名存根、PLT、过程链接表)放入可以以 24 位访问的代码中,然后将蹦床使用可以到达目标的较长分支。
链接器不能将这些蹦床放在代码中的任何位置,因为如果您在代码中间添加某些内容,则会破坏穿过中间的相对引用。链接器不知道 全部.o 文件中的引用,仅限未解析的引用。因此,链接器必须仅在将 .o 文件链接在一起时在解析它们的引用之前在 .o 文件之间放置蹦床。
当您接近构建目录的根目录时,旧的增量构建方法只是将 .o 文件合并为更大的 .o 文件。因此,当您的 .o 文件变得太大以至于分支无法到达其自己的 .o 文件之外以到达蹦床时,您就会遇到问题。没有办法解决这个引用。
对于精简档案,最终链接是在数千个非常小的 .o 文件上完成的。这为链接器放置这些蹦床提供了最大的灵活性,这意味着您永远不会遇到此限制。
答案2
对于“为什么?”这个问题我没有答案。你的问题的一部分,但至少从 Linux 0.97(1992 年 8 月 1 日)开始使用增量链接:
OBJS= namei.o inode.o file.o dir.o misc.o fat.o
msdos.o: $(OBJS)
$(LD) -r -o msdos.o $(OBJS)
或者
OBJS= bitmap.o freelists.o truncate.o namei.o inode.o \
file.o dir.o symlink.o blkdev.o chrdev.o fifo.o
ext.o: $(OBJS)
$(LD) -r -o ext.o $(OBJS)
https://github.com/mpe/linux-fullhistory/commit/e60feb868bfa9d248c71a1a3bdd8c2857f1d433d
然而,这些古老的提交并没有提到基本原理,所以你可能不得不问 Linus 到底为什么这样做而不是一次性链接起来。我怀疑这主要是为了保持构建系统良好的模块化,而不是用一个巨大的链接线列出所有对象。
如果您现在想更改它,您必须提出一个非常有力的案例,因为对构建系统进行如此大的结构更改不会只是为了好玩。